Skip to content

Commit 5f6be78

Browse files
authored
Provide href attribute support in a elements (#313)
* 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 * 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 * 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 * Add test for the anchor element This test also can be used to demonstrate missing support of dominant-baseline attribute of the text * Update some other refs Changes because of new implementation of bounding box
1 parent 36bb1b5 commit 5f6be78

File tree

12 files changed

+73
-3
lines changed

12 files changed

+73
-3
lines changed

src/nodes/anchor.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Group } from './group'
2+
import { Context } from '../context/context'
3+
import { getAttribute } from '../utils/node'
4+
5+
export class Anchor extends Group {
6+
protected async renderCore(context: Context): Promise<void> {
7+
await super.renderCore(context)
8+
9+
const href = getAttribute(this.element, context.styleSheets, 'href')
10+
if (href) {
11+
const box = this.getBoundingBox(context)
12+
const scale = context.pdf.internal.scaleFactor
13+
const ph = context.pdf.internal.pageSize.getHeight()
14+
15+
context.pdf.link(scale*(box[0] * context.transform.sx + context.transform.tx),
16+
ph - scale*(box[1] * context.transform.sy + context.transform.ty), scale*box[2], scale*box[3], { url: href })
17+
}
18+
}
19+
}

src/nodes/text.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface TrimInfo {
2424
}
2525

2626
export class TextNode extends GraphicsNode {
27+
private boundingBox: number[] = []
2728
private processTSpans(
2829
textNode: SvgNode,
2930
node: Element,
@@ -159,6 +160,7 @@ export class TextNode extends GraphicsNode {
159160
renderingMode: textRenderingMode === 'fill' ? void 0 : textRenderingMode,
160161
charSpace: charSpace === 0 ? void 0 : charSpace
161162
})
163+
this.boundingBox = [textX + dx - xOffset, textY + dy + 0.1 * pdfFontSize, context.textMeasure.measureTextWidth(transformedText, context.attributeState), pdfFontSize]
162164
}
163165
} else {
164166
// otherwise loop over tspans and position each relative to the previous one
@@ -228,7 +230,7 @@ export class TextNode extends GraphicsNode {
228230
}
229231

230232
protected getBoundingBoxCore(context: Context): Rect {
231-
return defaultBoundingBox(this.element, context)
233+
return this.boundingBox.length > 0 ? this.boundingBox : defaultBoundingBox(this.element, context)
232234
}
233235

234236
protected computeNodeTransformCore(context: Context): Matrix {

src/parse.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { RadialGradient } from './nodes/radialgradient'
1717
import { Polyline } from './nodes/polyline'
1818
import { Svg } from './nodes/svg'
1919
import { Group } from './nodes/group'
20+
import { Anchor } from './nodes/anchor'
2021
import cssesc from 'cssesc'
2122
import { ClipPath } from './nodes/clippath'
2223
import { Symbol } from './nodes/symbol'
@@ -29,6 +30,8 @@ export function parse(node: Element, idMap?: { [id: string]: SvgNode }): SvgNode
2930

3031
switch (node.tagName.toLowerCase()) {
3132
case 'a':
33+
svgnode = new Anchor(node, children)
34+
break
3235
case 'g':
3336
svgnode = new Group(node, children)
3437
break

src/utils/bbox.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,20 @@ export function getBoundingBoxByChildren(context: Context, svgnode: SvgNode): nu
77
if (getAttribute(svgnode.element, context.styleSheets, 'display') === 'none') {
88
return [0, 0, 0, 0]
99
}
10-
let boundingBox = [0, 0, 0, 0]
10+
let boundingBox : number[] = []
1111
svgnode.children.forEach(child => {
1212
const nodeBox = child.getBoundingBox(context)
13+
if ((nodeBox[0] === 0) && (nodeBox[1] === 0) && (nodeBox[2] === 0) && (nodeBox[3] === 0))
14+
return
15+
const transform = child.computeNodeTransform(context)
16+
// TODO: take into account rotation matrix
17+
nodeBox[0] = nodeBox[0] * transform.sx + transform.tx
18+
nodeBox[1] = nodeBox[1] * transform.sy + transform.ty
19+
nodeBox[2] = nodeBox[2] * transform.sx
20+
nodeBox[3] = nodeBox[3] * transform.sy
21+
if (boundingBox.length === 0)
22+
boundingBox = nodeBox
23+
else
1324
boundingBox = [
1425
Math.min(boundingBox[0], nodeBox[0]),
1526
Math.min(boundingBox[1], nodeBox[1]),
@@ -19,7 +30,7 @@ export function getBoundingBoxByChildren(context: Context, svgnode: SvgNode): nu
1930
Math.min(boundingBox[1], nodeBox[1])
2031
]
2132
})
22-
return boundingBox
33+
return boundingBox.length === 0 ? [0, 0, 0, 0] : boundingBox
2334
}
2435

2536
export function defaultBoundingBox(element: Element, context: Context): Rect {

test/common/tests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
window.tests = [
2+
'anchor',
23
'attribute-style-precedence',
34
'clippath',
45
'clippath-empty',

test/specs/anchor/reference.pdf

6.54 KB
Binary file not shown.

test/specs/anchor/spec.svg

Lines changed: 34 additions & 0 deletions
Loading
0 Bytes
Binary file not shown.
25 Bytes
Binary file not shown.
-2 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)