Skip to content

Commit

Permalink
Merge branch 'master' into group_a
Browse files Browse the repository at this point in the history
  • Loading branch information
HackbrettXXX authored Oct 16, 2024
2 parents f95939e + 0891c38 commit b575039
Show file tree
Hide file tree
Showing 16 changed files with 145 additions and 29 deletions.
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

0 comments on commit b575039

Please sign in to comment.