From c9fb4554c8bd4803712d3d9cf4f06956bdcd8d88 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 1 Oct 2024 12:43:26 +0200 Subject: [PATCH 1/5] Fix getBoundingBoxByChildren function Do not use dummy (all 0) bounding box when search bonding box for all children. Otherwise minimal values always will be 0. Also take into account transformation attributes of child node - it may differ from child to child. Apply scale and translation, but not rotation yet --- src/utils/bbox.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/utils/bbox.ts b/src/utils/bbox.ts index 66d3236a..c9091962 100644 --- a/src/utils/bbox.ts +++ b/src/utils/bbox.ts @@ -7,9 +7,20 @@ export function getBoundingBoxByChildren(context: Context, svgnode: SvgNode): nu if (getAttribute(svgnode.element, context.styleSheets, 'display') === 'none') { return [0, 0, 0, 0] } - let boundingBox = [0, 0, 0, 0] + let boundingBox : number[] = [] svgnode.children.forEach(child => { const nodeBox = child.getBoundingBox(context) + if ((nodeBox[0] === 0) && (nodeBox[1] === 0) && (nodeBox[2] === 0) && (nodeBox[3] === 0)) + return + const transform = child.computeNodeTransform(context) + // TODO: take into account rotation matrix + nodeBox[0] = nodeBox[0] * transform.sx + transform.tx + nodeBox[1] = nodeBox[1] * transform.sy + transform.ty + nodeBox[2] = nodeBox[2] * transform.sx + nodeBox[3] = nodeBox[3] * transform.sy + if (boundingBox.length === 0) + boundingBox = nodeBox + else boundingBox = [ Math.min(boundingBox[0], nodeBox[0]), Math.min(boundingBox[1], nodeBox[1]), @@ -19,7 +30,7 @@ export function getBoundingBoxByChildren(context: Context, svgnode: SvgNode): nu Math.min(boundingBox[1], nodeBox[1]) ] }) - return boundingBox + return boundingBox.length === 0 ? [0, 0, 0, 0] : boundingBox } export function defaultBoundingBox(element: Element, context: Context): Rect { From 3070ae40c0409e90b5d686c644cdde54d8dba4c4 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 1 Oct 2024 12:45:39 +0200 Subject: [PATCH 2/5] Provide bounding box for plain text - without tspan While `a` element typically used together with text, one need to have estimation of bounding box for text. To avoid reimplementation of complete text rendering logic just remember text position and sizes as bounding box. Implemented only for plain text, any usage of tspan will not work properly --- src/nodes/text.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nodes/text.ts b/src/nodes/text.ts index 4cd0c07e..ba73d068 100644 --- a/src/nodes/text.ts +++ b/src/nodes/text.ts @@ -24,6 +24,7 @@ interface TrimInfo { } export class TextNode extends GraphicsNode { + private boundingBox: number[] = [] private processTSpans( textNode: SvgNode, node: Element, @@ -159,6 +160,7 @@ export class TextNode extends GraphicsNode { renderingMode: textRenderingMode === 'fill' ? void 0 : textRenderingMode, charSpace: charSpace === 0 ? void 0 : charSpace }) + this.boundingBox = [textX + dx - xOffset, textY + dy + 0.1 * pdfFontSize, context.textMeasure.measureTextWidth(transformedText, context.attributeState), pdfFontSize] } } else { // otherwise loop over tspans and position each relative to the previous one @@ -228,7 +230,7 @@ export class TextNode extends GraphicsNode { } protected getBoundingBoxCore(context: Context): Rect { - return defaultBoundingBox(this.element, context) + return this.boundingBox.length > 0 ? this.boundingBox : defaultBoundingBox(this.element, context) } protected computeNodeTransformCore(context: Context): Matrix { From b60f9a0d9f2d2b91089a89fa5414d12a863be63d Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 1 Oct 2024 12:47:48 +0200 Subject: [PATCH 3/5] Introduce Anchor class It derives from Group class and uses same rendering. Plus if `href` attribute specified - adds link with bounding box. Requires usage of scale and page height values - otherwise created link area does not match drawn text --- src/nodes/anchor.ts | 19 +++++++++++++++++++ src/parse.ts | 3 +++ 2 files changed, 22 insertions(+) create mode 100644 src/nodes/anchor.ts diff --git a/src/nodes/anchor.ts b/src/nodes/anchor.ts new file mode 100644 index 00000000..0601bbfc --- /dev/null +++ b/src/nodes/anchor.ts @@ -0,0 +1,19 @@ +import { Group } from './group' +import { Context } from '../context/context' +import { getAttribute } from '../utils/node' + +export class Anchor extends Group { + protected async renderCore(context: Context): Promise { + await super.renderCore(context) + + const href = getAttribute(this.element, context.styleSheets, 'href') + if (href) { + const box = this.getBoundingBox(context) + const scale = context.pdf.internal.scaleFactor + const ph = context.pdf.internal.pageSize.getHeight() + + context.pdf.link(scale*(box[0] * context.transform.sx + context.transform.tx), + ph - scale*(box[1] * context.transform.sy + context.transform.ty), scale*box[2], scale*box[3], { url: href }) + } + } +} diff --git a/src/parse.ts b/src/parse.ts index f0708aac..7bfa0615 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -17,6 +17,7 @@ import { RadialGradient } from './nodes/radialgradient' import { Polyline } from './nodes/polyline' import { Svg } from './nodes/svg' import { Group } from './nodes/group' +import { Anchor } from './nodes/anchor' import cssesc from 'cssesc' import { ClipPath } from './nodes/clippath' import { Symbol } from './nodes/symbol' @@ -29,6 +30,8 @@ export function parse(node: Element, idMap?: { [id: string]: SvgNode }): SvgNode switch (node.tagName.toLowerCase()) { case 'a': + svgnode = new Anchor(node, children) + break case 'g': svgnode = new Group(node, children) break From 702be9e702be9e6b9f4aef4666a000b31c2e8084 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 28 Oct 2024 11:16:03 +0100 Subject: [PATCH 4/5] Add test for the anchor element This test also can be used to demonstrate missing support of dominant-baseline attribute of the text --- test/common/tests.js | 1 + test/specs/anchor/reference.pdf | Bin 0 -> 6697 bytes test/specs/anchor/spec.svg | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 test/specs/anchor/reference.pdf create mode 100644 test/specs/anchor/spec.svg diff --git a/test/common/tests.js b/test/common/tests.js index 1b9818fb..6bd06da5 100644 --- a/test/common/tests.js +++ b/test/common/tests.js @@ -1,4 +1,5 @@ window.tests = [ + 'anchor', 'attribute-style-precedence', 'clippath', 'clippath-empty', diff --git a/test/specs/anchor/reference.pdf b/test/specs/anchor/reference.pdf new file mode 100644 index 0000000000000000000000000000000000000000..21cac67a6dd1d9affd63fa6a6f793eb321de7c96 GIT binary patch literal 6697 zcmc&(?{3>R5dY4nATR_=F+>une^wC`h?Axbnjx+2uET}0iQ^>C_;sz2I9wkt z$2kpf8ODEdP#NU>+JcVKbu}(bM{{h$^bAMZpv%=ha=o9V6O|TY81Q|FS6&lK2e`klq^)L&6BTT6!}_`PIVO@y&vk6B*^p+YOD``d!?V`c^ZzFxdQe) zjQu!28vB`w!dU$@^W$k4PoMD@FW|R>0TK+5hS2Pu;|Yr_koHqpF!%GBf=3wm(avTF z@`E%rQyQiKFUTkH!aSTzBDMSM(FsBd%`qIyvdIAPBu)rm;iTVzKf&*qnsgk;5H_v@ z@gyGbqs7c0D~O_A`bLs?Zp}lIc%mUlbir+i zij}Nv$P!(Q)R2{hY@fZOY2UH_i0S7ZK*L8j%(TFa(Ivn)t!+W@Xb*rHjppodm4ZjN z?Bxg);U8kKi!z)BDqGKnkI_ijkjgSSJxWk*SHG7 zwBgGY6XVjXf)FXu(@e`0mhIYxZMY8XFWiD8VLLtBl9p$A((IX%3rOJ@4fqb2+3F>n z9<`X5=v!^55&0OS&=aYae%Et(mqys{iU@>vz`ABd5@=6{#>&os-B6&eF!&ZQ2yg+B z(=r21?3#iD1|cx`?gj>1(CjXa$j31YVBD4&2z(bXZV3#&yMZAIXzkL7z!YQd08E*V zC}i3m-Uf|W1cF8hL_0FO01{Q`5YY`$Ap=-+Y9&L9k#|9Mu?3+jhPspzLUjxkXNq@t^efMH`RUA0xhdgX>X#qVwi|;x4X_=}q}$p2$MhrOnq79X?)i2IY@@XLIhMBk~uxuV{tNibAUxz(YJ z%=MA_4BOkq9BRh>@_kyqFJP~v-S7Aq2*EKRpp<+rIeG>ulOsGxSsYD3%I26rk#eB= zRgx|o+)EO!fMp393JZcZWsn6)QPgyR7My|)b%3m>Mgw6{jR(R6+5k6F{r9W!Emosp zqVQSo>!O*Rjf{v zXgQA?<9trl4HR3XV8vAM$sOA^ZGOW`p4oH2Ez}ZMJh=J*!s|Q(>XB`pfbvqCXL@p@ zuVq*O`?@|0YRwxw6aC$ivpV|P?xvjMfv;M9dvMfm^E_u`T-)$AY_sL29RS>hKHKU+ z*sYIi!~X(o@I0u2wdA12+mM5baTvj&02j+J{HoAz`XEViC}|X-e-Ym#oI(;J`{ImW g^JCKZjmL|Jh + + + + hanging + + + + mathematical + + + + middle + + + + central + + + + alphabetic + + + + ideographic + + + + + + + + + From f6e62f662442016e01d48f116859c7ef86dc5640 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 7 Nov 2024 16:02:53 +0100 Subject: [PATCH 5/5] Update some other refs Changes because of new implementation of bounding box --- .../complete-computer-network/reference.pdf | Bin 116526 -> 116526 bytes test/specs/complete-movies/reference.pdf | Bin 22563 -> 22588 bytes .../reference.pdf | Bin 15003 -> 15001 bytes .../complete-organization-chart/reference.pdf | Bin 128911 -> 128911 bytes .../complete-social-network/reference.pdf | Bin 197562 -> 197561 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/specs/complete-computer-network/reference.pdf b/test/specs/complete-computer-network/reference.pdf index 6b4258bb2997e2b6bda3db9bd906b3e812a84219..448ca4544f3fb05f8ff5fb941465c5763c15be3c 100644 GIT binary patch delta 73 zcmZ42$G)zQeZvV=3ljrN0|YQKRj@SIGc`0t2pO7KD41L7nInrC7@IVyZBt{^TL=KN CPZ4YY delta 73 zcmZ42$G)zQeZvV=3sXxY0|YQKRZSxiD zW=Up43zN-Jq1zbi&CC_RDi!h+xWEiUOG`@&G%+IsLo-thF%wHO3^6lPOfgFfbUTa; fP0bV(DDVM6MtG^onxhOOs diff --git a/test/specs/complete-organization-chart-new/reference.pdf b/test/specs/complete-organization-chart-new/reference.pdf index d5f717d9dc73466d9283094db45681f08aa122c4..8c0a37bf8929ad7e712ea7277f2120b568abc995 100644 GIT binary patch delta 91 zcmbPTIhA^sC|VYY delta 93 zcmbPPI=gg3Jde7$o{_17uCckEv4M#t5-?XVG&IyRHw9CchMQY>nzR|sHnUsYXB0J6 eFaQCCJOwT=!_dUg!f^6$%jxWG&C_(ur$L^H@zTXQ`uruI}u(UKc0h(;2 zXKZSKiX3b%JFFnVc%-G=HBu-lA`Q9NP@F#$!)1nf3bQzZAAn}gl9-OG#d tx;S>Djr5Gnj1XE3j4ZGlZ3$Ed^adQ5n{5YrTT_|Zra~Od*T&db2mt1WY25$- delta 478 zcmeBw&))x@eS^(*Aqzc2V+&IQ1TdZKw_Q@e#LyH;(86qU`F3#$f1qL$gi0XGOhMP! zNYBvJ5=pV4v4Vw(o{15%LPK)}OEWzaa|;toI54xAUXaA7wwd{YZ-6z}8E`2e!x(I` zv8e&F4s)={hUQ2{8=7tJR$%nf1i1~(BT%;?yV%$iVgjn?z-}``ay}TH& ri(@z1SkKVX5Xm7%IE=Q`Gc`9xQfF?u9q4UMWonxWaWG#SV`CuzqMK-A diff --git a/test/specs/complete-social-network/reference.pdf b/test/specs/complete-social-network/reference.pdf index 4e95b905c84e5f959543e2e0e371485a353cd82c..307ade46b504c36154acbb8d51bbf8224f0970da 100644 GIT binary patch delta 2298 zcmZuyJ!@1^5M?)o5K@RuV)sBISmZHZ_s$HKeiTB8e?vq&OL=x_gp?_SwEh8M6aR={ zXG(Pium-gdUEBG6qr6D ztLv-9;n7)!cPC6eRhps6I+NhtW$4@k$1|MP%Rhe?PrtuhEuP%Zj6LxV3*OIMdn{$k zjH)B^>gM<2*WpNQ+h2hwL}QO^(|`bl*3`ZQqG0Di-QZRs`qSIzqZN~H?kb}dG2qP3 zs%zG=16*CaSiX67sy(UB!0z1%c@NHWbnT3LW10bL=hh?Y3|7_ypMF@r`?>^QUH@CY zUk=)h6LufF{o607AYiFhfm6*{+8Sbb)bOELI*ZlraKpAS0VW_3C4p2*5*RL(wlO2r zRAbuhF~r1NOPYjDQe#0)jfFj(6{RFb9FsGJgojpPJPA)Vbx1YWgsU-fi>oDzsa7`5 z4AZ?Ulyq3xuqD*uX*zY<5KerCl+K#8`53~}TGJ#RA!-{tM*5p@dEtyxX~o1V*7iCL zNTC~c2@NW=stpX=i48F~LFQJVVT~qD541XEVYqvCM(DfA1T+#zgrt%P$+-Jvfsut!+^+vQ5Oty+i5>ue2D=F-8cJzPR0g+2=6E8~$fp&3uDdO~6QcY8)FsW;R6TGIS0o6D=!z1sw35moNL zU#(8>o=14|2(bl&nIYxUD77_1W7ZbI1y0K5_n*~+uTT5c{hOI{3xyUL_#b9&iSvwu z9dm#EYxVPVpk9)s#4dWEwndA8qv!=oN+69nwJ6#2W9?n^hu4osE2gmj!I2e9TB2oZ zh0Nj-aDVw^{o>&(ZBv#7*lcTNSq9YxhNO7`ifUzx9d)u zv2gRgIUfFiBuW-yoG8T1y{>Wr3WgYI$OYCNuA2+z1Ob?H4gzy2L0}f)x-deL5W{>} zqw|iDOXviZLSX_4g$XU>y6-^@nXgafR>?$N2ok3d6N3;l4LC*!I4)5PYuP)~O>>u2 zLb9Ody08=r-HGKr;l!tqL#|T2A03FYgig#iu`XOP0I>)l)dD@!i&Qm0dD?|;cXpb=4mUG_Uo-?le-8p-Hmny@F$Y%_iR)$U z6eY}4{V@zK5aOhIFPaJjTm9UFn36b*q@a23=>PNvXkn-oYB37-J1n@s6qZ$)H53+i zb{a{yef3mw4&6y9VI(3FoKZ+2*Zq$;j(nXe2cX@s3qInA^9O$FWY>rp)iLu;uN^f1 z1`SRR{!jLse?)4#{JV{T5Rd~Q_@}!P<9(!hoPNbR+kZUHA1qU@H{CLabFyr~enXhg hX}6e$+wKmUqxdcu9r;mgPLGbh+_K}dvuEee{{b@Z%Sr$M