@@ -55,6 +55,37 @@ import { MetadataParser } from "./metadata_parser.js";
5555import { stringToPDFString } from "./string_utils.js" ;
5656import { StructTreeRoot } from "./struct_tree.js" ;
5757
58+ /**
59+ * @import {XRef} from "./xref.js";
60+ */
61+
62+ /**
63+ * @callback GetAttachmentContent
64+ * Callback used to lazily fetch attachment content.
65+ * @param {string } id
66+ * Unique attachment identifier.
67+ * @returns {CatalogAttachmentContent }
68+ * Result.
69+ */
70+
71+ /**
72+ * @typedef {Uint8Array | null } CatalogAttachmentContent
73+ * Attachment value.
74+ */
75+
76+ /**
77+ * @typedef CatalogAttachment
78+ * Attachment metadata.
79+ * @property {CatalogAttachmentContent | undefined } [content]
80+ * Value, when already available.
81+ * @property {string } description
82+ * Description.
83+ * @property {string } filename
84+ * Filename (just the basename) for display.
85+ * @property {string } rawFilename
86+ * File path.
87+ */
88+
5889const isRef = v => v instanceof Ref ;
5990
6091const isValidExplicitDest = _isValidExplicitDest . bind (
@@ -88,8 +119,18 @@ function fetchRemoteDest(action) {
88119class Catalog {
89120 #actualNumPages = null ;
90121
122+ /** @type {RefSetCache | null } */
123+ #attachmentIdByRef = null ;
124+
91125 #catDict = null ;
92126
127+ /**
128+ * Attachment dictionaries keyed by attachment id.
129+ *
130+ * @type {Map<string, Dict> }
131+ */
132+ attachmentDictById = new Map ( ) ;
133+
93134 builtInCMapCache = new Map ( ) ;
94135
95136 fontCache = new RefSetCache ( ) ;
@@ -123,6 +164,29 @@ class Catalog {
123164 this . toplevelPagesDict ; // eslint-disable-line no-unused-expressions
124165 }
125166
167+ /**
168+ * Attachment ids keyed by embedded-file reference.
169+ *
170+ * @type {RefSetCache }
171+ */
172+ get attachmentIdByRef ( ) {
173+ if ( this . #attachmentIdByRef) {
174+ return this . #attachmentIdByRef;
175+ }
176+
177+ const attachmentIdByRef = new RefSetCache ( ) ;
178+ for ( const [ name , ref ] of this . rawEmbeddedFiles || [ ] ) {
179+ if ( ! ( ref instanceof Ref ) ) {
180+ continue ;
181+ }
182+ attachmentIdByRef . put (
183+ ref ,
184+ stringToPDFString ( name , /* keepEscapeSequence = */ true )
185+ ) ;
186+ }
187+ return ( this . #attachmentIdByRef = attachmentIdByRef ) ;
188+ }
189+
126190 cloneDict ( ) {
127191 return this . #catDict. clone ( ) ;
128192 }
@@ -369,6 +433,7 @@ class Catalog {
369433
370434 const outlineItem = {
371435 action : data . action ,
436+ attachmentId : data . attachmentId ,
372437 attachment : data . attachment ,
373438 dest : data . dest ,
374439 url : data . url ,
@@ -1068,8 +1133,15 @@ class Catalog {
10681133 ) ;
10691134 }
10701135
1136+ /**
1137+ * Get attachments.
1138+ *
1139+ * @returns {Record<string, CatalogAttachment> | null }
1140+ * Attachments.
1141+ */
10711142 get attachments ( ) {
10721143 const obj = this . #catDict. get ( "Names" ) ;
1144+ /** @type {Record<string, CatalogAttachment> | null } */
10731145 let attachments = null ;
10741146
10751147 if ( obj instanceof Dict && obj . has ( "EmbeddedFiles" ) ) {
@@ -1084,6 +1156,32 @@ class Catalog {
10841156 return shadow ( this , "attachments" , attachments ) ;
10851157 }
10861158
1159+ /**
1160+ * Get content for an attachment.
1161+ *
1162+ * @param {string } id
1163+ * Unique attachment identifier (required).
1164+ * @returns {CatalogAttachmentContent }
1165+ * Content.
1166+ */
1167+ attachmentContent ( id ) {
1168+ const dict = this . attachmentDictById . get ( id ) ;
1169+ if ( dict ) {
1170+ return FileSpec . readContent ( dict ) ;
1171+ }
1172+
1173+ const obj = this . #catDict. get ( "Names" ) ;
1174+ if ( obj instanceof Dict && obj . has ( "EmbeddedFiles" ) ) {
1175+ const nameTree = new NameTree ( obj . getRaw ( "EmbeddedFiles" ) , this . xref ) ;
1176+ for ( const [ key , value ] of nameTree . getAll ( ) ) {
1177+ if ( stringToPDFString ( key , /* keepEscapeSequence = */ true ) === id ) {
1178+ return FileSpec . readContent ( value ) ;
1179+ }
1180+ }
1181+ }
1182+ return null ;
1183+ }
1184+
10871185 get rawEmbeddedFiles ( ) {
10881186 const obj = this . #catDict. get ( "Names" ) ;
10891187 if ( ! ( obj instanceof Dict ) || ! obj . has ( "EmbeddedFiles" ) ) {
@@ -1182,6 +1280,9 @@ class Catalog {
11821280
11831281 async cleanup ( manuallyTriggered = false ) {
11841282 clearGlobalCaches ( ) ;
1283+ this . #attachmentIdByRef?. clear ( ) ;
1284+ this . #attachmentIdByRef = null ;
1285+ this . attachmentDictById . clear ( ) ;
11851286 this . globalColorSpaceCache . clear ( ) ;
11861287 this . globalImageCache . clear ( /* onlyData = */ manuallyTriggered ) ;
11871288 this . pageKidsCountCache . clear ( ) ;
@@ -1556,8 +1657,8 @@ class Catalog {
15561657 * properties will be placed.
15571658 * @property {string } [docBaseUrl] - The document base URL that is used when
15581659 * attempting to recover valid absolute URLs from relative ones.
1559- * @property {Object } [docAttachments] - The document attachments (may not
1560- * exist in most PDF documents).
1660+ * @property {Record<string, CatalogAttachment> | null } [docAttachments] - The
1661+ * document attachments (may not exist in most PDF documents).
15611662 */
15621663
15631664 /**
@@ -1740,8 +1841,7 @@ class Catalog {
17401841 case "GoToR" :
17411842 const urlDict = action . get ( "F" ) ;
17421843 if ( urlDict instanceof Dict ) {
1743- const fs = new FileSpec ( urlDict , /* skipContent = */ true ) ;
1744- ( { rawFilename : url } = fs . serializable ) ;
1844+ url = new FileSpec ( urlDict ) . filename ;
17451845 } else if ( typeof urlDict === "string" ) {
17461846 url = urlDict ;
17471847 } else {
@@ -1766,22 +1866,21 @@ class Catalog {
17661866
17671867 case "GoToE" :
17681868 const target = action . get ( "T" ) ;
1769- let attachment ;
1869+ /** @type {string | null } */
1870+ let id = null ;
17701871
1771- if ( docAttachments && target instanceof Dict ) {
1872+ if ( target instanceof Dict ) {
17721873 const relationship = target . get ( "R" ) ;
17731874 const name = target . get ( "N" ) ;
17741875
17751876 if ( isName ( relationship , "C" ) && typeof name === "string" ) {
1776- attachment =
1777- docAttachments [
1778- stringToPDFString ( name , /* keepEscapeSequence = */ true )
1779- ] ;
1877+ id = stringToPDFString ( name , /* keepEscapeSequence = */ true ) ;
17801878 }
17811879 }
17821880
1783- if ( attachment ) {
1784- resultObj . attachment = attachment ;
1881+ if ( docAttachments && id ) {
1882+ resultObj . attachmentId = id ;
1883+ resultObj . attachment = docAttachments [ id ] ;
17851884
17861885 // NOTE: the destination is relative to the *attachment*.
17871886 const attachmentDest = fetchRemoteDest ( action ) ;
0 commit comments