UNPKG

@amandaghassaei/flat-svg

Version:

A TypeScript library for converting nested SVGs into a flat list of elements, paths, or segments and applying style-based filters.

349 lines (323 loc) 10.7 kB
import { POLYGON, POLYLINE } from './constants'; import { applyTransform } from './transforms'; import { CircleProperties, EllipseProperties, LineProperties, PathParser, PathProperties, PolygonProperties, PolylineProperties, RectProperties, Transform, } from './types'; import { removeWhitespacePadding } from './utils'; import svgpath from 'svgpath'; import { isNonNegativeNumber, isNumber, isString } from '@amandaghassaei/type-checks'; /* Export any geometry object as path in Abs coordinates with only L, H, V, B, and C types. */ const temp = [0, 0] as [number, number]; export function convertLineToPath( properties: LineProperties, parsingErrors: string[], transform?: Transform ) { let { x1, x2, y1, y2 } = properties; // x1, x2, y1, y2 default to 0. /* c8 ignore next if */ if (x1 === undefined) x1 = 0; if (x2 === undefined) x2 = 0; if (y1 === undefined) y1 = 0; if (y2 === undefined) y2 = 0; if (!isNumber(x1) || !isNumber(x2) || !isNumber(y1) || !isNumber(y2)) { parsingErrors.push(`Invalid <line> properties: ${JSON.stringify({ x1, y1, x2, y2 })}.`); return; } if (transform) { temp[0] = x1; temp[1] = y1; [x1, y1] = applyTransform(temp, transform); temp[0] = x2; temp[1] = y2; [x2, y2] = applyTransform(temp, transform); } return `M${x1},${y1} L${x2},${y2}`; } export function convertRectToPath( properties: RectProperties, parsingErrors: string[], transform?: Transform ) { let { x, y } = properties; // x and y default to 0. if (x === undefined) x = 0; if (y === undefined) y = 0; const { width, height } = properties; if ( !isNumber(x) || !isNumber(y) || !isNonNegativeNumber(width) || !isNonNegativeNumber(height) ) { parsingErrors.push( `Invalid <rect> properties: ${JSON.stringify({ x, y, width, height })}.` ); return; } let x1 = x; let y1 = y; let x2 = x + width; let y2 = y; let x3 = x + width; let y3 = y + height; let x4 = x; let y4 = y + height; if (transform) { temp[0] = x1; temp[1] = y1; [x1, y1] = applyTransform(temp, transform); temp[0] = x2; temp[1] = y2; [x2, y2] = applyTransform(temp, transform); temp[0] = x3; temp[1] = y3; [x3, y3] = applyTransform(temp, transform); temp[0] = x4; temp[1] = y4; [x4, y4] = applyTransform(temp, transform); } return `M${x1},${y1} L${x2},${y2} L${x3},${y3} L${x4},${y4} z`; } export function convertCircleToPath( properties: CircleProperties, parsingErrors: string[], _preserveArcs: boolean, transform?: Transform ) { let { cx, cy, r } = properties; // cx, cy, r default to 0. if (cx === undefined) cx = 0; /* c8 ignore next if */ if (cy === undefined) cy = 0; if (r === undefined) r = 0; if (!isNumber(cx) || !isNumber(cy) || !isNonNegativeNumber(r)) { parsingErrors.push(`Invalid <circle> properties: ${JSON.stringify({ cx, cy, r })}.`); return; } const pathParser = _convertEllipseToPath(cx, cy, r, r, _preserveArcs, transform); /* c8 ignore next 7 */ if (pathParser.err) { // Should not hit this. parsingErrors.push( `Problem parsing <circle> ${JSON.stringify({ cx, cy, r })} with ${pathParser.err}.` ); return; } return pathParser; } export function convertEllipseToPath( properties: EllipseProperties, parsingErrors: string[], _preserveArcs: boolean, transform?: Transform ) { let { cx, cy, rx, ry } = properties; // cx, cy, rx, ry default to 0. /* c8 ignore next if */ if (cx === undefined) cx = 0; if (cy === undefined) cy = 0; if (rx === undefined) rx = 0; if (ry === undefined) ry = 0; if (!isNumber(cx) || !isNumber(cy) || !isNonNegativeNumber(rx) || !isNonNegativeNumber(ry)) { parsingErrors.push(`Invalid <ellipse> properties: ${JSON.stringify({ cx, cy, rx, ry })}.`); return; } const pathParser = _convertEllipseToPath(cx, cy, rx, ry, _preserveArcs, transform); /* c8 ignore next 9 */ if (pathParser.err) { // Should not hit this. parsingErrors.push( `Problem parsing <ellipse> ${JSON.stringify({ cx, cy, rx, ry })} with ${ pathParser.err }.` ); return; } return pathParser; } // https://stackoverflow.com/questions/59011294/ellipse-to-path-convertion-using-javascript // const ellipsePoints = new Array(24).fill(0); function _convertEllipseToPath( cx: number, cy: number, rx: number, ry: number, _preserveArcs: boolean, transform?: Transform ) { // Convert ellipse to 2 arcs. const d = `M${cx - rx},${cy} a${rx},${ry} 0 1,0 ${rx * 2},0 a ${rx},${ry} 0 1,0 -${rx * 2},0`; let pathParser = svgpath(d).abs() as any as PathParser; // Convert arcs to bezier is _preserveArcs == false. if (!_preserveArcs) pathParser = pathParser.unarc(); // Apply transform. if (transform) pathParser = pathParser.matrix([ transform.a, transform.b, transform.c, transform.d, transform.e, transform.f, ]); return pathParser; // const kappa = 0.5522847498; // const ox = rx * kappa; // x offset for the control point // const oy = ry * kappa; // y offset for the control point // ellipsePoints[0] = cx - rx; // ellipsePoints[1] = cy; // ellipsePoints[2] = cx - rx; // ellipsePoints[3] = cy - oy; // ellipsePoints[4] = cx - ox; // ellipsePoints[5] = cy - ry; // ellipsePoints[6] = cx; // ellipsePoints[7] = cy - ry; // ellipsePoints[8] = cx + ox; // ellipsePoints[9] = cy - ry; // ellipsePoints[10] = cx + rx; // ellipsePoints[11] = cy - oy; // ellipsePoints[12] = cx + rx; // ellipsePoints[13] = cy; // ellipsePoints[14] = cx + rx; // ellipsePoints[15] = cy + oy; // ellipsePoints[16] = cx + ox; // ellipsePoints[17] = cy + ry; // ellipsePoints[18] = cx; // ellipsePoints[19] = cy + ry; // ellipsePoints[20] = cx - ox; // ellipsePoints[21] = cy + ry; // ellipsePoints[22] = cx - rx; // ellipsePoints[23] = cy + oy; // if (transform) { // for (let i = 0, length = ellipsePoints.length / 2; i < length; i++) { // temp[0] = ellipsePoints[2 * i]; // temp[1] = ellipsePoints[2 * i + 1]; // applyTransform(temp, transform); // ellipsePoints[2 * i] = temp[0]; // ellipsePoints[2 * i + 1] = temp[1]; // } // } // return `M${ellipsePoints[0]},${ellipsePoints[1]} \ // C${ellipsePoints[2]},${ellipsePoints[3]} ${ellipsePoints[4]},${ellipsePoints[5]} ${ellipsePoints[6]},${ellipsePoints[7]} \ // C${ellipsePoints[8]},${ellipsePoints[9]} ${ellipsePoints[10]},${ellipsePoints[11]} ${ellipsePoints[12]},${ellipsePoints[13]} \ // C${ellipsePoints[14]},${ellipsePoints[15]} ${ellipsePoints[16]},${ellipsePoints[17]} ${ellipsePoints[18]},${ellipsePoints[19]} \ // C${ellipsePoints[20]},${ellipsePoints[21]} ${ellipsePoints[22]},${ellipsePoints[23]} ${ellipsePoints[0]},${ellipsePoints[1]} \ // z`; } export function convertPolygonToPath( properties: PolygonProperties, parsingErrors: string[], transform?: Transform ) { const { points } = properties; if (!isString(points)) { parsingErrors.push(`Invalid <polygon> properties: ${JSON.stringify({ points })}.`); return; } const path = _convertPointsToPath(points, parsingErrors, POLYGON, transform); if (!path) return path; return path + ' z'; } export function convertPolylineToPath( properties: PolylineProperties, parsingErrors: string[], transform?: Transform ) { const { points } = properties; if (!isString(points)) { parsingErrors.push(`Invalid <polyline> properties: ${JSON.stringify({ points })}.`); return; } return _convertPointsToPath(points, parsingErrors, POLYLINE, transform); } function _convertPointsToPath( pointsString: string, parsingErrors: string[], elementType: typeof POLYGON | typeof POLYLINE, transform?: Transform ) { const points = removeWhitespacePadding(pointsString).split(' '); let d = ''; while (points.length) { const point = points.shift()!.split(','); if (point.length === 1) { // Sometimes polyline is not separated by commas, only by whitespace. if (points.length && points.length % 2 === 1) { point.push(points.shift()!); // Get next element in points array. } } if (point.length !== 2) { parsingErrors.push( `Unable to parse points string: "${pointsString}" in <${elementType}>.` ); return; } let x = parseFloat(point[0]); let y = parseFloat(point[1]); if (isNaN(x) || isNaN(y)) { parsingErrors.push( `Unable to parse points string: "${pointsString}" in <${elementType}>.` ); return; } if (transform) { temp[0] = x; temp[1] = y; [x, y] = applyTransform(temp, transform); } if (d === '') { d += `M${x},${y}`; } else { d += ` L${x},${y}`; } } return d; } export function convertPathToPath( properties: PathProperties, parsingErrors: string[], _preserveArcs: boolean, transform?: Transform ) { const { d } = properties; if (!isString(d)) { parsingErrors.push(`Invalid <path> properties: ${JSON.stringify({ d })}.`); return; } // Convert to absolute coordinates, // Convert smooth curves (T/S) to regular Bezier (Q/C). let pathParser = svgpath(d).abs().unshort() as any as PathParser; if (_preserveArcs) { // Convert arcs to bezier. pathParser = pathParser.unarc(); } // Apply transform. if (transform) { pathParser = pathParser.matrix([ transform.a, transform.b, transform.c, transform.d, transform.e, transform.f, ]); } if (pathParser.err) { parsingErrors.push( `Problem parsing <path> ${JSON.stringify({ d })} with ${pathParser.err}.` ); return; } return pathParser; }