151 lines
4.0 KiB
TypeScript
151 lines
4.0 KiB
TypeScript
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];
|
||
// An arc may appear before any anchor point (e.g. a path that starts with
|
||
// "A", or one whose leading commands push no point). Without `lastPoint`
|
||
// there is nothing to arc from, so skip it instead of throwing — this keeps
|
||
// the documented "malformed path returns []" contract.
|
||
if (!lastPoint || !['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;
|
||
};
|
||
|
||
const ZERO_RANGE = { minX: 0, minY: 0, maxX: 0, maxY: 0 } as const;
|
||
|
||
export const getSvgPathRange = (path: string) => {
|
||
try {
|
||
// Delegate to svg-pathdata's bounds, which correctly account for relative
|
||
// commands, H/V/Z (which have no x/y), and arc bulge. The previous hand-rolled
|
||
// version defaulted missing coordinates to 0 — injecting a spurious (0,0) for
|
||
// Z, treating relative deltas as absolute, and ignoring arc extent.
|
||
const { minX, minY, maxX, maxY } = new SVGPathData(path).getBounds();
|
||
if ([minX, minY, maxX, maxY].some((v) => v == null || !Number.isFinite(v))) {
|
||
return { ...ZERO_RANGE };
|
||
}
|
||
return { minX, minY, maxX, maxY };
|
||
} catch {
|
||
return { ...ZERO_RANGE };
|
||
}
|
||
};
|
||
|
||
export type SvgPoints = ReturnType<typeof toPoints>;
|