@@ -82,6 +82,297 @@ function resetFile(pdfFile) {
8282 pdfFile = pdfFile . replace ( / \/ P r o d u c e r \( [ ^ ) ] + \) / , "/Producer (jsPDF 0.0.0)" ) ;
8383 return pdfFile ;
8484}
85+
86+ function findRepeatedPattern ( differences ) {
87+ if ( differences . length < 2 ) return null ;
88+
89+ // Group differences by type
90+ const groups = differences . reduce ( ( acc , diff ) => {
91+ const type = getPdfLineType ( diff . expected || diff . actual ) ;
92+ if ( ! acc [ type ] ) acc [ type ] = [ ] ;
93+ acc [ type ] . push ( diff ) ;
94+ return acc ;
95+ } , { } ) ;
96+
97+ // Find patterns within each group
98+ const patterns = [ ] ;
99+ for ( const [ type , diffs ] of Object . entries ( groups ) ) {
100+ if ( diffs . length > 3 ) {
101+ // Check if all differences in this group follow a similar pattern
102+ const firstDiff = diffs [ 0 ] ;
103+ const isConsistent = diffs . every ( diff =>
104+ ( diff . actual === firstDiff . actual && diff . expected === firstDiff . expected ) ||
105+ ( type === 'text-pos' && diff . actual ?. endsWith ( firstDiff . actual ?. split ( ' ' ) . pop ( ) ) && diff . expected ?. endsWith ( firstDiff . expected ?. split ( ' ' ) . pop ( ) ) )
106+ ) ;
107+
108+ if ( isConsistent ) {
109+ patterns . push ( {
110+ type,
111+ count : diffs . length ,
112+ sample : firstDiff ,
113+ startLine : firstDiff . line ,
114+ consistent : true
115+ } ) ;
116+ } else {
117+ patterns . push ( {
118+ type,
119+ count : diffs . length ,
120+ sample : firstDiff ,
121+ startLine : firstDiff . line ,
122+ consistent : false
123+ } ) ;
124+ }
125+ }
126+ }
127+
128+ return patterns . length > 0 ? patterns : null ;
129+ }
130+
131+ function getPdfLineType ( line ) {
132+ if ( ! line ) return 'unknown' ;
133+ // PDF structure elements
134+ if ( line . match ( / ^ \d + \d + o b j $ / ) ) return 'obj-header' ;
135+ if ( line . match ( / ^ \d { 10 } \d { 5 } [ f n ] $ / ) ) return 'xref-entry' ;
136+ if ( line . match ( / ^ e n d o b j | s t r e a m | e n d s t r e a m $ / ) ) return 'pdf-structure' ;
137+
138+ // Font-related elements
139+ if ( line . startsWith ( '/BaseFont' ) ) return 'font-def' ;
140+ if ( line . match ( / ^ \/ F \d + \d + \d + R $ / ) ) return 'font-ref' ;
141+ if ( line . match ( / ^ \/ F \d + \d + T f $ / ) ) return 'font-select' ;
142+
143+ // Text operations
144+ if ( line . match ( / ^ \d + \. \d + \d + \. \d + T d $ / ) ) return 'text-pos' ;
145+ if ( line . match ( / ^ B T | E T $ / ) ) return 'text-block' ;
146+ if ( line . match ( / ^ \d + \. \d + T L $ / ) ) return 'text-leading' ;
147+ if ( line . match ( / ^ \( \S + \) T j $ / ) ) return 'text-show' ;
148+
149+ // Graphics operations
150+ if ( line . match ( / ^ \d + \. \d + \d + \. \d + \d + \. \d + ( r g | R G ) $ / ) ) return 'color' ;
151+ if ( line . match ( / ^ [ Q q ] $ / ) ) return 'graphics-state' ;
152+ if ( line . match ( / ^ \d + \. \d + w $ / ) ) return 'line-width' ;
153+ if ( line . match ( / ^ \d + \. \d + \d + \. \d + [ m l ] $ / ) ) return 'path' ;
154+ if ( line . match ( / ^ [ W S F f B b ] \* ? $ / ) ) return 'path-op' ;
155+ if ( line . match ( / ^ [ 0 1 ] [ J L j ] $ / ) ) return 'style' ;
156+
157+ // Length and metadata
158+ if ( line . match ( / ^ \/ L e n g t h \d + $ / ) ) return 'length' ;
159+ if ( line . match ( / ^ \/ P r o d u c e r | \/ C r e a t i o n D a t e | \/ C r e a t o r / ) ) return 'metadata' ;
160+
161+ return 'unknown' ;
162+ }
163+
164+ function compareArrays ( actual , expected ) {
165+ let differences = [ ] ;
166+ let consecutiveDiffs = 0 ;
167+ let currentBlock = [ ] ;
168+
169+ for ( let i = 0 ; i < Math . max ( actual . length , expected . length ) ; i ++ ) {
170+ if ( actual [ i ] !== expected [ i ] ) {
171+ consecutiveDiffs ++ ;
172+ currentBlock . push ( {
173+ line : i + 1 ,
174+ actual : actual [ i ] ,
175+ expected : expected [ i ]
176+ } ) ;
177+ } else {
178+ if ( consecutiveDiffs > 0 ) {
179+ const patterns = findRepeatedPattern ( currentBlock ) ;
180+ if ( patterns ) {
181+ differences . push ( {
182+ patterns,
183+ startLine : currentBlock [ 0 ] . line ,
184+ count : currentBlock . length
185+ } ) ;
186+ } else if ( currentBlock . length <= 3 ) {
187+ differences . push ( ...currentBlock ) ;
188+ } else {
189+ differences . push ( {
190+ truncated : true ,
191+ count : currentBlock . length ,
192+ startLine : currentBlock [ 0 ] . line ,
193+ sample : currentBlock [ 0 ]
194+ } ) ;
195+ }
196+ currentBlock = [ ] ;
197+ }
198+ consecutiveDiffs = 0 ;
199+ }
200+ }
201+
202+ // Handle any remaining differences
203+ if ( currentBlock . length > 0 ) {
204+ const patterns = findRepeatedPattern ( currentBlock ) ;
205+ if ( patterns ) {
206+ differences . push ( {
207+ patterns,
208+ startLine : currentBlock [ 0 ] . line ,
209+ count : currentBlock . length
210+ } ) ;
211+ } else if ( currentBlock . length <= 3 ) {
212+ differences . push ( ...currentBlock ) ;
213+ } else {
214+ differences . push ( {
215+ truncated : true ,
216+ count : currentBlock . length ,
217+ startLine : currentBlock [ 0 ] . line ,
218+ sample : currentBlock [ 0 ]
219+ } ) ;
220+ }
221+ }
222+
223+ return differences ;
224+ }
225+
226+ const DIFFERENCE_DESCRIPTIONS = {
227+ // PDF structure
228+ 'obj-header' : 'Object header' ,
229+ 'xref-entry' : 'Cross-reference entry' ,
230+ 'pdf-structure' : 'PDF structure element' ,
231+
232+ // Font-related
233+ 'font-def' : 'Font definition' ,
234+ 'font-ref' : 'Font reference' ,
235+ 'font-select' : 'Font selection' ,
236+
237+ // Text operations
238+ 'text-pos' : 'Text position' ,
239+ 'text-block' : 'Text block marker' ,
240+ 'text-leading' : 'Text leading' ,
241+ 'text-show' : 'Text content' ,
242+
243+ // Graphics operations
244+ 'color' : 'Color setting' ,
245+ 'graphics-state' : 'Graphics state' ,
246+ 'line-width' : 'Line width' ,
247+ 'path' : 'Path command' ,
248+ 'path-op' : 'Path operation' ,
249+ 'style' : 'Style setting' ,
250+
251+ // Length and metadata
252+ 'length' : 'Content length' ,
253+ 'metadata' : 'Document metadata' ,
254+
255+ 'unknown' : 'Uncategorized'
256+ } ;
257+
258+ function formatDifferences ( differences ) {
259+ const MAX_DIFFERENCES_TO_SHOW = 10 ;
260+
261+ let message = '' ;
262+ let totalDiffs = 0 ;
263+ let shownDiffs = 0 ;
264+
265+ // First, count total differences
266+ for ( const diff of differences ) {
267+ if ( diff . patterns ) {
268+ totalDiffs += diff . count ;
269+ } else if ( diff . truncated ) {
270+ totalDiffs += diff . count ;
271+ } else {
272+ totalDiffs ++ ;
273+ }
274+ }
275+
276+ message += `\nTotal differences: ${ totalDiffs } \n` ;
277+
278+ // Group similar patterns together
279+ const patternGroups = new Map ( ) ;
280+
281+ for ( const diff of differences ) {
282+ if ( diff . patterns ) {
283+ for ( const pattern of diff . patterns ) {
284+ if ( pattern . consistent ) {
285+ const key = `${ pattern . type } :${ pattern . sample . expected } :${ pattern . sample . actual } ` ;
286+ if ( ! patternGroups . has ( key ) ) {
287+ patternGroups . set ( key , {
288+ type : pattern . type ,
289+ sample : pattern . sample ,
290+ count : 0 ,
291+ locations : [ ]
292+ } ) ;
293+ }
294+ const group = patternGroups . get ( key ) ;
295+ group . count += pattern . count ;
296+ group . locations . push ( `${ pattern . startLine } -${ pattern . startLine + pattern . count - 1 } ` ) ;
297+ }
298+ }
299+ }
300+ }
301+
302+ // Output grouped patterns first
303+ if ( patternGroups . size > 0 ) {
304+ message += '\nConsistent patterns:\n' ;
305+ for ( const group of patternGroups . values ( ) ) {
306+ if ( shownDiffs >= MAX_DIFFERENCES_TO_SHOW ) {
307+ message += `\n... and ${ patternGroups . size - shownDiffs } more consistent patterns\n` ;
308+ break ;
309+ }
310+ message += ` - ${ group . count } ${ DIFFERENCE_DESCRIPTIONS [ group . type ] } differences:\n` ;
311+ const actualStr = group . sample . actual === undefined ? '<missing>' : group . sample . actual ;
312+ const expectedStr = group . sample . expected === undefined ? '<missing>' : group . sample . expected ;
313+ message += ` Expected: "${ expectedStr } "\n Actual: "${ actualStr } "\n` ;
314+ message += ` At lines: ${ group . locations . join ( ', ' ) } \n` ;
315+ shownDiffs ++ ;
316+ }
317+ }
318+
319+ // Then output other differences
320+ let remainingDiffsToShow = MAX_DIFFERENCES_TO_SHOW - shownDiffs ;
321+ let skippedDiffs = 0 ;
322+
323+ for ( const diff of differences ) {
324+ if ( remainingDiffsToShow <= 0 ) {
325+ skippedDiffs ++ ;
326+ continue ;
327+ }
328+
329+ if ( diff . patterns ) {
330+ const inconsistentPatterns = diff . patterns . filter ( p => ! p . consistent ) ;
331+ if ( inconsistentPatterns . length > 0 ) {
332+ message += `\nFound ${ diff . count } differences at line ${ diff . startLine } :\n` ;
333+ for ( const pattern of inconsistentPatterns ) {
334+ if ( remainingDiffsToShow <= 0 ) {
335+ skippedDiffs ++ ;
336+ continue ;
337+ }
338+ message += ` - ${ pattern . count } ${ DIFFERENCE_DESCRIPTIONS [ pattern . type ] } differences, e.g.:\n` ;
339+ const actualStr = pattern . sample . actual === undefined ? '<missing>' : pattern . sample . actual ;
340+ const expectedStr = pattern . sample . expected === undefined ? '<missing>' : pattern . sample . expected ;
341+ message += ` Expected: "${ expectedStr } "\n Actual: "${ actualStr } "\n` ;
342+ remainingDiffsToShow -- ;
343+ }
344+ }
345+ } else if ( diff . truncated ) {
346+ message += `\n${ diff . count } differences at line ${ diff . startLine } (showing first):\n` ;
347+ const actualStr = diff . sample . actual === undefined ? '<missing>' : diff . sample . actual ;
348+ const expectedStr = diff . sample . expected === undefined ? '<missing>' : diff . sample . expected ;
349+ const type = getPdfLineType ( expectedStr || actualStr ) ;
350+ message += ` ${ DIFFERENCE_DESCRIPTIONS [ type ] } :\n` ;
351+ message += ` Expected: "${ expectedStr } "\n Actual: "${ actualStr } "\n ... and ${ diff . count - 1 } more differences\n` ;
352+ remainingDiffsToShow -- ;
353+ } else {
354+ const actualStr = diff . actual === undefined ? '<missing>' : diff . actual ;
355+ const expectedStr = diff . expected === undefined ? '<missing>' : diff . expected ;
356+ const type = getPdfLineType ( expectedStr || actualStr ) ;
357+
358+ if ( actualStr === '<missing>' ) {
359+ message += `\nLine ${ diff . line } : Extra ${ DIFFERENCE_DESCRIPTIONS [ type ] } : "${ expectedStr } "` ;
360+ } else if ( expectedStr === '<missing>' ) {
361+ message += `\nLine ${ diff . line } : Extra ${ DIFFERENCE_DESCRIPTIONS [ type ] } : "${ actualStr } "` ;
362+ } else {
363+ message += `\nLine ${ diff . line } (${ DIFFERENCE_DESCRIPTIONS [ type ] } ):\n Expected: "${ expectedStr } "\n Actual: "${ actualStr } "` ;
364+ }
365+ remainingDiffsToShow -- ;
366+ }
367+ }
368+
369+ if ( skippedDiffs > 0 ) {
370+ message += `\n\n... and ${ skippedDiffs } more differences not shown` ;
371+ }
372+
373+ return message ;
374+ }
375+
85376/**
86377 * Find a better way to set this
87378 * @type {Boolean }
@@ -104,7 +395,13 @@ globalVar.comparePdf = function(actual, expectedFile, suite) {
104395 var expected = resetFile ( pdf . replace ( / ^ \s + | \s + $ / g, "" ) ) ;
105396 actual = resetFile ( actual . replace ( / ^ \s + | \s + $ / g, "" ) ) ;
106397
107- expect ( actual . replace ( / [ \r ] / g, "" ) . split ( "\n" ) ) . toEqual (
108- expected . replace ( / [ \r ] / g, "" ) . split ( "\n" )
109- ) ;
398+ const actualLines = actual . replace ( / [ \r ] / g, "" ) . split ( "\n" ) ;
399+ const expectedLines = expected . replace ( / [ \r ] / g, "" ) . split ( "\n" ) ;
400+
401+ const differences = compareArrays ( actualLines , expectedLines ) ;
402+
403+ if ( differences . length > 0 ) {
404+ const message = formatDifferences ( differences ) ;
405+ fail ( `PDF comparison failed:${ message } ` ) ;
406+ }
110407} ;
0 commit comments