@@ -25,6 +25,7 @@ import { StringStream } from "../stream.js";
2525import { stringToAsciiOrUTF16BE } from "../core_utils.js" ;
2626
2727const MAX_LEAVES_PER_PAGES_NODE = 16 ;
28+ const MAX_IN_NAME_TREE_NODE = 64 ;
2829
2930class PageData {
3031 constructor ( page , documentData ) {
@@ -39,6 +40,7 @@ class PageData {
3940class DocumentData {
4041 constructor ( document ) {
4142 this . document = document ;
43+ this . pageLabels = null ;
4244 this . pagesMap = new RefSetCache ( ) ;
4345 this . oldRefMapping = new RefSetCache ( ) ;
4446 }
@@ -61,6 +63,7 @@ class PDFEditor {
6163 this . version = "1.7" ;
6264 this . title = title ;
6365 this . author = author ;
66+ this . pageLabels = null ;
6467 }
6568
6669 /**
@@ -253,6 +256,8 @@ class PDFEditor {
253256 await Promise . all ( promises ) ;
254257 promises . length = 0 ;
255258
259+ this . #collectPageLabels( ) ;
260+
256261 for ( const page of this . oldPages ) {
257262 promises . push ( this . #postCollectPageData( page ) ) ;
258263 }
@@ -270,7 +275,12 @@ class PDFEditor {
270275 * @param {DocumentData } documentData
271276 * @return {Promise<void> }
272277 */
273- async #collectDocumentData( documentData ) { }
278+ async #collectDocumentData( documentData ) {
279+ const { document } = documentData ;
280+ await document . pdfManager
281+ . ensureCatalog ( "rawPageLabels" )
282+ . then ( pageLabels => ( documentData . pageLabels = pageLabels ) ) ;
283+ }
274284
275285 /**
276286 * Post process the collected page data.
@@ -306,6 +316,56 @@ class PDFEditor {
306316 pageData . annotations = newAnnotations . length > 0 ? newAnnotations : null ;
307317 }
308318
319+ async #collectPageLabels( ) {
320+ // We can only preserve page labels when editing a single PDF file.
321+ // This is consistent with behavior in Adobe Acrobat.
322+ if ( ! this . hasSingleFile ) {
323+ return ;
324+ }
325+ const {
326+ documentData : { document, pageLabels } ,
327+ } = this . oldPages [ 0 ] ;
328+ if ( ! pageLabels ) {
329+ return ;
330+ }
331+ const numPages = document . numPages ;
332+ const oldPageLabels = [ ] ;
333+ const oldPageIndices = new Set (
334+ this . oldPages . map ( ( { page : { pageIndex } } ) => pageIndex )
335+ ) ;
336+ let currentLabel = null ;
337+ let stFirstIndex = - 1 ;
338+ for ( let i = 0 ; i < numPages ; i ++ ) {
339+ const newLabel = pageLabels . get ( i ) ;
340+ if ( newLabel ) {
341+ currentLabel = newLabel ;
342+ stFirstIndex = currentLabel . has ( "St" ) ? i : - 1 ;
343+ }
344+ if ( ! oldPageIndices . has ( i ) ) {
345+ continue ;
346+ }
347+ if ( stFirstIndex !== - 1 ) {
348+ const st = currentLabel . get ( "St" ) ;
349+ currentLabel = currentLabel . clone ( ) ;
350+ currentLabel . set ( "St" , st + ( i - stFirstIndex ) ) ;
351+ stFirstIndex = - 1 ;
352+ }
353+ oldPageLabels . push ( currentLabel ) ;
354+ }
355+ currentLabel = oldPageLabels [ 0 ] ;
356+ let currentIndex = 0 ;
357+ const newPageLabels = ( this . pageLabels = [ [ 0 , currentLabel ] ] ) ;
358+ for ( let i = 0 , ii = oldPageLabels . length ; i < ii ; i ++ ) {
359+ const label = oldPageLabels [ i ] ;
360+ if ( label === currentLabel ) {
361+ continue ;
362+ }
363+ currentIndex = i ;
364+ currentLabel = label ;
365+ newPageLabels . push ( [ currentIndex , currentLabel ] ) ;
366+ }
367+ }
368+
309369 /**
310370 * Create a copy of a page.
311371 * @param {number } pageIndex
@@ -423,6 +483,63 @@ class PDFEditor {
423483 }
424484 }
425485
486+ /**
487+ * Create a name or number tree from the given map.
488+ * @param {Array<[string, any]> } map
489+ * @returns {Ref }
490+ */
491+ #makeNameNumTree( map , areNames ) {
492+ const allEntries = map . sort (
493+ areNames
494+ ? ( [ keyA ] , [ keyB ] ) => keyA . localeCompare ( keyB )
495+ : ( [ keyA ] , [ keyB ] ) => keyA - keyB
496+ ) ;
497+ const maxLeaves =
498+ MAX_IN_NAME_TREE_NODE <= 1 ? allEntries . length : MAX_IN_NAME_TREE_NODE ;
499+ const [ treeRef , treeDict ] = this . newDict ;
500+ const stack = [ { dict : treeDict , entries : allEntries } ] ;
501+ const valueType = areNames ? "Names" : "Nums" ;
502+
503+ while ( stack . length > 0 ) {
504+ const { dict, entries } = stack . pop ( ) ;
505+ if ( entries . length <= maxLeaves ) {
506+ dict . set ( "Limits" , [ entries [ 0 ] [ 0 ] , entries . at ( - 1 ) [ 0 ] ] ) ;
507+ dict . set ( valueType , entries . flat ( ) ) ;
508+ continue ;
509+ }
510+ const entriesChunks = [ ] ;
511+ const chunkSize = Math . ceil ( entries . length / maxLeaves ) ;
512+ for ( let i = 0 ; i < entries . length ; i += chunkSize ) {
513+ entriesChunks . push ( entries . slice ( i , i + chunkSize ) ) ;
514+ }
515+ const entriesRefs = [ ] ;
516+ dict . set ( "Kids" , entriesRefs ) ;
517+ for ( const chunk of entriesChunks ) {
518+ const [ entriesRef , entriesDict ] = this . newDict ;
519+ entriesRefs . push ( entriesRef ) ;
520+ entriesDict . set ( "Limits" , [ chunk [ 0 ] [ 0 ] , chunk . at ( - 1 ) [ 0 ] ] ) ;
521+ stack . push ( { dict : entriesDict , entries : chunk } ) ;
522+ }
523+ }
524+ return treeRef ;
525+ }
526+
527+ /**
528+ * Create the page labels tree if it exists.
529+ */
530+ #makePageLabelsTree( ) {
531+ const { pageLabels } = this ;
532+ if ( ! pageLabels || pageLabels . length === 0 ) {
533+ return ;
534+ }
535+ const { rootDict } = this ;
536+ const pageLabelsRef = this . #makeNameNumTree(
537+ this . pageLabels ,
538+ /* areNames = */ false
539+ ) ;
540+ rootDict . set ( "PageLabels" , pageLabelsRef ) ;
541+ }
542+
426543 /**
427544 * Create the root dictionary.
428545 * @returns {Promise<void> }
@@ -432,6 +549,7 @@ class PDFEditor {
432549 rootDict . setIfName ( "Type" , "Catalog" ) ;
433550 rootDict . set ( "Version" , this . version ) ;
434551 this . #makePageTree( ) ;
552+ this . #makePageLabelsTree( ) ;
435553 }
436554
437555 /**
0 commit comments