File size: 3,630 Bytes
f56a29b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | import { SVGPathData } from 'svg-pathdata';
import arcToBezier from 'svg-arc-to-cubic-bezier';
import { createLogger } from '@/lib/logger';
const log = createLogger('SvgPathParser');
const typeMap = {
1: 'Z',
2: 'M',
4: 'H',
8: 'V',
16: 'L',
32: 'C',
64: 'S',
128: 'Q',
256: 'T',
512: 'A',
};
/**
* 简单解析SVG路径
* @param d SVG path d属性
*/
export const parseSvgPath = (d: string) => {
const pathData = new SVGPathData(d);
const ret = pathData.commands.map((item) => {
return { ...item, type: typeMap[item.type] };
});
return ret;
};
export type SvgPath = ReturnType<typeof parseSvgPath>;
/**
* 解析SVG路径,并将圆弧(A)类型的路径转为三次贝塞尔(C)类型的路径
* @param d SVG path d属性
*
* Returns an empty array if the path is malformed (e.g. unrecognised commands).
* Mirrors the defensive behaviour of {@link getSvgPathRange}: a single bad path
* (often produced by upstream LLM hallucinations) shouldn't take down the whole
* PPTX export.
*/
export const toPoints = (d: string) => {
let pathData: SVGPathData;
try {
pathData = new SVGPathData(d);
} catch (err) {
log.warn(`Failed to parse SVG path "${d}":`, err);
return [];
}
const points = [];
for (const item of pathData.commands) {
const type = typeMap[item.type];
if (item.type === 2 || item.type === 16) {
points.push({
x: item.x,
y: item.y,
relative: item.relative,
type,
});
}
if (item.type === 32) {
points.push({
x: item.x,
y: item.y,
curve: {
type: 'cubic',
x1: item.x1,
y1: item.y1,
x2: item.x2,
y2: item.y2,
},
relative: item.relative,
type,
});
} else if (item.type === 128) {
points.push({
x: item.x,
y: item.y,
curve: {
type: 'quadratic',
x1: item.x1,
y1: item.y1,
},
relative: item.relative,
type,
});
} else if (item.type === 512) {
const lastPoint = points[points.length - 1];
if (!['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue;
const cubicBezierPoints = arcToBezier({
px: lastPoint.x as number,
py: lastPoint.y as number,
cx: item.x,
cy: item.y,
rx: item.rX,
ry: item.rY,
xAxisRotation: item.xRot,
largeArcFlag: item.lArcFlag,
sweepFlag: item.sweepFlag,
});
for (const cbPoint of cubicBezierPoints) {
points.push({
x: cbPoint.x,
y: cbPoint.y,
curve: {
type: 'cubic',
x1: cbPoint.x1,
y1: cbPoint.y1,
x2: cbPoint.x2,
y2: cbPoint.y2,
},
relative: false,
type: 'C',
});
}
} else if (item.type === 1) {
points.push({ close: true, type });
} else continue;
}
return points;
};
export const getSvgPathRange = (path: string) => {
try {
const pathData = new SVGPathData(path);
const xList = [];
const yList = [];
for (const item of pathData.commands) {
const x = 'x' in item ? item.x : 0;
const y = 'y' in item ? item.y : 0;
xList.push(x);
yList.push(y);
}
return {
minX: Math.min(...xList),
minY: Math.min(...yList),
maxX: Math.max(...xList),
maxY: Math.max(...yList),
};
} catch {
return {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
};
}
};
export type SvgPoints = ReturnType<typeof toPoints>;
|