11"use strict" ;
22
3- const { dialog } = require ( 'electron' )
4- const pdfjsLib = require ( "pdfjs-dist/legacy/build/pdf.js" ) ;
53const fs = require ( 'fs' ) ;
64const path = require ( 'path' ) ;
7- const Store = require ( 'electron-store ' ) ;
5+ const { dialog } = require ( 'electron' ) ;
86const { exec } = require ( 'child_process' ) ;
9-
7+ const pdfjsLib = require ( "pdfjs-dist/legacy/build/pdf.js" ) ;
8+ const { PDFDocument } = require ( 'pdf-lib' ) ;
9+ const Store = require ( 'electron-store' ) ;
1010const log = require ( 'electron-log' ) ;
11+ const fns = require ( 'date-fns' )
12+
13+
1114log . transports . console . format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {text}' ;
1215
1316// Define the configuration data scheme. This sceheme will be store persistently.
1417const configSchema = {
1518 config : {
1619 type : "object" ,
17- required : [ "ver" , "directoryList" , "enableRecursive" , "showRelativePath" , "showTitle" , "trimNewline" ] ,
20+ required : [ "ver" , "directoryList" , "pdfAppExecutablePath" , " enableRecursive", "showRelativePath" , "showTitle" , "trimNewline" , "pageLength" , "showModificationDate" , "groupAnnotations" , "sortGroupAsc "] ,
1821 properties : {
19- ver : { type : "string" , default : "0.5.2 " } , // Config version -> for future update checking
22+ ver : { type : "string" , default : "0.8.5 " } , // Config version -> for future update checking
2023 directoryList : { type : "array" , default : [ ] } ,
2124 pdfAppExecutablePath : { type : "string" , default : "" } ,
2225 enableRecursive : { type : "boolean" , default : true } ,
2326 showRelativePath : { type : "boolean" , default : true } ,
2427 showTitle : { type : "boolean" , default : true } ,
2528 trimNewline : { type : "boolean" , default : true } ,
2629 pageLength : { type : "number" , default : 10 , maximum : 100 , minimum : - 1 } ,
30+ showModificationDate : { type : "boolean" , default : false } ,
31+ groupAnnotations : { type : "boolean" , default : true } ,
32+ sortGroupAsc : { type : "boolean" , default : true }
2733 } ,
2834 default : { }
2935 }
3036} ;
3137const store = new Store ( { schema : configSchema , clearInvalidConfig : true } ) ;
3238
39+ const standardFontDataUrl = path . join ( __dirname , '../../node_modules/pdfjs-dist/standard_fonts/' ) ;
40+
41+ const errorDateValue = "N/A" ; // When date parsing failed.
3342
3443// Recursive loop to get all files and directories
3544const getAllPdfFiles = async ( parentDirectory , directoryList , enableRecursive , pdfFileList , statCounter ) => {
@@ -115,7 +124,8 @@ const readPdfDocument = async (config, documentPath) => {
115124 let currentPDFannotation = [ ] ;
116125
117126 // Read PDF document
118- let documentObject = pdfjsLib . getDocument ( documentPath ) ;
127+ // Set standardFontDataUrl to avoid warning on missing fonts
128+ let documentObject = pdfjsLib . getDocument ( { url : documentPath , standardFontDataUrl : standardFontDataUrl } ) ;
119129 let pdf = await documentObject . promise ;
120130
121131 // Set the PDF's title as document name if showTitle is enabled
@@ -139,15 +149,19 @@ const readPdfDocument = async (config, documentPath) => {
139149
140150 /* DataTable's array format:
141151 0 - Full PDF File Path (including PDF Filename)
142- 1 - PDF Filename / Document Title
143- 2 - Page Number
144- 3 - Annotation Type
145- 4 - Annotation Content
152+ 1 - Annotation Counter - for sorting (value is assigned after the return)
153+ 2 - PDF Filename / Document Title
154+ 3 - Page Number
155+ 4 - Annotation Type
156+ 5 - Annotation Content
157+ 6 - Creation Date / Modification Date
146158 */
147159
148160 for ( const item of currentPageAnnotationList ) {
149- currentPDFannotation . push ( [ documentPath , documentName , currentPageNo , item [ 0 ] , item [ 1 ] ] ) ;
161+ currentPDFannotation . push ( [ documentPath , null , documentName , currentPageNo , item [ 0 ] , item [ 1 ] , parsePdfDate ( item [ 2 ] ) ] ) ;
162+
150163 }
164+
151165 }
152166
153167 return currentPDFannotation ;
@@ -169,16 +183,23 @@ const getPageAnnotation = async (pageContent, config) => {
169183 if ( annotationRawList [ i ] . subtype == "Highlight" ) {
170184 // Get annotate string
171185 let annotateText = annotationRawList [ i ] . contentsObj . str ;
186+
187+ // Get creationDateTime / modificationDateTime
188+ let dateTime = ( config . showModificationDate === true ) ? annotationRawList [ i ] . modificationDate : annotationRawList [ i ] . creationDate ;
189+
172190 if ( annotateText . length > 0 ) {
173191 if ( config . trimNewline === true ) {
174- pageAnnotationList . push ( [ "Comment" , annotateText . replace ( / (?: \r \n | \r | \n ) / g, " " ) ] ) ;
192+ pageAnnotationList . push ( [ `<span class="material-icons-round table-comment">chat</span>` , annotateText . replace ( / (?: \r \n | \r | \n ) / g, " " ) , dateTime ] ) ;
175193 } else { // Convert newline char to <br> tags
176- pageAnnotationList . push ( [ "Comment" , annotateText . replace ( / (?: \r \n | \r | \n ) / g, "<br>" ) ] ) ;
194+ pageAnnotationList . push ( [ `<span class="material-icons-round table-comment">chat</span>` , annotateText . replace ( / (?: \r \n | \r | \n ) / g, "<br>" ) , dateTime ] ) ;
177195 }
178196
179197 } else {
198+
180199 // No annotate string, so this is highlight only without comment
181200 // Store the rectangle dimensions value
201+ // Before store, insert creationDateTime / modificationDateTime as the 5th item
202+ annotationRawList [ i ] . rect . push ( dateTime ) ;
182203 annotationRectangleList . push ( annotationRawList [ i ] . rect ) ;
183204 }
184205 }
@@ -214,7 +235,7 @@ const getPageAnnotation = async (pageContent, config) => {
214235 textArray . push ( currentText . text ) ;
215236 }
216237 }
217- pageAnnotationList . push ( [ "Highlight" , textArray . join ( ' ' ) ] ) ;
238+ pageAnnotationList . push ( [ `<span class="material-icons-round table-highlight">border_color</span>` , textArray . join ( ' ' ) , rectangleDimensions [ 4 ] ] ) ; // [4] is the creationDate / modificationDate
218239 }
219240 }
220241 }
@@ -223,6 +244,27 @@ const getPageAnnotation = async (pageContent, config) => {
223244}
224245
225246
247+ const parsePdfDate = ( dateString ) => {
248+
249+ // Ensure date string is non-empty
250+ if ( dateString === undefined || dateString === null || typeof dateString !== "string" || dateString === "" ) {
251+ return errorDateValue ;
252+ }
253+
254+ try {
255+ // Slice string for primary data without unrelated token
256+ // Slice based on -> D:YYYYMMDDHHmmSSOHH'mm'. Refer: https://www.verypdf.com/pdfinfoeditor/pdf-date-format.htm
257+ dateString = dateString . slice ( 2 , 19 ) + dateString . slice ( 20 , 22 ) ;
258+ let date = fns . parse ( dateString , "yyyyMMddHHmmssxx" , new Date ( ) ) ; // Refer: https://date-fns.org/v2.29.3/docs/parse
259+
260+ return date . toLocaleString ( ) . toUpperCase ( ) . replace ( ", " , "<br>" ) ;
261+ } catch ( error ) {
262+ console . error ( error ) ;
263+ return errorDateValue ;
264+ }
265+ }
266+
267+
226268module . exports = {
227269 getConfig : ( event , reset ) => { // Read config file
228270 // Reset config if set
@@ -270,24 +312,27 @@ module.exports = {
270312 } ,
271313 getAllPdfAnnotation : async ( event , directoryData , config , frontend ) => { // Iterate over list file to read each PDF
272314
273- let current = 1 ;
274-
315+ let currentPdfCount = 1 ;
316+ let currentAnnotationCount = 1 ;
275317 let allAnotationList = [ ]
276318 let failedFileList = [ ]
277319 // Iterate over each PDF file
278320 for ( const file of directoryData . pdfFileList ) {
279321
280- frontend . setStatus ( `Reading PDF Document: ${ current } /${ directoryData . statCounter . file } (${ ( current / directoryData . statCounter . file * 100 ) . toFixed ( 1 ) } %)` ) ;
322+ frontend . setStatus ( `Reading PDF Document: ${ currentPdfCount } /${ directoryData . statCounter . file } (${ ( currentPdfCount / directoryData . statCounter . file * 100 ) . toFixed ( 1 ) } %)` ) ;
281323 try {
282324 let documentAnnotationList = await readPdfDocument ( config , file ) ;
283325 for ( const annotationItem of documentAnnotationList ) {
326+ // Add annotation counter value
327+ annotationItem [ 1 ] = currentAnnotationCount ++ ;
328+
284329 allAnotationList . push ( annotationItem ) ;
285330 }
286331 } catch ( error ) {
287332 log . error ( file , error ) ;
288333 failedFileList . push ( file ) ;
289334 }
290- current ++ ;
335+ currentPdfCount ++ ;
291336 }
292337
293338 return { annotationList : allAnotationList , failedList : failedFileList } ;
@@ -319,6 +364,92 @@ module.exports = {
319364 log . error ( error ) ;
320365 return { success : false , reason : "Unable to open the PDF file: " + documentPath } ;
321366 }
367+ } ,
368+ getEditorInfo : async ( event , documentPath ) => { // Get PDF document info for editor
369+
370+ // Seperate exist check, access check and document read for clearer error info
371+ try {
372+ // Check existence
373+ if ( fs . existsSync ( documentPath ) !== true ) {
374+ return { success : false , reason : "Unable to find the PDF document file." } ;
375+ }
376+
377+ // Check read permission
378+ await fs . promises . access ( documentPath , fs . constants . R_OK ) ;
379+ } catch ( error ) {
380+ log . error ( error ) ;
381+ return { success : false , reason : "Unable to access the PDF document file." } ;
382+ }
383+
384+ try {
385+ // Read document
386+ const contents = await fs . promises . readFile ( documentPath ) ;
387+ const pdfDoc = await PDFDocument . load ( contents ) ;
388+ const documentTitle = pdfDoc . getTitle ( ) ;
389+
390+ if ( documentTitle === undefined ) {
391+ return { success : true , documentTitle : "" } ;
392+ } else {
393+ return { success : true , documentTitle : documentTitle } ;
394+ }
395+ } catch ( error ) {
396+ log . error ( error ) ;
397+ return { success : false , reason : "Unable to read the PDF document file." } ;
398+ }
399+ } ,
400+ saveEditorInfo : async ( event , documentPath , newInfo ) => { // Save PDF document info from editor
401+
402+ // Seperate exist check, access check and document write for clearer error info
403+ try {
404+ // Check existence
405+ if ( fs . existsSync ( documentPath ) !== true ) {
406+ return { success : false , reason : "Failed to find the PDF document file." } ;
407+ }
408+
409+ // Check write permission
410+ await fs . promises . access ( documentPath , fs . constants . W_OK ) ;
411+ } catch ( error ) {
412+ log . error ( error ) ;
413+ return { success : false , reason : "Failed to write the PDF document file. Close any application that is open the PDF document file." } ;
414+ }
415+
416+ // Read document
417+ let contents = null ;
418+ let pdfDoc = null ;
419+
420+ try {
421+ // Read document
422+ contents = await fs . promises . readFile ( documentPath ) ;
423+ pdfDoc = await PDFDocument . load ( contents ) ;
424+
425+ // Set title
426+ pdfDoc . setTitle ( newInfo . documentTitle ) ;
427+
428+ // Set PDF producer
429+ // pdfDoc.setProducer((pdfDoc.getProducer() === undefined) ? "" : pdfDoc.getProducer());
430+
431+ } catch ( error ) {
432+ log . error ( error ) ;
433+ return { success : false , reason : "Failed to save the PDF document's title." } ;
434+ }
435+
436+ // Made backup copy first
437+ try {
438+ await fs . promises . copyFile ( documentPath , documentPath + ".backup" , fs . constants . COPYFILE_EXCL ) ;
439+ } catch ( error ) {
440+ log . info ( error ) ;
441+ }
442+
443+
444+ try {
445+ // Save document
446+ await fs . promises . writeFile ( documentPath , await pdfDoc . save ( ) ) ;
447+
448+ return { success : true } ;
449+ } catch ( error ) {
450+ log . error ( error ) ;
451+ return { success : false , reason : "Failed to save the PDF document's title. Close any application that is open the PDF document file." } ;
452+ }
322453 }
323454
324455}
0 commit comments