From cd10b3ccbffb2be27431a51ebf922173d17c311e Mon Sep 17 00:00:00 2001 From: akowalczewski Date: Thu, 4 Dec 2025 15:59:16 +0100 Subject: [PATCH 1/5] Accessibility: Fixed Link annotation is not nested inside a Link structure element --- .gitignore | 1 + examples/accessible-links.pdf | Bin 0 -> 15245 bytes examples/kitchen-sink-accessible-tiny.js | 67 +++++++++++++++++++++++ examples/kitchen-sink-accessible.js | 4 +- examples/kitchen-sink-accessible.pdf | Bin 686795 -> 687286 bytes lib/mixins/annotations.js | 15 +++++ lib/mixins/text.js | 6 +- lib/structure_annotation.js | 7 +++ lib/structure_element.js | 36 ++++++++++++ tests/unit/annotations.spec.js | 37 +++++++++++++ tests/unit/markings.spec.js | 17 ++++++ tests/unit/structure_annotation.spec.js | 66 ++++++++++++++++++++++ tests/unit/text.spec.js | 43 +++++++++++++++ 13 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 examples/accessible-links.pdf create mode 100644 examples/kitchen-sink-accessible-tiny.js create mode 100644 lib/structure_annotation.js create mode 100644 tests/unit/structure_annotation.spec.js diff --git a/.gitignore b/.gitignore index 247e36b4..416d4bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ playground/ build/ js/ .vscode +.idea coverage package-lock.json /examples/browserify/bundle.js diff --git a/examples/accessible-links.pdf b/examples/accessible-links.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4a41fdd1fd77ed0be589c8296546834d151c2ae1 GIT binary patch literal 15245 zcmcJ0by$>L*RM*aN;e}R4HI-J-JOCoLpMVsf`oL3(g;X*3rI=}DvC&_ph&k0e)nLZ z&-0%5JJ-3+{DHIA-h1uUdym&|F{(;Rb3izGuo=GrA8ZH|1O_>p+F%O{V{>VMxKy#Z zR6!63Kvc)(QUK|L4FG~A2m(H%h>Bvv9n6u-Ap(Ey(^8ki<^ukJn5_^9CqXVQOGigb zJ2AYK6|AhEcki#gl{kRCV; zsev?z8_*8n;%0_WHE{trgP`YRg7BZ0>6bKi2oSR@+yxG@b_JP$>;Mt3fdFv;^Y8)d zf0ze^wA*(HRm9~1qKc^v=uAoAJJN8xXKO`8f7tQo@RuEtynfjO#H9h~_m>OK#8Wpo zFYtLIem`8*#1and`>xy%_bI{6txd!oJ-^$57jQM-nRR|y7pXTOraIi!(apsS?g~OW z6(F3qlq(1fC}?8p3dnX&g!ArKb#XM)fCFyf0%8lqr3v>$fVkxB0g1$a{44R}Upb^N z0N2fPTDkVE^{1^eu`JnK;dAUBx$;Zn@ZA; zFDCJtd$8^zF6K>qqMNAlD+69`LfHr3Ha0FQ4hMS(XBhe2Foe}OD$QoPrH-@mDKU}9 z-!>=nIlizR4y8o8{fB%%6A2j+KLg>f`?_wqd21qIjN7W&*vsRyNWo}tz0{M* zBgU!GHx46mt1EezeF#q#85mLo9et@`+W4GlA{I zC4gV|sdKEv*LpK0c?KMH2X#(4Z}o`d(^So8-4A7>GVzYWjx5%!F!VR?iCkcH7VP}KMMpDnIcHz{9^*?@Usa?K`0o?1A&6MA>7;$Ft^_C&BzcqTcZhg zMSvt7&D`vP>i2v8XYesg!rdJmKr)W*aKyQ3WFh(K7$6GF-OPY|I};VC5)wd~Asm76 z_ay#4wsv%o1hNqX{Sl6u)(AUb^dl6-+z?jC9e-Cr==m!6BLW0qV4!IKw;X@&?PaW? zb?{B#(Ei=-tLsWn7`W^w5ORD^@)aiX*bG+1t*!L*tOKewm^m;nh0|q&E=f(1lIf^k zLpQ&-LyZ%dyVwX_jT{CKTgg!8<>sndzUG~s^Ec?PS_MW;{2Z|-QUS9@Q$<@9oAAEygWeKEAjZ96>2ZNA!!^1=~Gxn8Yl@mGus z6PV2SXxTo?&l^^340KeP67*h`Jeqql^ZI1|&GQngViddLT>e<`IYbs+Felv4FK~&RuHcnzF|>s7&Y6%* z;leAa#=54rK)%72?RcU`Sf%&g&Zlf~D872~ViQDQ3c`Bdp{I)>G)LnJ*(03mzz$Qr zSO>=O-NHqM5JXgB>aK?5S6<@nl6wL{TqI)-p?H8XIEN`Lb4Q{eL==OA%ox+bf8FV_ zF#1Ym_>uTzHd?BL(FGLV!eDQ!dlps!h7T}sOd#xmNtHCxbHqniPJ=vRMT_rKWJjxV zLF96ZUx@Kv{O|(Z)tHn0-kJ&F)FqjDgGN-N+=~%?4^?$mIgLT&)$MpRh4;x{;NqBY zO`%;5pf&U#-D2}36!nh*1>yEATn|_EP`#|yW+@tQS)wT?D*%I}bKi;nlYdtGHGWi? zIWK-3+V;))OTM90PpU|=uI-yc-YK>bu;SNULSyd(p+mFDShLY>@`&BqsmT1@oEr9) z7q=8VaZXWF=Ge$&Znj$~KC^xh$Ty!xMrZL#e1<-y5NZ>uCo`=$Eq_OG6hVB4a=m~H zCTj8Em?N!S@Om2#HRc@_cMJ~9SZp`}hGi%A>V2{C3w#~xVB9bBxGlJC_ZsF6JZVKk zM&hK=y&}716}xeVI;?er@>e@7(t~R%37ee-X2K5_T>0-+%�VR>k4&ppxe#tIHJa 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 literal 0 HcmV?d00001 diff --git a/examples/kitchen-sink-accessible-tiny.js b/examples/kitchen-sink-accessible-tiny.js new file mode 100644 index 00000000..23b13f46 --- /dev/null +++ b/examples/kitchen-sink-accessible-tiny.js @@ -0,0 +1,67 @@ +var PDFDocument = require('../'); +var fs = require('fs'); + +// Create a new PDFDocument +var doc = new PDFDocument({ + autoFirstPage: true, + bufferPages: true, + pdfVersion: '1.5', + // @ts-ignore PDF/UA needs to be enforced for PAC accessibility checker + subset: 'PDF/UA', + tagged: true, + displayTitle: true, + lang: 'en-US', + fontSize: 12, +}); + +doc.pipe(fs.createWriteStream('accessible-links.pdf')); + +// Set some meta data +doc.info['Title'] = 'Test Document'; +doc.info['Author'] = 'Devon Govett'; + +// Initialise document logical structure +var struct = doc.struct('Document'); +doc.addStructure(struct); + +// Register a font name for use later +doc.registerFont('Palatino', 'fonts/PalatinoBold.ttf'); + +// Set the font and draw some text +struct.add( + doc.struct('P', () => { + doc + .font('Palatino') + .fontSize(25) + .text('Some text with an embedded font! ', 100, 100); + }) +); + + +// Add another page +doc.addPage(); + +// Add some text with annotations +var linkSection = doc.struct('Sect'); +struct.add(linkSection); + +linkSection.add( + doc.struct( + 'Link', + { + alt: 'Here is a link! ' + }, + () => { + doc.font('Palatino').fillColor('blue').text('Here is a link!', 100, 100, { + link: 'http://google.com/', + underline: true + }); + } + ) +); + +linkSection.end(); + + +// End and flush the document +doc.end(); diff --git a/examples/kitchen-sink-accessible.js b/examples/kitchen-sink-accessible.js index bf0a824e..06b25d8e 100644 --- a/examples/kitchen-sink-accessible.js +++ b/examples/kitchen-sink-accessible.js @@ -7,7 +7,9 @@ var doc = new PDFDocument({ pdfVersion: '1.5', lang: 'en-US', tagged: true, - displayTitle: true + displayTitle: true, + // @ts-ignore PDF/UA needs to be enforced for PAC accessibility checker + subset: 'PDF/UA', }); doc.pipe(fs.createWriteStream('kitchen-sink-accessible.pdf')); diff --git a/examples/kitchen-sink-accessible.pdf b/examples/kitchen-sink-accessible.pdf index 002f9b523da6bf86d78fab985120d2802599a0af..91a2f54f00dedbe6ba1602ff9552e9b4e413ea26 100644 GIT binary patch delta 1583 zcmah}U1%It6!z@K+T2vBq|sIdZ`TH!LOS=}xp!vnvRTAbu|JyFR^JLvI2 zx@o{{p-|t%7r6=|zWAmOf_6;RfNyTvrckAig0zYVdGtYi&@)|21~?@3Bi?!JjKbll58@)}kgoJZAn_fma^2&P0vY`Dog2AZ*4vaD0rGO2j&Q zaWSOU)Qcxh6KiVb0+os~iucpARPx?!xoi$zg8lJ(9k{$}NO8d=my&Z8KXV0^ZS&zZ z*fV%4tj$Jql-p)y9STGL{EI99&|~qy_wnUbAn>#`dHmR!FrtpLm6U|FpgwbfIIfvq z0~)VhgOQxMy9UGYuiwEhFzO~7&tZr4H)_@M^&q5{7=j=P(KpIrgR)Fbj# zM=-8Y_GHl-CCL@dBwU|J^rI(T#F(u8HOJsP&%hqxn z^Ekc=Ga_Rxo++t4CzArXW+Xx_v-`9(uU{m?9WTv}F~L&`a&8NxwSzHY_(RHI(#`lt zGCS=-E|q(~rOpErau-Xd6FNF$F7661r3H?}5gE)=Ds5miX7Wqxk1lGM;xgHbpUcT* zLf_)t_Z^=HGb;3$TTwO>DsaL;YUyhh$TBVc@0N37NA`K delta 1300 zcmai!Pe@cj9LM?Xn&s3X(ybPP-XS3a+4;@Pdov>%gCmi$~>YN)Sxq-3sr7QWdg70`TL}qjm4hN6lL8d9?>F{R8oLYeWMlQk1 zJ{6{r?b?#oYn$$6xD%!?EyD{l{{fnVdn@o1T4W)~1;ig6udS(Hj7 z3o0qh(sk0CMu8?fPFhpUHOI$EUz$l8%#M=`q^K~@tK?BXy0QCIYswQ$DV1cXVgpgF zkb36+1kp9cMRZ#GQA)e;e^pt=y(D8I=17=leT>{il%B#Uvw1GBJ2$j#yM+Q{s|cBl zS!|iaWMfw+g?R4VP0~_Jbk{PzOKYdXgmt!iqLJ;M7*cDDtELpi-+^k|4~tXbMy){P zobm)yUTkM*%hA{{h@MQoD@idbIA?+6ZWLN(A9GS6L6V1+zo5{shC>HKd>{qJzCGF+}y%YKn~%0*X^*4C3}ddMHca6#Ds diff --git a/lib/mixins/annotations.js b/lib/mixins/annotations.js index 51a72c57..ad9b7047 100644 --- a/lib/mixins/annotations.js +++ b/lib/mixins/annotations.js @@ -1,3 +1,5 @@ +import PDFAnnotationReference from '../structure_annotation'; + export default { annotate(x, y, w, h, options) { options.Type = 'Annot'; @@ -19,6 +21,9 @@ export default { options.Dest = new String(options.Dest); } + const structParent = options.structParent; + delete options.structParent; + // Capitalize keys for (let key in options) { const val = options[key]; @@ -27,6 +32,12 @@ export default { const ref = this.ref(options); this.page.annotations.push(ref); + + if (structParent && typeof structParent.add === 'function') { + const annotRef = new PDFAnnotationReference(ref); + structParent.add(annotRef); + } + ref.end(); return this; }, @@ -77,6 +88,10 @@ export default { options.A.end(); } + if (options.structParent && !options.Contents) { + options.Contents = new String(''); + } + return this.annotate(x, y, w, h, options); }, diff --git a/lib/mixins/text.js b/lib/mixins/text.js index be34348b..12ff368a 100644 --- a/lib/mixins/text.js +++ b/lib/mixins/text.js @@ -531,7 +531,11 @@ export default { // create link annotations if the link option is given if (options.link != null) { - this.link(x, y, renderedWidth, this.currentLineHeight(), options.link); + const linkOptions = {}; + if (this._currentStructureElement && this._currentStructureElement.dictionary.data.S === 'Link') { + linkOptions.structParent = this._currentStructureElement; + } + this.link(x, y, renderedWidth, this.currentLineHeight(), options.link, linkOptions); } if (options.goTo != null) { this.goTo(x, y, renderedWidth, this.currentLineHeight(), options.goTo); diff --git a/lib/structure_annotation.js b/lib/structure_annotation.js new file mode 100644 index 00000000..fe5ddbfd --- /dev/null +++ b/lib/structure_annotation.js @@ -0,0 +1,7 @@ +class PDFAnnotationReference { + constructor(annotationRef) { + this.annotationRef = annotationRef; + } +} + +export default PDFAnnotationReference; diff --git a/lib/structure_element.js b/lib/structure_element.js index 4f62d7ae..9f9cf19e 100644 --- a/lib/structure_element.js +++ b/lib/structure_element.js @@ -4,6 +4,7 @@ By Ben Schmidt */ import PDFStructureContent from './structure_content'; +import PDFAnnotationReference from './structure_annotation'; class PDFStructureElement { constructor(document, type, options = {}, children = null) { @@ -71,6 +72,10 @@ class PDFStructureElement { this._addContentToParentTree(child); } + if (child instanceof PDFAnnotationReference) { + this._addAnnotationToParentTree(child.annotationRef); + } + if (typeof child === 'function' && this._attached) { // _contentForClosure() adds the content to the parent tree child = this._contentForClosure(child); @@ -90,6 +95,15 @@ class PDFStructureElement { }); } + _addAnnotationToParentTree(annotRef) { + const parentTreeKey = this.document.createStructParentTreeNextKey(); + + annotRef.data.StructParent = parentTreeKey; + + const parentTree = this.document.getStructParentTree(); + parentTree.add(parentTreeKey, this.dictionary); + } + setParent(parentRef) { if (this.dictionary.data.P) { throw new Error(`Structure element added to more than one parent`); @@ -137,13 +151,25 @@ class PDFStructureElement { return ( child instanceof PDFStructureElement || child instanceof PDFStructureContent || + child instanceof PDFAnnotationReference || typeof child === 'function' ); } _contentForClosure(closure) { const content = this.document.markStructureContent(this.dictionary.data.S); + + const prevStructElement = this.document._currentStructureElement; + this.document._currentStructureElement = this; + + const wasEnded = this._ended; + this._ended = false; + closure(); + + this._ended = wasEnded; + + this.document._currentStructureElement = prevStructElement; this.document.endMarkedContent(); this._addContentToParentTree(content); @@ -209,6 +235,16 @@ class PDFStructureElement { } }); } + + if (child instanceof PDFAnnotationReference) { + const pageRef = this.document.page.dictionary; + const objr = { + Type: 'OBJR', + Obj: child.annotationRef, + Pg: pageRef, + }; + this.dictionary.data.K.push(objr); + } } } diff --git a/tests/unit/annotations.spec.js b/tests/unit/annotations.spec.js index 2210f7ca..34f2a97a 100644 --- a/tests/unit/annotations.spec.js +++ b/tests/unit/annotations.spec.js @@ -178,4 +178,41 @@ describe('Annotations', () => { ]); }); }); + + describe('annotations with structure parent', () => { + test('should add structParent to link annotations', () => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false, + tagged: true, + }); + + const docData = logData(document); + + const linkElement = document.struct('Link'); + document.addStructure(linkElement); + + document.link(100, 100, 100, 20, 'http://example.com', { + structParent: linkElement, + }); + + linkElement.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/StructParent 0'); + expect(dataStr).toContain('/Contents ()'); + }); + + test('should work without structParent (backwards compatibility)', () => { + const docData = logData(document); + + document.link(100, 100, 100, 20, 'http://example.com'); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/Subtype /Link'); + expect(dataStr).not.toContain('/StructParent'); + }); + }); }); diff --git a/tests/unit/markings.spec.js b/tests/unit/markings.spec.js index 6486c17d..c07fedf0 100644 --- a/tests/unit/markings.spec.js +++ b/tests/unit/markings.spec.js @@ -525,6 +525,23 @@ EMC document.struct('Foo', [1]); }).toThrow(); }); + + test('_currentStructureElement tracking with closures', () => { + const section = document.struct('Sect'); + document.addStructure(section); + + let capturedStructElement = null; + + const paragraph = document.struct('P', () => { + capturedStructElement = document._currentStructureElement; + }); + + section.add(paragraph); + section.end(); + document.end(); + + expect(capturedStructElement).toBe(paragraph); + }); }); describe('accessible document', () => { diff --git a/tests/unit/structure_annotation.spec.js b/tests/unit/structure_annotation.spec.js new file mode 100644 index 00000000..28faa82a --- /dev/null +++ b/tests/unit/structure_annotation.spec.js @@ -0,0 +1,66 @@ +import PDFDocument from '../../lib/document'; +import PDFAnnotationReference from '../../lib/structure_annotation'; +import { logData } from './helpers'; + +describe('PDFAnnotationReference', () => { + let document; + + beforeEach(() => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false, + tagged: true, + }); + }); + + test('should add annotation reference to structure element with StructParent', () => { + const docData = logData(document); + + const linkElement = document.struct('Link'); + document.addStructure(linkElement); + + const annotRef = document.ref({ + Type: 'Annot', + Subtype: 'Link', + Rect: [100, 100, 200, 120], + }); + + linkElement.add(new PDFAnnotationReference(annotRef)); + linkElement.end(); + annotRef.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/Type /OBJR'); + expect(dataStr).toContain('/StructParent 0'); + }); + + test('should handle multiple annotations with different StructParent values', () => { + const docData = logData(document); + + const section = document.struct('Sect'); + document.addStructure(section); + + const link1 = document.struct('Link'); + const link2 = document.struct('Link'); + section.add(link1); + section.add(link2); + + const annotRef1 = document.ref({ Type: 'Annot', Subtype: 'Link' }); + const annotRef2 = document.ref({ Type: 'Annot', Subtype: 'Link' }); + + link1.add(new PDFAnnotationReference(annotRef1)); + link2.add(new PDFAnnotationReference(annotRef2)); + + link1.end(); + link2.end(); + section.end(); + annotRef1.end(); + annotRef2.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/StructParent 0'); + expect(dataStr).toContain('/StructParent 1'); + }); +}); diff --git a/tests/unit/text.spec.js b/tests/unit/text.spec.js index 6a1153cc..9427d7eb 100644 --- a/tests/unit/text.spec.js +++ b/tests/unit/text.spec.js @@ -193,4 +193,47 @@ Q expect(docData).toContainText({ text }); }); }); + + describe('text with structure parent links', () => { + beforeEach(() => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false, + tagged: true, + }); + }); + + test('should auto-link text inside Link structure element', () => { + const docData = logData(document); + + const linkElement = document.struct('Link', () => { + document.text('Click here', 100, 100, { + link: 'http://example.com', + }); + }); + + document.addStructure(linkElement); + linkElement.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/S /Link'); + expect(dataStr).toContain('/StructParent'); + }); + + test('should not add StructParent outside Link structure', () => { + const docData = logData(document); + + document.text('Click here', 100, 100, { + link: 'http://example.com', + }); + + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/Subtype /Link'); + expect(dataStr).not.toContain('/StructParent'); + }); + }); }); + From fd207a4841ddc99483f04600b640adb158e20c99 Mon Sep 17 00:00:00 2001 From: adyry Date: Thu, 4 Dec 2025 16:06:44 +0100 Subject: [PATCH 2/5] Accessibility: Fixed Link annotation is not nested inside a Link structure element --- .gitignore | 1 + examples/accessible-links.pdf | Bin 0 -> 15245 bytes examples/kitchen-sink-accessible-tiny.js | 67 +++++++++++++++++++++++ examples/kitchen-sink-accessible.js | 4 +- examples/kitchen-sink-accessible.pdf | Bin 686795 -> 687286 bytes lib/mixins/annotations.js | 15 +++++ lib/mixins/text.js | 6 +- lib/structure_annotation.js | 7 +++ lib/structure_element.js | 36 ++++++++++++ tests/unit/annotations.spec.js | 37 +++++++++++++ tests/unit/markings.spec.js | 17 ++++++ tests/unit/structure_annotation.spec.js | 66 ++++++++++++++++++++++ tests/unit/text.spec.js | 43 +++++++++++++++ 13 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 examples/accessible-links.pdf create mode 100644 examples/kitchen-sink-accessible-tiny.js create mode 100644 lib/structure_annotation.js create mode 100644 tests/unit/structure_annotation.spec.js diff --git a/.gitignore b/.gitignore index 247e36b4..416d4bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ playground/ build/ js/ .vscode +.idea coverage package-lock.json /examples/browserify/bundle.js diff --git a/examples/accessible-links.pdf b/examples/accessible-links.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4a41fdd1fd77ed0be589c8296546834d151c2ae1 GIT binary patch literal 15245 zcmcJ0by$>L*RM*aN;e}R4HI-J-JOCoLpMVsf`oL3(g;X*3rI=}DvC&_ph&k0e)nLZ z&-0%5JJ-3+{DHIA-h1uUdym&|F{(;Rb3izGuo=GrA8ZH|1O_>p+F%O{V{>VMxKy#Z zR6!63Kvc)(QUK|L4FG~A2m(H%h>Bvv9n6u-Ap(Ey(^8ki<^ukJn5_^9CqXVQOGigb zJ2AYK6|AhEcki#gl{kRCV; zsev?z8_*8n;%0_WHE{trgP`YRg7BZ0>6bKi2oSR@+yxG@b_JP$>;Mt3fdFv;^Y8)d zf0ze^wA*(HRm9~1qKc^v=uAoAJJN8xXKO`8f7tQo@RuEtynfjO#H9h~_m>OK#8Wpo zFYtLIem`8*#1and`>xy%_bI{6txd!oJ-^$57jQM-nRR|y7pXTOraIi!(apsS?g~OW z6(F3qlq(1fC}?8p3dnX&g!ArKb#XM)fCFyf0%8lqr3v>$fVkxB0g1$a{44R}Upb^N z0N2fPTDkVE^{1^eu`JnK;dAUBx$;Zn@ZA; zFDCJtd$8^zF6K>qqMNAlD+69`LfHr3Ha0FQ4hMS(XBhe2Foe}OD$QoPrH-@mDKU}9 z-!>=nIlizR4y8o8{fB%%6A2j+KLg>f`?_wqd21qIjN7W&*vsRyNWo}tz0{M* zBgU!GHx46mt1EezeF#q#85mLo9et@`+W4GlA{I zC4gV|sdKEv*LpK0c?KMH2X#(4Z}o`d(^So8-4A7>GVzYWjx5%!F!VR?iCkcH7VP}KMMpDnIcHz{9^*?@Usa?K`0o?1A&6MA>7;$Ft^_C&BzcqTcZhg zMSvt7&D`vP>i2v8XYesg!rdJmKr)W*aKyQ3WFh(K7$6GF-OPY|I};VC5)wd~Asm76 z_ay#4wsv%o1hNqX{Sl6u)(AUb^dl6-+z?jC9e-Cr==m!6BLW0qV4!IKw;X@&?PaW? zb?{B#(Ei=-tLsWn7`W^w5ORD^@)aiX*bG+1t*!L*tOKewm^m;nh0|q&E=f(1lIf^k zLpQ&-LyZ%dyVwX_jT{CKTgg!8<>sndzUG~s^Ec?PS_MW;{2Z|-QUS9@Q$<@9oAAEygWeKEAjZ96>2ZNA!!^1=~Gxn8Yl@mGus z6PV2SXxTo?&l^^340KeP67*h`Jeqql^ZI1|&GQngViddLT>e<`IYbs+Felv4FK~&RuHcnzF|>s7&Y6%* z;leAa#=54rK)%72?RcU`Sf%&g&Zlf~D872~ViQDQ3c`Bdp{I)>G)LnJ*(03mzz$Qr zSO>=O-NHqM5JXgB>aK?5S6<@nl6wL{TqI)-p?H8XIEN`Lb4Q{eL==OA%ox+bf8FV_ zF#1Ym_>uTzHd?BL(FGLV!eDQ!dlps!h7T}sOd#xmNtHCxbHqniPJ=vRMT_rKWJjxV zLF96ZUx@Kv{O|(Z)tHn0-kJ&F)FqjDgGN-N+=~%?4^?$mIgLT&)$MpRh4;x{;NqBY zO`%;5pf&U#-D2}36!nh*1>yEATn|_EP`#|yW+@tQS)wT?D*%I}bKi;nlYdtGHGWi? zIWK-3+V;))OTM90PpU|=uI-yc-YK>bu;SNULSyd(p+mFDShLY>@`&BqsmT1@oEr9) z7q=8VaZXWF=Ge$&Znj$~KC^xh$Ty!xMrZL#e1<-y5NZ>uCo`=$Eq_OG6hVB4a=m~H zCTj8Em?N!S@Om2#HRc@_cMJ~9SZp`}hGi%A>V2{C3w#~xVB9bBxGlJC_ZsF6JZVKk zM&hK=y&}716}xeVI;?er@>e@7(t~R%37ee-X2K5_T>0-+%�VR>k4&ppxe#tIHJa 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 literal 0 HcmV?d00001 diff --git a/examples/kitchen-sink-accessible-tiny.js b/examples/kitchen-sink-accessible-tiny.js new file mode 100644 index 00000000..23b13f46 --- /dev/null +++ b/examples/kitchen-sink-accessible-tiny.js @@ -0,0 +1,67 @@ +var PDFDocument = require('../'); +var fs = require('fs'); + +// Create a new PDFDocument +var doc = new PDFDocument({ + autoFirstPage: true, + bufferPages: true, + pdfVersion: '1.5', + // @ts-ignore PDF/UA needs to be enforced for PAC accessibility checker + subset: 'PDF/UA', + tagged: true, + displayTitle: true, + lang: 'en-US', + fontSize: 12, +}); + +doc.pipe(fs.createWriteStream('accessible-links.pdf')); + +// Set some meta data +doc.info['Title'] = 'Test Document'; +doc.info['Author'] = 'Devon Govett'; + +// Initialise document logical structure +var struct = doc.struct('Document'); +doc.addStructure(struct); + +// Register a font name for use later +doc.registerFont('Palatino', 'fonts/PalatinoBold.ttf'); + +// Set the font and draw some text +struct.add( + doc.struct('P', () => { + doc + .font('Palatino') + .fontSize(25) + .text('Some text with an embedded font! ', 100, 100); + }) +); + + +// Add another page +doc.addPage(); + +// Add some text with annotations +var linkSection = doc.struct('Sect'); +struct.add(linkSection); + +linkSection.add( + doc.struct( + 'Link', + { + alt: 'Here is a link! ' + }, + () => { + doc.font('Palatino').fillColor('blue').text('Here is a link!', 100, 100, { + link: 'http://google.com/', + underline: true + }); + } + ) +); + +linkSection.end(); + + +// End and flush the document +doc.end(); diff --git a/examples/kitchen-sink-accessible.js b/examples/kitchen-sink-accessible.js index bf0a824e..06b25d8e 100644 --- a/examples/kitchen-sink-accessible.js +++ b/examples/kitchen-sink-accessible.js @@ -7,7 +7,9 @@ var doc = new PDFDocument({ pdfVersion: '1.5', lang: 'en-US', tagged: true, - displayTitle: true + displayTitle: true, + // @ts-ignore PDF/UA needs to be enforced for PAC accessibility checker + subset: 'PDF/UA', }); doc.pipe(fs.createWriteStream('kitchen-sink-accessible.pdf')); diff --git a/examples/kitchen-sink-accessible.pdf b/examples/kitchen-sink-accessible.pdf index 002f9b523da6bf86d78fab985120d2802599a0af..91a2f54f00dedbe6ba1602ff9552e9b4e413ea26 100644 GIT binary patch delta 1583 zcmah}U1%It6!z@K+T2vBq|sIdZ`TH!LOS=}xp!vnvRTAbu|JyFR^JLvI2 zx@o{{p-|t%7r6=|zWAmOf_6;RfNyTvrckAig0zYVdGtYi&@)|21~?@3Bi?!JjKbll58@)}kgoJZAn_fma^2&P0vY`Dog2AZ*4vaD0rGO2j&Q zaWSOU)Qcxh6KiVb0+os~iucpARPx?!xoi$zg8lJ(9k{$}NO8d=my&Z8KXV0^ZS&zZ z*fV%4tj$Jql-p)y9STGL{EI99&|~qy_wnUbAn>#`dHmR!FrtpLm6U|FpgwbfIIfvq z0~)VhgOQxMy9UGYuiwEhFzO~7&tZr4H)_@M^&q5{7=j=P(KpIrgR)Fbj# zM=-8Y_GHl-CCL@dBwU|J^rI(T#F(u8HOJsP&%hqxn z^Ekc=Ga_Rxo++t4CzArXW+Xx_v-`9(uU{m?9WTv}F~L&`a&8NxwSzHY_(RHI(#`lt zGCS=-E|q(~rOpErau-Xd6FNF$F7661r3H?}5gE)=Ds5miX7Wqxk1lGM;xgHbpUcT* zLf_)t_Z^=HGb;3$TTwO>DsaL;YUyhh$TBVc@0N37NA`K delta 1300 zcmai!Pe@cj9LM?Xn&s3X(ybPP-XS3a+4;@Pdov>%gCmi$~>YN)Sxq-3sr7QWdg70`TL}qjm4hN6lL8d9?>F{R8oLYeWMlQk1 zJ{6{r?b?#oYn$$6xD%!?EyD{l{{fnVdn@o1T4W)~1;ig6udS(Hj7 z3o0qh(sk0CMu8?fPFhpUHOI$EUz$l8%#M=`q^K~@tK?BXy0QCIYswQ$DV1cXVgpgF zkb36+1kp9cMRZ#GQA)e;e^pt=y(D8I=17=leT>{il%B#Uvw1GBJ2$j#yM+Q{s|cBl zS!|iaWMfw+g?R4VP0~_Jbk{PzOKYdXgmt!iqLJ;M7*cDDtELpi-+^k|4~tXbMy){P zobm)yUTkM*%hA{{h@MQoD@idbIA?+6ZWLN(A9GS6L6V1+zo5{shC>HKd>{qJzCGF+}y%YKn~%0*X^*4C3}ddMHca6#Ds diff --git a/lib/mixins/annotations.js b/lib/mixins/annotations.js index 51a72c57..ad9b7047 100644 --- a/lib/mixins/annotations.js +++ b/lib/mixins/annotations.js @@ -1,3 +1,5 @@ +import PDFAnnotationReference from '../structure_annotation'; + export default { annotate(x, y, w, h, options) { options.Type = 'Annot'; @@ -19,6 +21,9 @@ export default { options.Dest = new String(options.Dest); } + const structParent = options.structParent; + delete options.structParent; + // Capitalize keys for (let key in options) { const val = options[key]; @@ -27,6 +32,12 @@ export default { const ref = this.ref(options); this.page.annotations.push(ref); + + if (structParent && typeof structParent.add === 'function') { + const annotRef = new PDFAnnotationReference(ref); + structParent.add(annotRef); + } + ref.end(); return this; }, @@ -77,6 +88,10 @@ export default { options.A.end(); } + if (options.structParent && !options.Contents) { + options.Contents = new String(''); + } + return this.annotate(x, y, w, h, options); }, diff --git a/lib/mixins/text.js b/lib/mixins/text.js index be34348b..12ff368a 100644 --- a/lib/mixins/text.js +++ b/lib/mixins/text.js @@ -531,7 +531,11 @@ export default { // create link annotations if the link option is given if (options.link != null) { - this.link(x, y, renderedWidth, this.currentLineHeight(), options.link); + const linkOptions = {}; + if (this._currentStructureElement && this._currentStructureElement.dictionary.data.S === 'Link') { + linkOptions.structParent = this._currentStructureElement; + } + this.link(x, y, renderedWidth, this.currentLineHeight(), options.link, linkOptions); } if (options.goTo != null) { this.goTo(x, y, renderedWidth, this.currentLineHeight(), options.goTo); diff --git a/lib/structure_annotation.js b/lib/structure_annotation.js new file mode 100644 index 00000000..fe5ddbfd --- /dev/null +++ b/lib/structure_annotation.js @@ -0,0 +1,7 @@ +class PDFAnnotationReference { + constructor(annotationRef) { + this.annotationRef = annotationRef; + } +} + +export default PDFAnnotationReference; diff --git a/lib/structure_element.js b/lib/structure_element.js index 4f62d7ae..9f9cf19e 100644 --- a/lib/structure_element.js +++ b/lib/structure_element.js @@ -4,6 +4,7 @@ By Ben Schmidt */ import PDFStructureContent from './structure_content'; +import PDFAnnotationReference from './structure_annotation'; class PDFStructureElement { constructor(document, type, options = {}, children = null) { @@ -71,6 +72,10 @@ class PDFStructureElement { this._addContentToParentTree(child); } + if (child instanceof PDFAnnotationReference) { + this._addAnnotationToParentTree(child.annotationRef); + } + if (typeof child === 'function' && this._attached) { // _contentForClosure() adds the content to the parent tree child = this._contentForClosure(child); @@ -90,6 +95,15 @@ class PDFStructureElement { }); } + _addAnnotationToParentTree(annotRef) { + const parentTreeKey = this.document.createStructParentTreeNextKey(); + + annotRef.data.StructParent = parentTreeKey; + + const parentTree = this.document.getStructParentTree(); + parentTree.add(parentTreeKey, this.dictionary); + } + setParent(parentRef) { if (this.dictionary.data.P) { throw new Error(`Structure element added to more than one parent`); @@ -137,13 +151,25 @@ class PDFStructureElement { return ( child instanceof PDFStructureElement || child instanceof PDFStructureContent || + child instanceof PDFAnnotationReference || typeof child === 'function' ); } _contentForClosure(closure) { const content = this.document.markStructureContent(this.dictionary.data.S); + + const prevStructElement = this.document._currentStructureElement; + this.document._currentStructureElement = this; + + const wasEnded = this._ended; + this._ended = false; + closure(); + + this._ended = wasEnded; + + this.document._currentStructureElement = prevStructElement; this.document.endMarkedContent(); this._addContentToParentTree(content); @@ -209,6 +235,16 @@ class PDFStructureElement { } }); } + + if (child instanceof PDFAnnotationReference) { + const pageRef = this.document.page.dictionary; + const objr = { + Type: 'OBJR', + Obj: child.annotationRef, + Pg: pageRef, + }; + this.dictionary.data.K.push(objr); + } } } diff --git a/tests/unit/annotations.spec.js b/tests/unit/annotations.spec.js index 2210f7ca..34f2a97a 100644 --- a/tests/unit/annotations.spec.js +++ b/tests/unit/annotations.spec.js @@ -178,4 +178,41 @@ describe('Annotations', () => { ]); }); }); + + describe('annotations with structure parent', () => { + test('should add structParent to link annotations', () => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false, + tagged: true, + }); + + const docData = logData(document); + + const linkElement = document.struct('Link'); + document.addStructure(linkElement); + + document.link(100, 100, 100, 20, 'http://example.com', { + structParent: linkElement, + }); + + linkElement.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/StructParent 0'); + expect(dataStr).toContain('/Contents ()'); + }); + + test('should work without structParent (backwards compatibility)', () => { + const docData = logData(document); + + document.link(100, 100, 100, 20, 'http://example.com'); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/Subtype /Link'); + expect(dataStr).not.toContain('/StructParent'); + }); + }); }); diff --git a/tests/unit/markings.spec.js b/tests/unit/markings.spec.js index 6486c17d..c07fedf0 100644 --- a/tests/unit/markings.spec.js +++ b/tests/unit/markings.spec.js @@ -525,6 +525,23 @@ EMC document.struct('Foo', [1]); }).toThrow(); }); + + test('_currentStructureElement tracking with closures', () => { + const section = document.struct('Sect'); + document.addStructure(section); + + let capturedStructElement = null; + + const paragraph = document.struct('P', () => { + capturedStructElement = document._currentStructureElement; + }); + + section.add(paragraph); + section.end(); + document.end(); + + expect(capturedStructElement).toBe(paragraph); + }); }); describe('accessible document', () => { diff --git a/tests/unit/structure_annotation.spec.js b/tests/unit/structure_annotation.spec.js new file mode 100644 index 00000000..28faa82a --- /dev/null +++ b/tests/unit/structure_annotation.spec.js @@ -0,0 +1,66 @@ +import PDFDocument from '../../lib/document'; +import PDFAnnotationReference from '../../lib/structure_annotation'; +import { logData } from './helpers'; + +describe('PDFAnnotationReference', () => { + let document; + + beforeEach(() => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false, + tagged: true, + }); + }); + + test('should add annotation reference to structure element with StructParent', () => { + const docData = logData(document); + + const linkElement = document.struct('Link'); + document.addStructure(linkElement); + + const annotRef = document.ref({ + Type: 'Annot', + Subtype: 'Link', + Rect: [100, 100, 200, 120], + }); + + linkElement.add(new PDFAnnotationReference(annotRef)); + linkElement.end(); + annotRef.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/Type /OBJR'); + expect(dataStr).toContain('/StructParent 0'); + }); + + test('should handle multiple annotations with different StructParent values', () => { + const docData = logData(document); + + const section = document.struct('Sect'); + document.addStructure(section); + + const link1 = document.struct('Link'); + const link2 = document.struct('Link'); + section.add(link1); + section.add(link2); + + const annotRef1 = document.ref({ Type: 'Annot', Subtype: 'Link' }); + const annotRef2 = document.ref({ Type: 'Annot', Subtype: 'Link' }); + + link1.add(new PDFAnnotationReference(annotRef1)); + link2.add(new PDFAnnotationReference(annotRef2)); + + link1.end(); + link2.end(); + section.end(); + annotRef1.end(); + annotRef2.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/StructParent 0'); + expect(dataStr).toContain('/StructParent 1'); + }); +}); diff --git a/tests/unit/text.spec.js b/tests/unit/text.spec.js index 6a1153cc..9427d7eb 100644 --- a/tests/unit/text.spec.js +++ b/tests/unit/text.spec.js @@ -193,4 +193,47 @@ Q expect(docData).toContainText({ text }); }); }); + + describe('text with structure parent links', () => { + beforeEach(() => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false, + tagged: true, + }); + }); + + test('should auto-link text inside Link structure element', () => { + const docData = logData(document); + + const linkElement = document.struct('Link', () => { + document.text('Click here', 100, 100, { + link: 'http://example.com', + }); + }); + + document.addStructure(linkElement); + linkElement.end(); + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/S /Link'); + expect(dataStr).toContain('/StructParent'); + }); + + test('should not add StructParent outside Link structure', () => { + const docData = logData(document); + + document.text('Click here', 100, 100, { + link: 'http://example.com', + }); + + document.end(); + + const dataStr = docData.join('\n'); + expect(dataStr).toContain('/Subtype /Link'); + expect(dataStr).not.toContain('/StructParent'); + }); + }); }); + From 03abf95057584e7fbef2cce3e9409a8af0a3ea19 Mon Sep 17 00:00:00 2001 From: adyry Date: Thu, 4 Dec 2025 16:09:30 +0100 Subject: [PATCH 3/5] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06fcded2..26da5504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Unreleased - Fix garbled text copying in Chrome/Edge for PDFs with >256 unique characters (#1659) +- Fix Link accessibility issues ### [v0.17.2] - 2025-08-30 From 5b177d061a3d83dd53c20ab6b132f174defbbb8d Mon Sep 17 00:00:00 2001 From: adyry Date: Thu, 18 Dec 2025 13:40:05 +0100 Subject: [PATCH 4/5] Fix links leakage into subsequent structures --- ...accessible-tiny.js => accessible-links.js} | 27 +++++++++-- examples/accessible-links.pdf | Bin 15245 -> 15906 bytes lib/mixins/markings.js | 7 +++ tests/unit/text.spec.js | 44 ++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) rename examples/{kitchen-sink-accessible-tiny.js => accessible-links.js} (69%) diff --git a/examples/kitchen-sink-accessible-tiny.js b/examples/accessible-links.js similarity index 69% rename from examples/kitchen-sink-accessible-tiny.js rename to examples/accessible-links.js index 23b13f46..13deaff4 100644 --- a/examples/kitchen-sink-accessible-tiny.js +++ b/examples/accessible-links.js @@ -1,4 +1,4 @@ -var PDFDocument = require('../'); +var PDFDocument = require('../js/pdfkit'); var fs = require('fs'); // Create a new PDFDocument @@ -45,21 +45,40 @@ doc.addPage(); var linkSection = doc.struct('Sect'); struct.add(linkSection); -linkSection.add( +var paragraph = doc.struct('P'); +linkSection.add(paragraph); + +paragraph.add( + doc.struct('Span', () => { + doc.font('Palatino').fillColor('black').text('This is some text before ', 100, 100, { + continued: true + }); + }) +); + +paragraph.add( doc.struct( 'Link', { alt: 'Here is a link! ' }, () => { - doc.font('Palatino').fillColor('blue').text('Here is a link!', 100, 100, { + doc.fillColor('blue').text('Here is a link!', { link: 'http://google.com/', - underline: true + underline: true, + continued: true }); } ) ); +paragraph.add( + doc.struct('Span', () => { + doc.fillColor('black').text(' and this is text after the link.'); + }) +); + +paragraph.end(); linkSection.end(); diff --git a/examples/accessible-links.pdf b/examples/accessible-links.pdf index 4a41fdd1fd77ed0be589c8296546834d151c2ae1..fc9722a6e4867336c5e398b2a1888ed9f5e3e0b9 100644 GIT binary patch delta 11679 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 delta 11050 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 G*8c;$2Sz{u diff --git a/lib/mixins/markings.js b/lib/mixins/markings.js index a0592e24..7a1c5f05 100644 --- a/lib/mixins/markings.js +++ b/lib/mixins/markings.js @@ -99,6 +99,13 @@ export default { endMarkedContent() { this.page.markings.pop(); this.addContent('EMC'); + if (this._textOptions) { + delete this._textOptions.link; + delete this._textOptions.goTo; + delete this._textOptions.destination; + delete this._textOptions.underline; + delete this._textOptions.strike; + } return this; }, diff --git a/tests/unit/text.spec.js b/tests/unit/text.spec.js index 9427d7eb..073064f2 100644 --- a/tests/unit/text.spec.js +++ b/tests/unit/text.spec.js @@ -234,6 +234,50 @@ Q expect(dataStr).toContain('/Subtype /Link'); expect(dataStr).not.toContain('/StructParent'); }); + + test('should not leak link options to subsequent structure elements with continued text', () => { + const docData = logData(document); + + const paragraph = document.struct('P'); + document.addStructure(paragraph); + + paragraph.add( + document.struct('Span', () => { + document.text('This is some text before ', 100, 100, { + continued: true, + }); + }) + ); + + paragraph.add( + document.struct('Link', () => { + document.text('Here is a link!', { + link: 'http://google.com/', + underline: true, + continued: true, + }); + }) + ); + + paragraph.add( + document.struct('Span', () => { + document.text(' and this is text after the link.'); + }) + ); + + paragraph.end(); + document.end(); + + const dataStr = docData.join('\n'); + + // Count how many link annotations exist - should be exactly 1 + const linkMatches = dataStr.match(/\/Subtype \/Link/g); + expect(linkMatches).toBeTruthy(); + expect(linkMatches.length).toBe(1); + + expect(dataStr).toContain('/S /Span'); + expect(dataStr).toContain('/S /Link'); + }); }); }); From bd8e7e673294bb836ac98b6e3aff05626437d5cf Mon Sep 17 00:00:00 2001 From: adyry Date: Thu, 18 Dec 2025 13:43:29 +0100 Subject: [PATCH 5/5] Prettier --- examples/accessible-links.js | 27 ++++++++++++++------------- lib/mixins/text.js | 14 ++++++++++++-- tests/unit/text.spec.js | 11 +++++------ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/examples/accessible-links.js b/examples/accessible-links.js index 13deaff4..bc4e2cbd 100644 --- a/examples/accessible-links.js +++ b/examples/accessible-links.js @@ -1,4 +1,4 @@ -var PDFDocument = require('../js/pdfkit'); +var PDFDocument = require('../'); var fs = require('fs'); // Create a new PDFDocument @@ -34,10 +34,9 @@ struct.add( .font('Palatino') .fontSize(25) .text('Some text with an embedded font! ', 100, 100); - }) + }), ); - // Add another page doc.addPage(); @@ -50,37 +49,39 @@ linkSection.add(paragraph); paragraph.add( doc.struct('Span', () => { - doc.font('Palatino').fillColor('black').text('This is some text before ', 100, 100, { - continued: true - }); - }) + doc + .font('Palatino') + .fillColor('black') + .text('This is some text before ', 100, 100, { + continued: true, + }); + }), ); paragraph.add( doc.struct( 'Link', { - alt: 'Here is a link! ' + alt: 'Here is a link! ', }, () => { doc.fillColor('blue').text('Here is a link!', { link: 'http://google.com/', underline: true, - continued: true + continued: true, }); - } - ) + }, + ), ); paragraph.add( doc.struct('Span', () => { doc.fillColor('black').text(' and this is text after the link.'); - }) + }), ); paragraph.end(); linkSection.end(); - // End and flush the document doc.end(); diff --git a/lib/mixins/text.js b/lib/mixins/text.js index 12ff368a..82906531 100644 --- a/lib/mixins/text.js +++ b/lib/mixins/text.js @@ -532,10 +532,20 @@ export default { // create link annotations if the link option is given if (options.link != null) { const linkOptions = {}; - if (this._currentStructureElement && this._currentStructureElement.dictionary.data.S === 'Link') { + if ( + this._currentStructureElement && + this._currentStructureElement.dictionary.data.S === 'Link' + ) { linkOptions.structParent = this._currentStructureElement; } - this.link(x, y, renderedWidth, this.currentLineHeight(), options.link, linkOptions); + this.link( + x, + y, + renderedWidth, + this.currentLineHeight(), + options.link, + linkOptions, + ); } if (options.goTo != null) { this.goTo(x, y, renderedWidth, this.currentLineHeight(), options.goTo); diff --git a/tests/unit/text.spec.js b/tests/unit/text.spec.js index 073064f2..8d6cc7e7 100644 --- a/tests/unit/text.spec.js +++ b/tests/unit/text.spec.js @@ -246,7 +246,7 @@ Q document.text('This is some text before ', 100, 100, { continued: true, }); - }) + }), ); paragraph.add( @@ -256,28 +256,27 @@ Q underline: true, continued: true, }); - }) + }), ); paragraph.add( document.struct('Span', () => { document.text(' and this is text after the link.'); - }) + }), ); paragraph.end(); document.end(); const dataStr = docData.join('\n'); - + // Count how many link annotations exist - should be exactly 1 const linkMatches = dataStr.match(/\/Subtype \/Link/g); expect(linkMatches).toBeTruthy(); expect(linkMatches.length).toBe(1); - + expect(dataStr).toContain('/S /Span'); expect(dataStr).toContain('/S /Link'); }); }); }); -