Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for marker "orient" attribute and the "context-fill" and "context-stroke" values #308

Merged
merged 6 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/applyparseattributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions src/context/attributestate.ts
Original file line number Diff line number Diff line change
@@ -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 = ''
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<Pick<AttributeState, 'color' | 'contextFill' | 'contextStroke'>>
17 changes: 12 additions & 5 deletions src/context/referenceshandler.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -16,10 +16,10 @@ export class ReferencesHandler {

public async getRendered(
id: string,
color: RGBColor | null,
contextColors: ContextColors | null,
renderCallback: (node: SvgNode) => Promise<void>
): Promise<SvgNode> {
const key = this.generateKey(id, color)
const key = this.generateKey(id, contextColors)
if (this.renderedElements.hasOwnProperty(key)) {
return this.renderedElements[id]
}
Expand All @@ -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
}
}
2 changes: 1 addition & 1 deletion src/fill/parseFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
10 changes: 7 additions & 3 deletions src/markerlist.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AttributeState } from './context/attributestate'
import { Context } from './context/context'
import { MarkerNode } from './nodes/marker'

Expand Down Expand Up @@ -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()
}
}
Expand All @@ -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
}
}
29 changes: 27 additions & 2 deletions src/nodes/geometrynode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand All @@ -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 &&
Expand Down Expand Up @@ -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
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/nodes/gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
9 changes: 7 additions & 2 deletions src/nodes/marker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 9 additions & 10 deletions src/nodes/use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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<void> {
let bBox = node.getBoundingBox(refContext)
Expand All @@ -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[] {
Expand Down
15 changes: 12 additions & 3 deletions src/utils/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand All @@ -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)
Expand Down
Binary file modified test/specs/markers/reference.pdf
Binary file not shown.
13 changes: 13 additions & 0 deletions test/specs/markers/spec.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/specs/svg-use/reference.pdf
Binary file not shown.
Loading
Loading