1
1
import { PDFDocument , PDFName , PDFPage } from 'pdf-lib'
2
2
import { PDFViewerApplication } from 'pdfjs'
3
- import { IAnnotationStore , PdfjsAnnotationType } from '../const/definitions'
3
+ import { annotationDefinitions , CommentStatus , IAnnotationStore , PdfjsAnnotationType } from '../const/definitions'
4
4
import { TextParser } from './parse_text'
5
+ import ExcelJS from 'exceljs'
6
+ import { saveAs } from 'file-saver'
5
7
import { AnnotationParser } from './parse'
6
8
import { HighlightParser } from './parse_highlight'
7
9
import { UnderlineParser } from './parse_underline'
8
10
import { StrikeOutParser } from './parse_strikeout'
9
11
import { SquareParser } from './parse_square'
10
12
import { CircleParser } from './parse_circle'
11
13
import { InkParser } from './parse_ink'
12
- import { getTimestampString } from '../utils/utils'
14
+ import { formatPDFDate , getPDFDateTimestamp , getTimestampString } from '../utils/utils'
13
15
import { FreeTextParser } from './parse_freetext'
14
16
import { StampParser } from './parse_stamp'
15
17
import { LineParser } from './parse_line'
16
18
import { PolylineParser } from './parse_polyline'
19
+ import { t } from 'i18next'
17
20
18
21
// import { HighlightParser } from './parse_highlight' // future
19
22
// import { InkParser } from './parse_ink' // future
@@ -60,19 +63,18 @@ async function parseAnnotationToPdf(annotation: IAnnotationStore, page: PDFPage,
60
63
* @param filename - 下载时使用的文件名
61
64
*/
62
65
function downloadPdf ( data : Uint8Array , filename : string ) {
63
- // 获取 PDF 的有效 ArrayBuffer 内容,避免使用共享内存或偏移错误
66
+ // 提取安全的 ArrayBuffer
64
67
const arrayBuffer = data . buffer . slice ( data . byteOffset , data . byteOffset + data . byteLength )
65
- // 创建 Blob 并生成 URL 下载链接
68
+ // 创建 Blob
66
69
// @ts -ignore
67
70
const blob = new Blob ( [ arrayBuffer ] , { type : 'application/pdf' } )
68
- const url = URL . createObjectURL ( blob )
69
- const link = document . createElement ( 'a' )
70
- link . href = url
71
- link . download = filename
72
- document . body . appendChild ( link )
73
- link . click ( )
74
- document . body . removeChild ( link )
75
- URL . revokeObjectURL ( url )
71
+ // 使用 saveAs 下载
72
+ saveAs ( blob , `${ filename } .pdf` )
73
+ }
74
+
75
+ function downloadExcel ( data : any , filename : string ) {
76
+ const buffer = new Blob ( [ data ] , { type : 'application/octet-stream' } )
77
+ saveAs ( buffer , `${ filename } .xlsx` )
76
78
}
77
79
78
80
/**
@@ -107,12 +109,8 @@ async function exportAnnotationsToPdf(PDFViewerApplication: PDFViewerApplication
107
109
const response = await fetch ( PDFViewerApplication . _downloadUrl )
108
110
const pdfBytes = await response . arrayBuffer ( )
109
111
const pdfDoc = await PDFDocument . load ( pdfBytes )
110
-
111
112
// ✅ 清除原有的所有批注
112
113
clearAllAnnotations ( pdfDoc )
113
-
114
- console . log ( annotations )
115
-
116
114
// 遍历每一个注解并解析应用到对应页面
117
115
for ( const ann of annotations ) {
118
116
const page = pdfDoc . getPages ( ) [ ann . pageNumber - 1 ]
@@ -123,9 +121,199 @@ async function exportAnnotationsToPdf(PDFViewerApplication: PDFViewerApplication
123
121
const modifiedPdf = await pdfDoc . save ( )
124
122
// 使用 title + 时间戳作为文件名
125
123
const baseName = PDFViewerApplication . _title || 'annotated'
126
- const fileName = `${ baseName } - ${ getTimestampString ( ) } .pdf `
124
+ const fileName = `${ baseName } _ ${ getTimestampString ( ) } `
127
125
128
126
downloadPdf ( modifiedPdf , fileName )
129
127
}
130
128
131
- export { exportAnnotationsToPdf }
129
+ async function exportAnnotationsToExcel ( PDFViewerApplication : PDFViewerApplication , annotations : IAnnotationStore [ ] ) {
130
+ const rows : any [ ] = [ ]
131
+ // 先按页码升序,再按批注时间降序
132
+ annotations . sort ( ( a , b ) => {
133
+ if ( a . pageNumber !== b . pageNumber ) {
134
+ return a . pageNumber - b . pageNumber
135
+ }
136
+ return getPDFDateTimestamp ( b . date ) - getPDFDateTimestamp ( a . date )
137
+ } )
138
+ const getLastStatusName = ( annotation : IAnnotationStore ) : string => {
139
+ const lastWithStatus = [ ...( annotation . comments || [ ] ) ] . reverse ( ) . find ( c => c . status !== undefined && c . status !== null )
140
+
141
+ const status = lastWithStatus ?. status ?? CommentStatus . None
142
+ return t ( `comment.status.${ status . toLowerCase ( ) } ` )
143
+ }
144
+
145
+ let mainIndex = 1 // 主批注序号
146
+ let replyCounter : number = 0 // 回复计数器(每次主批注开始重置)
147
+
148
+ annotations . forEach ( annotation => {
149
+ const annotationName = annotationDefinitions . find ( def => def . type === annotation . type ) ?. name
150
+ const typeLabel = t ( `annotations.${ annotationName } ` )
151
+ // 主批注行
152
+ rows . push ( {
153
+ index : `${ mainIndex } ` ,
154
+ id : annotation . id ,
155
+ page : annotation . pageNumber ,
156
+ annotationType : typeLabel ,
157
+ recordType : t ( 'export.recordType.annotation' ) ,
158
+ author : annotation . title ,
159
+ content : annotation . contentsObj ?. text || '' ,
160
+ date : formatPDFDate ( annotation . date , true ) ,
161
+ status : getLastStatusName ( annotation )
162
+ } )
163
+ // 重置回复计数器
164
+ replyCounter = 0
165
+ // 回复行
166
+ annotation . comments . forEach ( comment => {
167
+ replyCounter ++
168
+ rows . push ( {
169
+ index : `${ mainIndex } .${ replyCounter } ` ,
170
+ id : comment . id ,
171
+ page : '' ,
172
+ annotationType : '--' ,
173
+ recordType : t ( 'export.recordType.reply' ) ,
174
+ author : comment . title ,
175
+ content : comment . content ,
176
+ date : formatPDFDate ( comment . date , true ) ,
177
+ status : ''
178
+ } )
179
+ } )
180
+ mainIndex ++
181
+ } )
182
+
183
+ // 创建 workbook 和 sheet
184
+ const workbook = new ExcelJS . Workbook ( )
185
+ const sheet = workbook . addWorksheet ( t ( 'export.sheetName' ) )
186
+
187
+ // 自定义列宽(单位为字符宽度)
188
+ sheet . columns = [
189
+ {
190
+ key : 'index' ,
191
+ header : '#' ,
192
+ width : 8 ,
193
+ style : {
194
+ alignment : { vertical : 'middle' }
195
+ }
196
+ } ,
197
+ {
198
+ key : 'id' ,
199
+ header : t ( 'export.fields.id' ) ,
200
+ width : 20 ,
201
+ style : {
202
+ alignment : {
203
+ vertical : 'middle'
204
+ }
205
+ }
206
+ } ,
207
+ {
208
+ key : 'page' ,
209
+ header : t ( 'export.fields.page' ) ,
210
+ width : 10 ,
211
+ style : {
212
+ alignment : {
213
+ vertical : 'middle'
214
+ }
215
+ }
216
+ } ,
217
+ {
218
+ key : 'annotationType' ,
219
+ header : t ( 'export.fields.annotationType' ) ,
220
+ width : 18 ,
221
+ style : {
222
+ alignment : {
223
+ vertical : 'middle'
224
+ }
225
+ }
226
+ } ,
227
+ {
228
+ key : 'recordType' ,
229
+ header : t ( 'export.fields.recordType' ) ,
230
+ width : 12 ,
231
+ style : {
232
+ alignment : {
233
+ vertical : 'middle'
234
+ }
235
+ }
236
+ } ,
237
+ {
238
+ key : 'author' ,
239
+ header : t ( 'export.fields.author' ) ,
240
+ width : 16 ,
241
+ style : {
242
+ alignment : {
243
+ vertical : 'middle'
244
+ }
245
+ }
246
+ } ,
247
+ {
248
+ key : 'content' ,
249
+ header : t ( 'export.fields.content' ) ,
250
+ width : 40 ,
251
+ style : {
252
+ alignment : {
253
+ wrapText : true ,
254
+ vertical : 'top'
255
+ }
256
+ }
257
+ } ,
258
+ {
259
+ key : 'date' ,
260
+ header : t ( 'export.fields.date' ) ,
261
+ width : 22 ,
262
+ style : {
263
+ alignment : {
264
+ vertical : 'middle'
265
+ }
266
+ }
267
+ } ,
268
+ {
269
+ key : 'status' ,
270
+ header : t ( 'export.fields.status' ) ,
271
+ width : 14 ,
272
+ style : {
273
+ alignment : {
274
+ vertical : 'middle'
275
+ }
276
+ }
277
+ }
278
+ ]
279
+
280
+ // 写入数据 + 样式
281
+ rows . forEach ( row => {
282
+ const addedRow = sheet . addRow ( row )
283
+ const isReply = row . recordType === t ( 'export.recordType.reply' )
284
+ addedRow . font = {
285
+ size : 12 ,
286
+ color : { argb : isReply ? '389e0d' : '000000' }
287
+ }
288
+ } )
289
+
290
+ // 表头样式
291
+ sheet . getRow ( 1 ) . eachCell ( cell => {
292
+ cell . font = { bold : true , size : 12 }
293
+ cell . fill = {
294
+ type : 'pattern' ,
295
+ pattern : 'solid' ,
296
+ fgColor : { argb : 'D9E1F2' }
297
+ }
298
+ } )
299
+
300
+ // 写入数据后给所有单元格加边框
301
+ sheet . eachRow ( row => {
302
+ row . eachCell ( cell => {
303
+ cell . border = {
304
+ top : { style : 'thin' , color : { argb : '000000' } } ,
305
+ left : { style : 'thin' , color : { argb : '000000' } } ,
306
+ bottom : { style : 'thin' , color : { argb : '000000' } } ,
307
+ right : { style : 'thin' , color : { argb : '000000' } }
308
+ }
309
+ } )
310
+ } )
311
+
312
+ // 导出
313
+ const buffer = await workbook . xlsx . writeBuffer ( )
314
+ const baseName = PDFViewerApplication . _title || 'annotated'
315
+ const fileName = `${ baseName } _${ getTimestampString ( ) } `
316
+ downloadExcel ( buffer , fileName )
317
+ }
318
+
319
+ export { exportAnnotationsToPdf , exportAnnotationsToExcel }
0 commit comments