Skip to content

Commit 0891c38

Browse files
authored
add support for marker "orient" attribute and the "context-fill" and "context-stroke" values (#308)
* fix: marker orientation and context-fill / context-stroke * fix: only copy colors to context if defined on element Added tests for marker, use and symbol * chore: rename color param to contextColors
1 parent 841c6e3 commit 0891c38

File tree

16 files changed

+145
-29
lines changed

16 files changed

+145
-29
lines changed

src/applyparseattributes.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function parseAttributes(context: Context, svgNode: SvgNode, node?: Eleme
1919
// update color first so currentColor becomes available for this node
2020
const color = getAttribute(domNode, context.styleSheets, 'color')
2121
if (color) {
22-
const fillColor = parseColor(color, context.attributeState.color)
22+
const fillColor = parseColor(color, context.attributeState)
2323
if (fillColor.ok) {
2424
context.attributeState.color = fillColor
2525
} else {
@@ -64,13 +64,21 @@ export function parseAttributes(context: Context, svgNode: SvgNode, node?: Eleme
6464
context.attributeState.stroke = null
6565
} else {
6666
// gradients, patterns not supported for strokes ...
67-
const strokeRGB = parseColor(stroke, context.attributeState.color)
67+
const strokeRGB = parseColor(stroke, context.attributeState)
6868
if (strokeRGB.ok) {
6969
context.attributeState.stroke = new ColorFill(strokeRGB)
7070
}
7171
}
7272
}
7373

74+
if (stroke && context.attributeState.stroke instanceof ColorFill) {
75+
context.attributeState.contextStroke = context.attributeState.stroke.color
76+
}
77+
78+
if (fill && context.attributeState.fill instanceof ColorFill) {
79+
context.attributeState.contextFill = context.attributeState.fill.color
80+
}
81+
7482
const lineCap = getAttribute(domNode, context.styleSheets, 'stroke-linecap')
7583
if (lineCap) {
7684
context.attributeState.strokeLinecap = lineCap

src/context/attributestate.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RGBColor } from '../utils/rgbcolor'
22
import { Fill } from '../fill/Fill'
33
import { ColorFill } from '../fill/ColorFill'
4+
import { Context } from './context'
45

56
export class AttributeState {
67
public xmlSpace = ''
@@ -26,6 +27,8 @@ export class AttributeState {
2627
public textAnchor = ''
2728
public visibility = ''
2829
public color: RGBColor | null = null
30+
public contextFill: RGBColor | null = null
31+
public contextStroke: RGBColor | null = null
2932
public fillRule: string | null = null
3033

3134
clone(): AttributeState {
@@ -56,6 +59,9 @@ export class AttributeState {
5659
clone.color = this.color
5760
clone.fillRule = this.fillRule
5861

62+
clone.contextFill = this.contextFill
63+
clone.contextStroke = this.contextStroke
64+
5965
return clone
6066
}
6167

@@ -87,6 +93,27 @@ export class AttributeState {
8793
attributeState.color = new RGBColor('rgb(0, 0, 0)')
8894
attributeState.fillRule = 'nonzero'
8995

96+
attributeState.contextFill = null
97+
attributeState.contextStroke = null
98+
9099
return attributeState
91100
}
101+
102+
static getContextColors(context: Context, includeCurrentColor = false): ContextColors {
103+
const colors: ContextColors = {}
104+
if (context.attributeState.contextFill) {
105+
colors['contextFill'] = context.attributeState.contextFill
106+
}
107+
108+
if (context.attributeState.contextStroke) {
109+
colors['contextStroke'] = context.attributeState.contextStroke
110+
}
111+
112+
if (includeCurrentColor && context.attributeState.color) {
113+
colors['color'] = context.attributeState.color
114+
}
115+
return colors
116+
}
92117
}
118+
119+
export type ContextColors = Partial<Pick<AttributeState, 'color' | 'contextFill' | 'contextStroke'>>

src/context/referenceshandler.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import cssEsc from 'cssesc'
22
import { SvgNode } from '../nodes/svgnode'
3-
import { RGBColor } from '../utils/rgbcolor'
3+
import { ContextColors } from './attributestate'
44

55
export class ReferencesHandler {
66
private readonly renderedElements: { [key: string]: SvgNode }
@@ -16,10 +16,10 @@ export class ReferencesHandler {
1616

1717
public async getRendered(
1818
id: string,
19-
color: RGBColor | null,
19+
contextColors: ContextColors | null,
2020
renderCallback: (node: SvgNode) => Promise<void>
2121
): Promise<SvgNode> {
22-
const key = this.generateKey(id, color)
22+
const key = this.generateKey(id, contextColors)
2323
if (this.renderedElements.hasOwnProperty(key)) {
2424
return this.renderedElements[id]
2525
}
@@ -36,7 +36,14 @@ export class ReferencesHandler {
3636
return this.idMap[cssEsc(id, { isIdentifier: true })]
3737
}
3838

39-
public generateKey(id: string, color: RGBColor | null): string {
40-
return this.idPrefix + '|' + id + '|' + (color || new RGBColor('rgb(0,0,0)')).toRGBA()
39+
public generateKey(id: string, contextColors: ContextColors | null): string {
40+
let colorHash = ''
41+
const keys = ['color', 'contextFill', 'contextStroke'] as const
42+
43+
if (contextColors) {
44+
colorHash = keys.map(key => contextColors[key]?.toRGBA() ?? '').join('|')
45+
}
46+
47+
return this.idPrefix + '|' + id + '|' + colorHash
4148
}
4249
}

src/fill/parseFill.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function parseFill(fill: string, context: Context): Fill | null {
2626
}
2727
} else {
2828
// plain color
29-
const fillColor = parseColor(fill, context.attributeState.color)
29+
const fillColor = parseColor(fill, context.attributeState)
3030
if (fillColor.ok) {
3131
return new ColorFill(fillColor)
3232
} else if (fill === 'none') {

src/markerlist.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AttributeState } from './context/attributestate'
12
import { Context } from './context/context'
23
import { MarkerNode } from './nodes/marker'
34

@@ -44,10 +45,11 @@ export class MarkerList {
4445

4546
// as the marker is already scaled by the current line width we must not apply the line width twice!
4647
context.pdf.saveGraphicsState()
47-
await context.refsHandler.getRendered(marker.id, null, node =>
48+
const contextColors = AttributeState.getContextColors(context)
49+
await context.refsHandler.getRendered(marker.id, contextColors, node =>
4850
(node as MarkerNode).apply(context)
4951
)
50-
context.pdf.doFormObject(marker.id, tf)
52+
context.pdf.doFormObject(context.refsHandler.generateKey(marker.id, contextColors), tf)
5153
context.pdf.restoreGraphicsState()
5254
}
5355
}
@@ -62,10 +64,12 @@ export class Marker {
6264
id: string
6365
anchor: number[]
6466
angle: number
67+
isStartMarker: boolean
6568

66-
constructor(id: string, anchor: number[], angle: number) {
69+
constructor(id: string, anchor: number[], angle: number, isStartMarker = false) {
6770
this.id = id
6871
this.anchor = anchor
6972
this.angle = angle
73+
this.isStartMarker = isStartMarker
7074
}
7175
}

src/nodes/geometrynode.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getAttribute } from '../utils/node'
77
import { GraphicsNode } from './graphicsnode'
88
import { SvgNode } from './svgnode'
99
import { Rect } from '../utils/geometry'
10+
import { MarkerNode } from './marker'
1011

1112
export abstract class GeometryNode extends GraphicsNode {
1213
private readonly hasMarkers: boolean
@@ -167,7 +168,8 @@ export abstract class GeometryNode extends GraphicsNode {
167168
markerStart!,
168169
[prev.x, prev.y],
169170
// @ts-ignore
170-
getAngle(last ? [last.x, last.y] : [prev.x, prev.y], [curr.x1, curr.y1])
171+
getAngle(last ? [last.x, last.y] : [prev.x, prev.y], [curr.x1, curr.y1]),
172+
true
171173
)
172174
)
173175
hasEndMarker &&
@@ -194,7 +196,7 @@ export abstract class GeometryNode extends GraphicsNode {
194196
// @ts-ignore
195197
const angle = last ? getDirectionVector([last.x, last.y], [curr.x, curr.y]) : curAngle
196198
markers.addMarker(
197-
new Marker(markerStart!, [prev.x, prev.y], Math.atan2(angle[1], angle[0]))
199+
new Marker(markerStart!, [prev.x, prev.y], Math.atan2(angle[1], angle[0]), true)
198200
)
199201
}
200202
hasEndMarker &&
@@ -242,6 +244,29 @@ export abstract class GeometryNode extends GraphicsNode {
242244
}
243245
}
244246
}
247+
248+
markers.markers.forEach(marker => {
249+
const markerNode = context.refsHandler.get(marker.id) as MarkerNode
250+
251+
if (!markerNode) return
252+
253+
const orient: string | undefined = getAttribute(
254+
markerNode.element,
255+
context.styleSheets,
256+
'orient'
257+
)
258+
259+
if (orient == null) return
260+
261+
if (marker.isStartMarker && orient === 'auto-start-reverse') {
262+
marker.angle += Math.PI
263+
}
264+
265+
if (!isNaN(Number(orient))) {
266+
marker.angle = (parseFloat(orient) / 180) * Math.PI
267+
}
268+
})
269+
245270
return markers
246271
}
247272
}

src/nodes/gradient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export abstract class Gradient extends NonRenderedNode {
7777
const colorAttr = getAttribute(stop.element, styleSheets, 'color')
7878
const color = parseColor(
7979
getAttribute(stop.element, styleSheets, 'stop-color') || '',
80-
colorAttr ? parseColor(colorAttr, null) : (this.contextColor as RGBColor | null)
80+
colorAttr
81+
? { color: parseColor(colorAttr, null) }
82+
: { color: this.contextColor as RGBColor | null }
8183
)
8284
const opacity = parseFloat(getAttribute(stop.element, styleSheets, 'stop-opacity') || '1')
8385
stops.push({

src/nodes/marker.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { svgNodeAndChildrenVisible } from '../utils/node'
66
import { Rect } from '../utils/geometry'
77
import { Matrix } from 'jspdf'
88
import { applyContext } from '../applyparseattributes'
9+
import { AttributeState } from '../context/attributestate'
910

1011
export class MarkerNode extends NonRenderedNode {
1112
async apply(parentContext: Context): Promise<void> {
@@ -15,12 +16,14 @@ export class MarkerNode extends NonRenderedNode {
1516

1617
parentContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], tfMatrix)
1718

19+
const contextColors = AttributeState.getContextColors(parentContext)
1820
const childContext = new Context(parentContext.pdf, {
1921
refsHandler: parentContext.refsHandler,
2022
styleSheets: parentContext.styleSheets,
2123
viewport: parentContext.viewport,
2224
svg2pdfParameters: parentContext.svg2pdfParameters,
23-
textMeasure: parentContext.textMeasure
25+
textMeasure: parentContext.textMeasure,
26+
attributeState: Object.assign(AttributeState.default(), contextColors)
2427
})
2528

2629
// "Properties do not inherit from the element referencing the 'marker' into the contents of the
@@ -33,7 +36,9 @@ export class MarkerNode extends NonRenderedNode {
3336
for (const child of this.children) {
3437
await child.render(childContext)
3538
}
36-
parentContext.pdf.endFormObject(this.element.getAttribute('id'))
39+
parentContext.pdf.endFormObject(
40+
childContext.refsHandler.generateKey(this.element.getAttribute('id')!, contextColors)
41+
)
3742
}
3843

3944
// eslint-disable-next-line @typescript-eslint/no-unused-vars

src/nodes/use.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { parseFloats } from '../utils/parsing'
88
import { SvgNode } from './svgnode'
99
import { Symbol } from './symbol'
1010
import { Viewport } from '../context/viewport'
11-
import { RGBColor } from '../utils/rgbcolor'
11+
import { AttributeState } from '../context/attributestate'
1212

1313
/**
1414
* Draws the element referenced by a use node, makes use of pdf's XObjects/FormObjects so nodes are only written once
@@ -61,17 +61,19 @@ export class Use extends GraphicsNode {
6161
t = context.pdf.Matrix(1, 0, 0, 1, x, y)
6262
}
6363

64+
const contextColors = AttributeState.getContextColors(context, true)
6465
const refContext = new Context(context.pdf, {
6566
refsHandler: context.refsHandler,
6667
styleSheets: context.styleSheets,
6768
withinUse: true,
6869
viewport: refNodeOpensViewport ? new Viewport(width!, height!) : context.viewport,
6970
svg2pdfParameters: context.svg2pdfParameters,
70-
textMeasure: context.textMeasure
71+
textMeasure: context.textMeasure,
72+
attributeState: Object.assign(AttributeState.default(), contextColors)
7173
})
72-
const color = context.attributeState.color
73-
await context.refsHandler.getRendered(id, color, node =>
74-
Use.renderReferencedNode(node, id, color, refContext)
74+
75+
await context.refsHandler.getRendered(id, contextColors, node =>
76+
Use.renderReferencedNode(node, id, refContext)
7577
)
7678

7779
context.pdf.saveGraphicsState()
@@ -86,14 +88,13 @@ export class Use extends GraphicsNode {
8688
context.pdf.clip().discardPath()
8789
}
8890

89-
context.pdf.doFormObject(context.refsHandler.generateKey(id, color), t)
91+
context.pdf.doFormObject(context.refsHandler.generateKey(id, contextColors), t)
9092
context.pdf.restoreGraphicsState()
9193
}
9294

9395
private static async renderReferencedNode(
9496
node: SvgNode,
9597
id: string,
96-
color: RGBColor | null,
9798
refContext: Context
9899
): Promise<void> {
99100
let bBox = node.getBoundingBox(refContext)
@@ -104,15 +105,13 @@ export class Use extends GraphicsNode {
104105
// still within.
105106
bBox = [bBox[0] - 0.5 * bBox[2], bBox[1] - 0.5 * bBox[3], bBox[2] * 2, bBox[3] * 2]
106107

107-
// set the color to use for the referenced node
108-
refContext.attributeState.color = color
109108
refContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], refContext.pdf.unitMatrix)
110109
if (node instanceof Symbol) {
111110
await node.apply(refContext)
112111
} else {
113112
await node.render(refContext)
114113
}
115-
refContext.pdf.endFormObject(refContext.refsHandler.generateKey(id, color))
114+
refContext.pdf.endFormObject(refContext.refsHandler.generateKey(id, refContext.attributeState))
116115
}
117116

118117
protected getBoundingBoxCore(context: Context): number[] {

src/utils/parsing.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* parses a comma, sign and/or whitespace separated string of floats and returns
33
* the single floats in an array
44
*/
5+
import { ContextColors } from '../context/attributestate'
56
import { RGBColor } from './rgbcolor'
67

78
export function parseFloats(str: string): number[] {
@@ -18,15 +19,23 @@ export function parseFloats(str: string): number[] {
1819
* extends RGBColor by rgba colors as RGBColor is not capable of it
1920
* currentcolor: the color to return if colorString === 'currentcolor'
2021
*/
21-
export function parseColor(colorString: string, currentcolor: RGBColor | null): RGBColor {
22+
export function parseColor(colorString: string, contextColors: ContextColors | null): RGBColor {
2223
if (colorString === 'transparent') {
2324
const transparent = new RGBColor('rgb(0,0,0)')
2425
transparent.a = 0
2526
return transparent
2627
}
2728

28-
if (colorString.toLowerCase() === 'currentcolor') {
29-
return currentcolor || new RGBColor('rgb(0,0,0)')
29+
if (contextColors && colorString.toLowerCase() === 'currentcolor') {
30+
return contextColors.color || new RGBColor('rgb(0,0,0)')
31+
}
32+
33+
if (contextColors && colorString.toLowerCase() === 'context-stroke') {
34+
return contextColors.contextStroke || new RGBColor('rgb(0,0,0)')
35+
}
36+
37+
if (contextColors && colorString.toLowerCase() === 'context-fill') {
38+
return contextColors.contextFill || new RGBColor('rgb(0,0,0)')
3039
}
3140

3241
const match = /\s*rgba\(((?:[^,\)]*,){3}[^,\)]*)\)\s*/.exec(colorString)

0 commit comments

Comments
 (0)