diff --git a/src/applyparseattributes.ts b/src/applyparseattributes.ts index c87773e..501cc72 100644 --- a/src/applyparseattributes.ts +++ b/src/applyparseattributes.ts @@ -19,7 +19,7 @@ export function parseAttributes(context: Context, svgNode: SvgNode, node?: Eleme // update color first so currentColor becomes available for this node const color = getAttribute(domNode, context.styleSheets, 'color') if (color) { - const fillColor = parseColor(color, context.attributeState.color) + const fillColor = parseColor(color, context.attributeState) if (fillColor.ok) { context.attributeState.color = fillColor } else { @@ -64,13 +64,21 @@ export function parseAttributes(context: Context, svgNode: SvgNode, node?: Eleme context.attributeState.stroke = null } else { // gradients, patterns not supported for strokes ... - const strokeRGB = parseColor(stroke, context.attributeState.color) + const strokeRGB = parseColor(stroke, context.attributeState) if (strokeRGB.ok) { context.attributeState.stroke = new ColorFill(strokeRGB) } } } + if (stroke && context.attributeState.stroke instanceof ColorFill) { + context.attributeState.contextStroke = context.attributeState.stroke.color + } + + if (fill && context.attributeState.fill instanceof ColorFill) { + context.attributeState.contextFill = context.attributeState.fill.color + } + const lineCap = getAttribute(domNode, context.styleSheets, 'stroke-linecap') if (lineCap) { context.attributeState.strokeLinecap = lineCap diff --git a/src/context/attributestate.ts b/src/context/attributestate.ts index fcca14c..63db323 100644 --- a/src/context/attributestate.ts +++ b/src/context/attributestate.ts @@ -1,6 +1,7 @@ import { RGBColor } from '../utils/rgbcolor' import { Fill } from '../fill/Fill' import { ColorFill } from '../fill/ColorFill' +import { Context } from './context' export class AttributeState { public xmlSpace = '' @@ -26,6 +27,8 @@ export class AttributeState { public textAnchor = '' public visibility = '' public color: RGBColor | null = null + public contextFill: RGBColor | null = null + public contextStroke: RGBColor | null = null public fillRule: string | null = null clone(): AttributeState { @@ -56,6 +59,9 @@ export class AttributeState { clone.color = this.color clone.fillRule = this.fillRule + clone.contextFill = this.contextFill + clone.contextStroke = this.contextStroke + return clone } @@ -87,6 +93,27 @@ export class AttributeState { attributeState.color = new RGBColor('rgb(0, 0, 0)') attributeState.fillRule = 'nonzero' + attributeState.contextFill = null + attributeState.contextStroke = null + return attributeState } + + static getContextColors(context: Context, includeCurrentColor = false): ContextColors { + const colors: ContextColors = {} + if (context.attributeState.contextFill) { + colors['contextFill'] = context.attributeState.contextFill + } + + if (context.attributeState.contextStroke) { + colors['contextStroke'] = context.attributeState.contextStroke + } + + if (includeCurrentColor && context.attributeState.color) { + colors['color'] = context.attributeState.color + } + return colors + } } + +export type ContextColors = Partial> diff --git a/src/context/referenceshandler.ts b/src/context/referenceshandler.ts index 7439844..afc459b 100644 --- a/src/context/referenceshandler.ts +++ b/src/context/referenceshandler.ts @@ -1,6 +1,6 @@ import cssEsc from 'cssesc' import { SvgNode } from '../nodes/svgnode' -import { RGBColor } from '../utils/rgbcolor' +import { ContextColors } from './attributestate' export class ReferencesHandler { private readonly renderedElements: { [key: string]: SvgNode } @@ -16,10 +16,10 @@ export class ReferencesHandler { public async getRendered( id: string, - color: RGBColor | null, + contextColors: ContextColors | null, renderCallback: (node: SvgNode) => Promise ): Promise { - const key = this.generateKey(id, color) + const key = this.generateKey(id, contextColors) if (this.renderedElements.hasOwnProperty(key)) { return this.renderedElements[id] } @@ -36,7 +36,14 @@ export class ReferencesHandler { return this.idMap[cssEsc(id, { isIdentifier: true })] } - public generateKey(id: string, color: RGBColor | null): string { - return this.idPrefix + '|' + id + '|' + (color || new RGBColor('rgb(0,0,0)')).toRGBA() + public generateKey(id: string, contextColors: ContextColors | null): string { + let colorHash = '' + const keys = ['color', 'contextFill', 'contextStroke'] as const + + if (contextColors) { + colorHash = keys.map(key => contextColors[key]?.toRGBA() ?? '').join('|') + } + + return this.idPrefix + '|' + id + '|' + colorHash } } diff --git a/src/fill/parseFill.ts b/src/fill/parseFill.ts index d58535e..6c1eba3 100644 --- a/src/fill/parseFill.ts +++ b/src/fill/parseFill.ts @@ -26,7 +26,7 @@ export function parseFill(fill: string, context: Context): Fill | null { } } else { // plain color - const fillColor = parseColor(fill, context.attributeState.color) + const fillColor = parseColor(fill, context.attributeState) if (fillColor.ok) { return new ColorFill(fillColor) } else if (fill === 'none') { diff --git a/src/markerlist.ts b/src/markerlist.ts index f1c7981..5774b90 100644 --- a/src/markerlist.ts +++ b/src/markerlist.ts @@ -1,3 +1,4 @@ +import { AttributeState } from './context/attributestate' import { Context } from './context/context' import { MarkerNode } from './nodes/marker' @@ -44,10 +45,11 @@ export class MarkerList { // as the marker is already scaled by the current line width we must not apply the line width twice! context.pdf.saveGraphicsState() - await context.refsHandler.getRendered(marker.id, null, node => + const contextColors = AttributeState.getContextColors(context) + await context.refsHandler.getRendered(marker.id, contextColors, node => (node as MarkerNode).apply(context) ) - context.pdf.doFormObject(marker.id, tf) + context.pdf.doFormObject(context.refsHandler.generateKey(marker.id, contextColors), tf) context.pdf.restoreGraphicsState() } } @@ -62,10 +64,12 @@ export class Marker { id: string anchor: number[] angle: number + isStartMarker: boolean - constructor(id: string, anchor: number[], angle: number) { + constructor(id: string, anchor: number[], angle: number, isStartMarker = false) { this.id = id this.anchor = anchor this.angle = angle + this.isStartMarker = isStartMarker } } diff --git a/src/nodes/geometrynode.ts b/src/nodes/geometrynode.ts index 5fd0c3d..bb4d49a 100644 --- a/src/nodes/geometrynode.ts +++ b/src/nodes/geometrynode.ts @@ -7,6 +7,7 @@ import { getAttribute } from '../utils/node' import { GraphicsNode } from './graphicsnode' import { SvgNode } from './svgnode' import { Rect } from '../utils/geometry' +import { MarkerNode } from './marker' export abstract class GeometryNode extends GraphicsNode { private readonly hasMarkers: boolean @@ -167,7 +168,8 @@ export abstract class GeometryNode extends GraphicsNode { markerStart!, [prev.x, prev.y], // @ts-ignore - getAngle(last ? [last.x, last.y] : [prev.x, prev.y], [curr.x1, curr.y1]) + getAngle(last ? [last.x, last.y] : [prev.x, prev.y], [curr.x1, curr.y1]), + true ) ) hasEndMarker && @@ -194,7 +196,7 @@ export abstract class GeometryNode extends GraphicsNode { // @ts-ignore const angle = last ? getDirectionVector([last.x, last.y], [curr.x, curr.y]) : curAngle markers.addMarker( - new Marker(markerStart!, [prev.x, prev.y], Math.atan2(angle[1], angle[0])) + new Marker(markerStart!, [prev.x, prev.y], Math.atan2(angle[1], angle[0]), true) ) } hasEndMarker && @@ -242,6 +244,29 @@ export abstract class GeometryNode extends GraphicsNode { } } } + + markers.markers.forEach(marker => { + const markerNode = context.refsHandler.get(marker.id) as MarkerNode + + if (!markerNode) return + + const orient: string | undefined = getAttribute( + markerNode.element, + context.styleSheets, + 'orient' + ) + + if (orient == null) return + + if (marker.isStartMarker && orient === 'auto-start-reverse') { + marker.angle += Math.PI + } + + if (!isNaN(Number(orient))) { + marker.angle = (parseFloat(orient) / 180) * Math.PI + } + }) + return markers } } diff --git a/src/nodes/gradient.ts b/src/nodes/gradient.ts index 659508d..6bcadb3 100644 --- a/src/nodes/gradient.ts +++ b/src/nodes/gradient.ts @@ -77,7 +77,9 @@ export abstract class Gradient extends NonRenderedNode { const colorAttr = getAttribute(stop.element, styleSheets, 'color') const color = parseColor( getAttribute(stop.element, styleSheets, 'stop-color') || '', - colorAttr ? parseColor(colorAttr, null) : (this.contextColor as RGBColor | null) + colorAttr + ? { color: parseColor(colorAttr, null) } + : { color: this.contextColor as RGBColor | null } ) const opacity = parseFloat(getAttribute(stop.element, styleSheets, 'stop-opacity') || '1') stops.push({ diff --git a/src/nodes/marker.ts b/src/nodes/marker.ts index 7acf808..130cb43 100644 --- a/src/nodes/marker.ts +++ b/src/nodes/marker.ts @@ -6,6 +6,7 @@ import { svgNodeAndChildrenVisible } from '../utils/node' import { Rect } from '../utils/geometry' import { Matrix } from 'jspdf' import { applyContext } from '../applyparseattributes' +import { AttributeState } from '../context/attributestate' export class MarkerNode extends NonRenderedNode { async apply(parentContext: Context): Promise { @@ -15,12 +16,14 @@ export class MarkerNode extends NonRenderedNode { parentContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], tfMatrix) + const contextColors = AttributeState.getContextColors(parentContext) const childContext = new Context(parentContext.pdf, { refsHandler: parentContext.refsHandler, styleSheets: parentContext.styleSheets, viewport: parentContext.viewport, svg2pdfParameters: parentContext.svg2pdfParameters, - textMeasure: parentContext.textMeasure + textMeasure: parentContext.textMeasure, + attributeState: Object.assign(AttributeState.default(), contextColors) }) // "Properties do not inherit from the element referencing the 'marker' into the contents of the @@ -33,7 +36,9 @@ export class MarkerNode extends NonRenderedNode { for (const child of this.children) { await child.render(childContext) } - parentContext.pdf.endFormObject(this.element.getAttribute('id')) + parentContext.pdf.endFormObject( + childContext.refsHandler.generateKey(this.element.getAttribute('id')!, contextColors) + ) } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/nodes/use.ts b/src/nodes/use.ts index 5bcaee8..36bc2a4 100644 --- a/src/nodes/use.ts +++ b/src/nodes/use.ts @@ -8,7 +8,7 @@ import { parseFloats } from '../utils/parsing' import { SvgNode } from './svgnode' import { Symbol } from './symbol' import { Viewport } from '../context/viewport' -import { RGBColor } from '../utils/rgbcolor' +import { AttributeState } from '../context/attributestate' /** * 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 { t = context.pdf.Matrix(1, 0, 0, 1, x, y) } + const contextColors = AttributeState.getContextColors(context, true) const refContext = new Context(context.pdf, { refsHandler: context.refsHandler, styleSheets: context.styleSheets, withinUse: true, viewport: refNodeOpensViewport ? new Viewport(width!, height!) : context.viewport, svg2pdfParameters: context.svg2pdfParameters, - textMeasure: context.textMeasure + textMeasure: context.textMeasure, + attributeState: Object.assign(AttributeState.default(), contextColors) }) - const color = context.attributeState.color - await context.refsHandler.getRendered(id, color, node => - Use.renderReferencedNode(node, id, color, refContext) + + await context.refsHandler.getRendered(id, contextColors, node => + Use.renderReferencedNode(node, id, refContext) ) context.pdf.saveGraphicsState() @@ -86,14 +88,13 @@ export class Use extends GraphicsNode { context.pdf.clip().discardPath() } - context.pdf.doFormObject(context.refsHandler.generateKey(id, color), t) + context.pdf.doFormObject(context.refsHandler.generateKey(id, contextColors), t) context.pdf.restoreGraphicsState() } private static async renderReferencedNode( node: SvgNode, id: string, - color: RGBColor | null, refContext: Context ): Promise { let bBox = node.getBoundingBox(refContext) @@ -104,15 +105,13 @@ export class Use extends GraphicsNode { // still within. bBox = [bBox[0] - 0.5 * bBox[2], bBox[1] - 0.5 * bBox[3], bBox[2] * 2, bBox[3] * 2] - // set the color to use for the referenced node - refContext.attributeState.color = color refContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], refContext.pdf.unitMatrix) if (node instanceof Symbol) { await node.apply(refContext) } else { await node.render(refContext) } - refContext.pdf.endFormObject(refContext.refsHandler.generateKey(id, color)) + refContext.pdf.endFormObject(refContext.refsHandler.generateKey(id, refContext.attributeState)) } protected getBoundingBoxCore(context: Context): number[] { diff --git a/src/utils/parsing.ts b/src/utils/parsing.ts index 3077f61..95ccccc 100644 --- a/src/utils/parsing.ts +++ b/src/utils/parsing.ts @@ -2,6 +2,7 @@ * parses a comma, sign and/or whitespace separated string of floats and returns * the single floats in an array */ +import { ContextColors } from '../context/attributestate' import { RGBColor } from './rgbcolor' export function parseFloats(str: string): number[] { @@ -18,15 +19,23 @@ export function parseFloats(str: string): number[] { * extends RGBColor by rgba colors as RGBColor is not capable of it * currentcolor: the color to return if colorString === 'currentcolor' */ -export function parseColor(colorString: string, currentcolor: RGBColor | null): RGBColor { +export function parseColor(colorString: string, contextColors: ContextColors | null): RGBColor { if (colorString === 'transparent') { const transparent = new RGBColor('rgb(0,0,0)') transparent.a = 0 return transparent } - if (colorString.toLowerCase() === 'currentcolor') { - return currentcolor || new RGBColor('rgb(0,0,0)') + if (contextColors && colorString.toLowerCase() === 'currentcolor') { + return contextColors.color || new RGBColor('rgb(0,0,0)') + } + + if (contextColors && colorString.toLowerCase() === 'context-stroke') { + return contextColors.contextStroke || new RGBColor('rgb(0,0,0)') + } + + if (contextColors && colorString.toLowerCase() === 'context-fill') { + return contextColors.contextFill || new RGBColor('rgb(0,0,0)') } const match = /\s*rgba\(((?:[^,\)]*,){3}[^,\)]*)\)\s*/.exec(colorString) diff --git a/test/specs/markers/reference.pdf b/test/specs/markers/reference.pdf index 770c790..df95005 100644 Binary files a/test/specs/markers/reference.pdf and b/test/specs/markers/reference.pdf differ diff --git a/test/specs/markers/spec.svg b/test/specs/markers/spec.svg index 139cc41..4cc640b 100644 --- a/test/specs/markers/spec.svg +++ b/test/specs/markers/spec.svg @@ -11,6 +11,15 @@ + + + + + + + + + @@ -38,4 +47,8 @@ + + + + diff --git a/test/specs/svg-use/reference.pdf b/test/specs/svg-use/reference.pdf index b60e7db..dda3a34 100644 Binary files a/test/specs/svg-use/reference.pdf and b/test/specs/svg-use/reference.pdf differ diff --git a/test/specs/svg-use/spec.svg b/test/specs/svg-use/spec.svg index c0ff4e8..8c74199 100644 --- a/test/specs/svg-use/spec.svg +++ b/test/specs/svg-use/spec.svg @@ -103,6 +103,10 @@ + + + + @@ -185,4 +189,9 @@ + + + + + diff --git a/test/specs/symbols/reference.pdf b/test/specs/symbols/reference.pdf index c011887..979c20d 100644 Binary files a/test/specs/symbols/reference.pdf and b/test/specs/symbols/reference.pdf differ diff --git a/test/specs/symbols/spec.svg b/test/specs/symbols/spec.svg index e6f8fae..d10c955 100644 --- a/test/specs/symbols/spec.svg +++ b/test/specs/symbols/spec.svg @@ -95,6 +95,10 @@ + + + + @@ -159,4 +163,8 @@ + + + +