@libpdf/core 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/index.d.mts +182 -14
- package/dist/index.mjs +1167 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { createCMSECDSASignature } from "pkijs";
|
|
|
9
9
|
import { base64 } from "@scure/base";
|
|
10
10
|
|
|
11
11
|
//#region package.json
|
|
12
|
-
var version = "0.2.
|
|
12
|
+
var version = "0.2.4";
|
|
13
13
|
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/objects/pdf-array.ts
|
|
@@ -13790,19 +13790,6 @@ var ZapfDingbatsEncoding = class ZapfDingbatsEncoding extends SimpleEncoding {
|
|
|
13790
13790
|
//#endregion
|
|
13791
13791
|
//#region src/fonts/standard-14.ts
|
|
13792
13792
|
/**
|
|
13793
|
-
* Standard 14 PDF Fonts
|
|
13794
|
-
*
|
|
13795
|
-
* These are the 14 fonts that every PDF reader must support.
|
|
13796
|
-
* They don't require embedding - the reader provides them.
|
|
13797
|
-
*
|
|
13798
|
-
* Width tables are stored by glyph name (like pdf.js) which works
|
|
13799
|
-
* with any encoding. Use getWidthByGlyphName() for lookup.
|
|
13800
|
-
*
|
|
13801
|
-
* Widths are in glyph units (1000 units = 1 em).
|
|
13802
|
-
*
|
|
13803
|
-
* Data extracted from pdf.js metrics.js (Mozilla, Apache 2.0 License)
|
|
13804
|
-
*/
|
|
13805
|
-
/**
|
|
13806
13793
|
* Standard 14 font names.
|
|
13807
13794
|
*/
|
|
13808
13795
|
const STANDARD_14_FONTS = [
|
|
@@ -13861,6 +13848,27 @@ function getBaseFontName(name) {
|
|
|
13861
13848
|
return name.includes("+") ? name.split("+")[1] : name;
|
|
13862
13849
|
}
|
|
13863
13850
|
/**
|
|
13851
|
+
* Get the font encoding for a Standard 14 font.
|
|
13852
|
+
*
|
|
13853
|
+
* - Symbol → SymbolEncoding
|
|
13854
|
+
* - ZapfDingbats → ZapfDingbatsEncoding
|
|
13855
|
+
* - All others (Helvetica, Times, Courier) → WinAnsiEncoding
|
|
13856
|
+
*/
|
|
13857
|
+
function getEncodingForStandard14(name) {
|
|
13858
|
+
const baseName = getBaseFontName(name);
|
|
13859
|
+
if (baseName === "Symbol") return SymbolEncoding.instance;
|
|
13860
|
+
if (baseName === "ZapfDingbats") return ZapfDingbatsEncoding.instance;
|
|
13861
|
+
return WinAnsiEncoding.instance;
|
|
13862
|
+
}
|
|
13863
|
+
/**
|
|
13864
|
+
* Check if a Standard 14 font uses WinAnsiEncoding.
|
|
13865
|
+
* Returns false for Symbol and ZapfDingbats (they use built-in encodings).
|
|
13866
|
+
*/
|
|
13867
|
+
function isWinAnsiStandard14(name) {
|
|
13868
|
+
const baseName = getBaseFontName(name);
|
|
13869
|
+
return baseName !== "Symbol" && baseName !== "ZapfDingbats";
|
|
13870
|
+
}
|
|
13871
|
+
/**
|
|
13864
13872
|
* Get basic metrics (ascent, descent, etc.) for a Standard 14 font.
|
|
13865
13873
|
*/
|
|
13866
13874
|
function getStandard14BasicMetrics(name) {
|
|
@@ -13987,7 +13995,130 @@ const CHAR_TO_GLYPH = {
|
|
|
13987
13995
|
123: "braceleft",
|
|
13988
13996
|
124: "bar",
|
|
13989
13997
|
125: "braceright",
|
|
13990
|
-
126: "asciitilde"
|
|
13998
|
+
126: "asciitilde",
|
|
13999
|
+
8364: "Euro",
|
|
14000
|
+
8218: "quotesinglbase",
|
|
14001
|
+
402: "florin",
|
|
14002
|
+
8222: "quotedblbase",
|
|
14003
|
+
8230: "ellipsis",
|
|
14004
|
+
8224: "dagger",
|
|
14005
|
+
8225: "daggerdbl",
|
|
14006
|
+
710: "circumflex",
|
|
14007
|
+
8240: "perthousand",
|
|
14008
|
+
352: "Scaron",
|
|
14009
|
+
8249: "guilsinglleft",
|
|
14010
|
+
338: "OE",
|
|
14011
|
+
381: "Zcaron",
|
|
14012
|
+
8216: "quoteleft",
|
|
14013
|
+
8217: "quoteright",
|
|
14014
|
+
8220: "quotedblleft",
|
|
14015
|
+
8221: "quotedblright",
|
|
14016
|
+
8226: "bullet",
|
|
14017
|
+
8211: "endash",
|
|
14018
|
+
8212: "emdash",
|
|
14019
|
+
732: "tilde",
|
|
14020
|
+
8482: "trademark",
|
|
14021
|
+
353: "scaron",
|
|
14022
|
+
8250: "guilsinglright",
|
|
14023
|
+
339: "oe",
|
|
14024
|
+
382: "zcaron",
|
|
14025
|
+
376: "Ydieresis",
|
|
14026
|
+
160: "space",
|
|
14027
|
+
161: "exclamdown",
|
|
14028
|
+
162: "cent",
|
|
14029
|
+
163: "sterling",
|
|
14030
|
+
164: "currency",
|
|
14031
|
+
165: "yen",
|
|
14032
|
+
166: "brokenbar",
|
|
14033
|
+
167: "section",
|
|
14034
|
+
168: "dieresis",
|
|
14035
|
+
169: "copyright",
|
|
14036
|
+
170: "ordfeminine",
|
|
14037
|
+
171: "guillemotleft",
|
|
14038
|
+
172: "logicalnot",
|
|
14039
|
+
173: "hyphen",
|
|
14040
|
+
174: "registered",
|
|
14041
|
+
175: "macron",
|
|
14042
|
+
176: "degree",
|
|
14043
|
+
177: "plusminus",
|
|
14044
|
+
178: "twosuperior",
|
|
14045
|
+
179: "threesuperior",
|
|
14046
|
+
180: "acute",
|
|
14047
|
+
181: "mu",
|
|
14048
|
+
182: "paragraph",
|
|
14049
|
+
183: "periodcentered",
|
|
14050
|
+
184: "cedilla",
|
|
14051
|
+
185: "onesuperior",
|
|
14052
|
+
186: "ordmasculine",
|
|
14053
|
+
187: "guillemotright",
|
|
14054
|
+
188: "onequarter",
|
|
14055
|
+
189: "onehalf",
|
|
14056
|
+
190: "threequarters",
|
|
14057
|
+
191: "questiondown",
|
|
14058
|
+
192: "Agrave",
|
|
14059
|
+
193: "Aacute",
|
|
14060
|
+
194: "Acircumflex",
|
|
14061
|
+
195: "Atilde",
|
|
14062
|
+
196: "Adieresis",
|
|
14063
|
+
197: "Aring",
|
|
14064
|
+
198: "AE",
|
|
14065
|
+
199: "Ccedilla",
|
|
14066
|
+
200: "Egrave",
|
|
14067
|
+
201: "Eacute",
|
|
14068
|
+
202: "Ecircumflex",
|
|
14069
|
+
203: "Edieresis",
|
|
14070
|
+
204: "Igrave",
|
|
14071
|
+
205: "Iacute",
|
|
14072
|
+
206: "Icircumflex",
|
|
14073
|
+
207: "Idieresis",
|
|
14074
|
+
208: "Eth",
|
|
14075
|
+
209: "Ntilde",
|
|
14076
|
+
210: "Ograve",
|
|
14077
|
+
211: "Oacute",
|
|
14078
|
+
212: "Ocircumflex",
|
|
14079
|
+
213: "Otilde",
|
|
14080
|
+
214: "Odieresis",
|
|
14081
|
+
215: "multiply",
|
|
14082
|
+
216: "Oslash",
|
|
14083
|
+
217: "Ugrave",
|
|
14084
|
+
218: "Uacute",
|
|
14085
|
+
219: "Ucircumflex",
|
|
14086
|
+
220: "Udieresis",
|
|
14087
|
+
221: "Yacute",
|
|
14088
|
+
222: "Thorn",
|
|
14089
|
+
223: "germandbls",
|
|
14090
|
+
224: "agrave",
|
|
14091
|
+
225: "aacute",
|
|
14092
|
+
226: "acircumflex",
|
|
14093
|
+
227: "atilde",
|
|
14094
|
+
228: "adieresis",
|
|
14095
|
+
229: "aring",
|
|
14096
|
+
230: "ae",
|
|
14097
|
+
231: "ccedilla",
|
|
14098
|
+
232: "egrave",
|
|
14099
|
+
233: "eacute",
|
|
14100
|
+
234: "ecircumflex",
|
|
14101
|
+
235: "edieresis",
|
|
14102
|
+
236: "igrave",
|
|
14103
|
+
237: "iacute",
|
|
14104
|
+
238: "icircumflex",
|
|
14105
|
+
239: "idieresis",
|
|
14106
|
+
240: "eth",
|
|
14107
|
+
241: "ntilde",
|
|
14108
|
+
242: "ograve",
|
|
14109
|
+
243: "oacute",
|
|
14110
|
+
244: "ocircumflex",
|
|
14111
|
+
245: "otilde",
|
|
14112
|
+
246: "odieresis",
|
|
14113
|
+
247: "divide",
|
|
14114
|
+
248: "oslash",
|
|
14115
|
+
249: "ugrave",
|
|
14116
|
+
250: "uacute",
|
|
14117
|
+
251: "ucircumflex",
|
|
14118
|
+
252: "udieresis",
|
|
14119
|
+
253: "yacute",
|
|
14120
|
+
254: "thorn",
|
|
14121
|
+
255: "ydieresis"
|
|
13991
14122
|
};
|
|
13992
14123
|
/**
|
|
13993
14124
|
* Get glyph name for a character (for Standard 14 font width lookup).
|
|
@@ -22717,7 +22848,7 @@ function validateFollowingOperator(scanner) {
|
|
|
22717
22848
|
const tokenStart = pos + i;
|
|
22718
22849
|
const firstCh = scanner.peekAt(tokenStart);
|
|
22719
22850
|
if (firstCh === -1) return false;
|
|
22720
|
-
if (isNumberStart(firstCh)) {
|
|
22851
|
+
if (isNumberStart$1(firstCh)) {
|
|
22721
22852
|
i++;
|
|
22722
22853
|
while (i < lookahead) {
|
|
22723
22854
|
const ch = scanner.peekAt(pos + i);
|
|
@@ -22772,7 +22903,7 @@ function validateFollowingOperator(scanner) {
|
|
|
22772
22903
|
}
|
|
22773
22904
|
return false;
|
|
22774
22905
|
}
|
|
22775
|
-
function isNumberStart(ch) {
|
|
22906
|
+
function isNumberStart$1(ch) {
|
|
22776
22907
|
return ch >= DIGIT_0 && ch <= DIGIT_9 || ch === CHAR_PLUS || ch === CHAR_MINUS || ch === CHAR_PERIOD;
|
|
22777
22908
|
}
|
|
22778
22909
|
/**
|
|
@@ -23722,6 +23853,854 @@ function getPaintOp(hasFill, hasStroke) {
|
|
|
23722
23853
|
return stroke();
|
|
23723
23854
|
}
|
|
23724
23855
|
|
|
23856
|
+
//#endregion
|
|
23857
|
+
//#region src/svg/arc-to-bezier.ts
|
|
23858
|
+
/**
|
|
23859
|
+
* Convert an SVG arc to one or more cubic bezier curves.
|
|
23860
|
+
*
|
|
23861
|
+
* Handles edge cases according to SVG spec:
|
|
23862
|
+
* - If rx=0 or ry=0, returns a line to the endpoint
|
|
23863
|
+
* - If start and end points are the same, returns empty array
|
|
23864
|
+
* - If radii are too small, they're scaled up automatically
|
|
23865
|
+
*
|
|
23866
|
+
* @param arc - Arc parameters in endpoint form
|
|
23867
|
+
* @returns Array of bezier curves that approximate the arc
|
|
23868
|
+
*/
|
|
23869
|
+
function arcToBezier(arc) {
|
|
23870
|
+
const { x1, y1, x2, y2, xAxisRotation, largeArcFlag, sweepFlag } = arc;
|
|
23871
|
+
let { rx, ry } = arc;
|
|
23872
|
+
if (x1 === x2 && y1 === y2) return [];
|
|
23873
|
+
if (rx === 0 || ry === 0) return [{
|
|
23874
|
+
cp1x: x1,
|
|
23875
|
+
cp1y: y1,
|
|
23876
|
+
cp2x: x2,
|
|
23877
|
+
cp2y: y2,
|
|
23878
|
+
x: x2,
|
|
23879
|
+
y: y2
|
|
23880
|
+
}];
|
|
23881
|
+
rx = Math.abs(rx);
|
|
23882
|
+
ry = Math.abs(ry);
|
|
23883
|
+
const phi = xAxisRotation * Math.PI / 180;
|
|
23884
|
+
const cosPhi = Math.cos(phi);
|
|
23885
|
+
const sinPhi = Math.sin(phi);
|
|
23886
|
+
const dx = (x1 - x2) / 2;
|
|
23887
|
+
const dy = (y1 - y2) / 2;
|
|
23888
|
+
const x1p = cosPhi * dx + sinPhi * dy;
|
|
23889
|
+
const y1p = -sinPhi * dx + cosPhi * dy;
|
|
23890
|
+
const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
23891
|
+
if (lambda > 1) {
|
|
23892
|
+
const sqrtLambda = Math.sqrt(lambda);
|
|
23893
|
+
rx = sqrtLambda * rx;
|
|
23894
|
+
ry = sqrtLambda * ry;
|
|
23895
|
+
}
|
|
23896
|
+
const rx2 = rx * rx;
|
|
23897
|
+
const ry2 = ry * ry;
|
|
23898
|
+
const x1p2 = x1p * x1p;
|
|
23899
|
+
const y1p2 = y1p * y1p;
|
|
23900
|
+
let sq = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2) / (rx2 * y1p2 + ry2 * x1p2);
|
|
23901
|
+
if (sq < 0) sq = 0;
|
|
23902
|
+
const coef = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(sq);
|
|
23903
|
+
const cxp = coef * rx * y1p / ry;
|
|
23904
|
+
const cyp = -coef * ry * x1p / rx;
|
|
23905
|
+
const midX = (x1 + x2) / 2;
|
|
23906
|
+
const midY = (y1 + y2) / 2;
|
|
23907
|
+
const cx = cosPhi * cxp - sinPhi * cyp + midX;
|
|
23908
|
+
const cy = sinPhi * cxp + cosPhi * cyp + midY;
|
|
23909
|
+
const ux = (x1p - cxp) / rx;
|
|
23910
|
+
const uy = (y1p - cyp) / ry;
|
|
23911
|
+
const vx = (-x1p - cxp) / rx;
|
|
23912
|
+
const vy = (-y1p - cyp) / ry;
|
|
23913
|
+
const theta1 = angleBetween(1, 0, ux, uy);
|
|
23914
|
+
let dTheta = angleBetween(ux, uy, vx, vy);
|
|
23915
|
+
if (!sweepFlag && dTheta > 0) dTheta -= 2 * Math.PI;
|
|
23916
|
+
if (sweepFlag && dTheta < 0) dTheta += 2 * Math.PI;
|
|
23917
|
+
const numSegments = Math.ceil(Math.abs(dTheta) / (Math.PI / 2));
|
|
23918
|
+
const segmentAngle = dTheta / numSegments;
|
|
23919
|
+
const curves = [];
|
|
23920
|
+
let currentAngle = theta1;
|
|
23921
|
+
for (let i = 0; i < numSegments; i++) {
|
|
23922
|
+
const nextAngle = currentAngle + segmentAngle;
|
|
23923
|
+
const curve = arcSegmentToBezier(cx, cy, rx, ry, phi, currentAngle, nextAngle);
|
|
23924
|
+
curves.push(curve);
|
|
23925
|
+
currentAngle = nextAngle;
|
|
23926
|
+
}
|
|
23927
|
+
return curves;
|
|
23928
|
+
}
|
|
23929
|
+
/**
|
|
23930
|
+
* Compute the angle between two vectors.
|
|
23931
|
+
*/
|
|
23932
|
+
function angleBetween(ux, uy, vx, vy) {
|
|
23933
|
+
const sign = ux * vy - uy * vx < 0 ? -1 : 1;
|
|
23934
|
+
let cos = (ux * vx + uy * vy) / (Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy));
|
|
23935
|
+
if (cos < -1) cos = -1;
|
|
23936
|
+
if (cos > 1) cos = 1;
|
|
23937
|
+
return sign * Math.acos(cos);
|
|
23938
|
+
}
|
|
23939
|
+
/**
|
|
23940
|
+
* Convert a single arc segment (up to 90 degrees) to a cubic bezier.
|
|
23941
|
+
*
|
|
23942
|
+
* Uses the standard arc approximation formula:
|
|
23943
|
+
* For an arc of angle theta centered at origin:
|
|
23944
|
+
* CP1 = P0 + alpha * tangent at P0
|
|
23945
|
+
* CP2 = P1 - alpha * tangent at P1
|
|
23946
|
+
*
|
|
23947
|
+
* Where alpha = (4/3) * tan(theta/4)
|
|
23948
|
+
*/
|
|
23949
|
+
function arcSegmentToBezier(cx, cy, rx, ry, phi, theta1, theta2) {
|
|
23950
|
+
const dTheta = theta2 - theta1;
|
|
23951
|
+
const t = Math.tan(dTheta / 4);
|
|
23952
|
+
const alpha = Math.sin(dTheta) * (Math.sqrt(4 + 3 * t * t) - 1) / 3;
|
|
23953
|
+
const cosPhi = Math.cos(phi);
|
|
23954
|
+
const sinPhi = Math.sin(phi);
|
|
23955
|
+
const cos1 = Math.cos(theta1);
|
|
23956
|
+
const sin1 = Math.sin(theta1);
|
|
23957
|
+
const cos2 = Math.cos(theta2);
|
|
23958
|
+
const sin2 = Math.sin(theta2);
|
|
23959
|
+
const p0x = cx + cosPhi * rx * cos1 - sinPhi * ry * sin1;
|
|
23960
|
+
const p0y = cy + sinPhi * rx * cos1 + cosPhi * ry * sin1;
|
|
23961
|
+
const p1x = cx + cosPhi * rx * cos2 - sinPhi * ry * sin2;
|
|
23962
|
+
const p1y = cy + sinPhi * rx * cos2 + cosPhi * ry * sin2;
|
|
23963
|
+
const t0x = -cosPhi * rx * sin1 - sinPhi * ry * cos1;
|
|
23964
|
+
const t0y = -sinPhi * rx * sin1 + cosPhi * ry * cos1;
|
|
23965
|
+
const t1x = -cosPhi * rx * sin2 - sinPhi * ry * cos2;
|
|
23966
|
+
const t1y = -sinPhi * rx * sin2 + cosPhi * ry * cos2;
|
|
23967
|
+
return {
|
|
23968
|
+
cp1x: p0x + alpha * t0x,
|
|
23969
|
+
cp1y: p0y + alpha * t0y,
|
|
23970
|
+
cp2x: p1x - alpha * t1x,
|
|
23971
|
+
cp2y: p1y - alpha * t1y,
|
|
23972
|
+
x: p1x,
|
|
23973
|
+
y: p1y
|
|
23974
|
+
};
|
|
23975
|
+
}
|
|
23976
|
+
|
|
23977
|
+
//#endregion
|
|
23978
|
+
//#region src/svg/path-parser.ts
|
|
23979
|
+
/**
|
|
23980
|
+
* Number of parameters required for each command type.
|
|
23981
|
+
*/
|
|
23982
|
+
const COMMAND_PARAMS = {
|
|
23983
|
+
M: 2,
|
|
23984
|
+
m: 2,
|
|
23985
|
+
L: 2,
|
|
23986
|
+
l: 2,
|
|
23987
|
+
H: 1,
|
|
23988
|
+
h: 1,
|
|
23989
|
+
V: 1,
|
|
23990
|
+
v: 1,
|
|
23991
|
+
C: 6,
|
|
23992
|
+
c: 6,
|
|
23993
|
+
S: 4,
|
|
23994
|
+
s: 4,
|
|
23995
|
+
Q: 4,
|
|
23996
|
+
q: 4,
|
|
23997
|
+
T: 2,
|
|
23998
|
+
t: 2,
|
|
23999
|
+
A: 7,
|
|
24000
|
+
a: 7,
|
|
24001
|
+
Z: 0,
|
|
24002
|
+
z: 0
|
|
24003
|
+
};
|
|
24004
|
+
/**
|
|
24005
|
+
* Check if a character is a command letter.
|
|
24006
|
+
*/
|
|
24007
|
+
function isCommandLetter(char) {
|
|
24008
|
+
return /^[MmLlHhVvCcSsQqTtAaZz]$/.test(char);
|
|
24009
|
+
}
|
|
24010
|
+
/**
|
|
24011
|
+
* Check if a character can start a number.
|
|
24012
|
+
*/
|
|
24013
|
+
function isNumberStart(char) {
|
|
24014
|
+
return /^[0-9.+-]$/.test(char);
|
|
24015
|
+
}
|
|
24016
|
+
/**
|
|
24017
|
+
* Tokenizer for SVG path strings.
|
|
24018
|
+
* Extracts command letters and numbers from the path string.
|
|
24019
|
+
*/
|
|
24020
|
+
var PathTokenizer = class {
|
|
24021
|
+
path;
|
|
24022
|
+
pos = 0;
|
|
24023
|
+
constructor(path) {
|
|
24024
|
+
this.path = path;
|
|
24025
|
+
}
|
|
24026
|
+
/**
|
|
24027
|
+
* Skip whitespace and commas.
|
|
24028
|
+
*/
|
|
24029
|
+
skipWhitespaceAndCommas() {
|
|
24030
|
+
while (this.pos < this.path.length) {
|
|
24031
|
+
const char = this.path[this.pos];
|
|
24032
|
+
if (char === " " || char === " " || char === "\n" || char === "\r" || char === ",") this.pos++;
|
|
24033
|
+
else break;
|
|
24034
|
+
}
|
|
24035
|
+
}
|
|
24036
|
+
/**
|
|
24037
|
+
* Read a number from the current position.
|
|
24038
|
+
* Handles integers, decimals, negative numbers, and scientific notation.
|
|
24039
|
+
*/
|
|
24040
|
+
readNumber() {
|
|
24041
|
+
this.skipWhitespaceAndCommas();
|
|
24042
|
+
if (this.pos >= this.path.length) return null;
|
|
24043
|
+
const char = this.path[this.pos];
|
|
24044
|
+
if (isCommandLetter(char)) return null;
|
|
24045
|
+
if (!isNumberStart(char)) {
|
|
24046
|
+
this.pos++;
|
|
24047
|
+
return null;
|
|
24048
|
+
}
|
|
24049
|
+
let numStr = "";
|
|
24050
|
+
let hasDecimal = false;
|
|
24051
|
+
let hasExponent = false;
|
|
24052
|
+
if (char === "+" || char === "-") {
|
|
24053
|
+
numStr += char;
|
|
24054
|
+
this.pos++;
|
|
24055
|
+
}
|
|
24056
|
+
while (this.pos < this.path.length) {
|
|
24057
|
+
const c = this.path[this.pos];
|
|
24058
|
+
if (c >= "0" && c <= "9") {
|
|
24059
|
+
numStr += c;
|
|
24060
|
+
this.pos++;
|
|
24061
|
+
} else if (c === "." && !hasDecimal && !hasExponent) {
|
|
24062
|
+
numStr += c;
|
|
24063
|
+
hasDecimal = true;
|
|
24064
|
+
this.pos++;
|
|
24065
|
+
} else if ((c === "e" || c === "E") && !hasExponent && numStr.length > 0) {
|
|
24066
|
+
numStr += c;
|
|
24067
|
+
hasExponent = true;
|
|
24068
|
+
this.pos++;
|
|
24069
|
+
if (this.pos < this.path.length) {
|
|
24070
|
+
const signChar = this.path[this.pos];
|
|
24071
|
+
if (signChar === "+" || signChar === "-") {
|
|
24072
|
+
numStr += signChar;
|
|
24073
|
+
this.pos++;
|
|
24074
|
+
}
|
|
24075
|
+
}
|
|
24076
|
+
} else break;
|
|
24077
|
+
}
|
|
24078
|
+
if (numStr === "" || numStr === "+" || numStr === "-" || numStr === ".") return null;
|
|
24079
|
+
const value = Number.parseFloat(numStr);
|
|
24080
|
+
return Number.isNaN(value) ? null : value;
|
|
24081
|
+
}
|
|
24082
|
+
/**
|
|
24083
|
+
* Read a command letter from the current position.
|
|
24084
|
+
*/
|
|
24085
|
+
readCommand() {
|
|
24086
|
+
this.skipWhitespaceAndCommas();
|
|
24087
|
+
if (this.pos >= this.path.length) return null;
|
|
24088
|
+
const char = this.path[this.pos];
|
|
24089
|
+
if (isCommandLetter(char)) {
|
|
24090
|
+
this.pos++;
|
|
24091
|
+
return char;
|
|
24092
|
+
}
|
|
24093
|
+
return null;
|
|
24094
|
+
}
|
|
24095
|
+
/**
|
|
24096
|
+
* Peek at the next character without consuming it.
|
|
24097
|
+
*/
|
|
24098
|
+
peek() {
|
|
24099
|
+
this.skipWhitespaceAndCommas();
|
|
24100
|
+
if (this.pos >= this.path.length) return null;
|
|
24101
|
+
return this.path[this.pos];
|
|
24102
|
+
}
|
|
24103
|
+
/**
|
|
24104
|
+
* Check if there's more content to parse.
|
|
24105
|
+
*/
|
|
24106
|
+
hasMore() {
|
|
24107
|
+
this.skipWhitespaceAndCommas();
|
|
24108
|
+
return this.pos < this.path.length;
|
|
24109
|
+
}
|
|
24110
|
+
/**
|
|
24111
|
+
* Read a single flag digit (0 or 1) for arc commands.
|
|
24112
|
+
* SVG arc flags are special - they're single digits that can be
|
|
24113
|
+
* concatenated without separators: "00" means two flags, both 0.
|
|
24114
|
+
*/
|
|
24115
|
+
readFlag() {
|
|
24116
|
+
this.skipWhitespaceAndCommas();
|
|
24117
|
+
if (this.pos >= this.path.length) return null;
|
|
24118
|
+
const char = this.path[this.pos];
|
|
24119
|
+
if (char === "0" || char === "1") {
|
|
24120
|
+
this.pos++;
|
|
24121
|
+
return char === "1" ? 1 : 0;
|
|
24122
|
+
}
|
|
24123
|
+
return null;
|
|
24124
|
+
}
|
|
24125
|
+
};
|
|
24126
|
+
/**
|
|
24127
|
+
* Parse an SVG path string into an array of commands.
|
|
24128
|
+
*
|
|
24129
|
+
* @param pathData - The SVG path `d` attribute string
|
|
24130
|
+
* @returns Array of parsed path commands
|
|
24131
|
+
*
|
|
24132
|
+
* @example
|
|
24133
|
+
* ```typescript
|
|
24134
|
+
* const commands = parseSvgPath("M 10 10 L 100 10 L 100 100 Z");
|
|
24135
|
+
* // [
|
|
24136
|
+
* // { type: "M", x: 10, y: 10 },
|
|
24137
|
+
* // { type: "L", x: 100, y: 10 },
|
|
24138
|
+
* // { type: "L", x: 100, y: 100 },
|
|
24139
|
+
* // { type: "Z" },
|
|
24140
|
+
* // ]
|
|
24141
|
+
* ```
|
|
24142
|
+
*/
|
|
24143
|
+
function parseSvgPath(pathData) {
|
|
24144
|
+
const commands = [];
|
|
24145
|
+
const tokenizer = new PathTokenizer(pathData);
|
|
24146
|
+
let currentCommand = null;
|
|
24147
|
+
let isFirstInSequence = true;
|
|
24148
|
+
while (tokenizer.hasMore()) {
|
|
24149
|
+
const nextChar = tokenizer.peek();
|
|
24150
|
+
if (nextChar && isCommandLetter(nextChar)) {
|
|
24151
|
+
currentCommand = tokenizer.readCommand();
|
|
24152
|
+
isFirstInSequence = true;
|
|
24153
|
+
}
|
|
24154
|
+
if (!currentCommand) break;
|
|
24155
|
+
const paramCount = COMMAND_PARAMS[currentCommand];
|
|
24156
|
+
if (currentCommand === "Z" || currentCommand === "z") {
|
|
24157
|
+
commands.push({ type: currentCommand });
|
|
24158
|
+
currentCommand = null;
|
|
24159
|
+
continue;
|
|
24160
|
+
}
|
|
24161
|
+
const params = [];
|
|
24162
|
+
const isArc = currentCommand === "A" || currentCommand === "a";
|
|
24163
|
+
for (let i = 0; i < paramCount; i++) {
|
|
24164
|
+
let num;
|
|
24165
|
+
if (isArc && (i === 3 || i === 4)) num = tokenizer.readFlag();
|
|
24166
|
+
else num = tokenizer.readNumber();
|
|
24167
|
+
if (num === null) break;
|
|
24168
|
+
params.push(num);
|
|
24169
|
+
}
|
|
24170
|
+
if (params.length !== paramCount) continue;
|
|
24171
|
+
const command = createCommand(currentCommand, params);
|
|
24172
|
+
if (command) commands.push(command);
|
|
24173
|
+
if (isFirstInSequence) {
|
|
24174
|
+
isFirstInSequence = false;
|
|
24175
|
+
if (currentCommand === "M") currentCommand = "L";
|
|
24176
|
+
else if (currentCommand === "m") currentCommand = "l";
|
|
24177
|
+
}
|
|
24178
|
+
const nextPeek = tokenizer.peek();
|
|
24179
|
+
if (!nextPeek || isCommandLetter(nextPeek)) {
|
|
24180
|
+
if (!nextPeek) break;
|
|
24181
|
+
}
|
|
24182
|
+
}
|
|
24183
|
+
return commands;
|
|
24184
|
+
}
|
|
24185
|
+
/**
|
|
24186
|
+
* Create a command object from type and parameters.
|
|
24187
|
+
*/
|
|
24188
|
+
function createCommand(type, params) {
|
|
24189
|
+
switch (type) {
|
|
24190
|
+
case "M":
|
|
24191
|
+
case "m": return {
|
|
24192
|
+
type,
|
|
24193
|
+
x: params[0],
|
|
24194
|
+
y: params[1]
|
|
24195
|
+
};
|
|
24196
|
+
case "L":
|
|
24197
|
+
case "l": return {
|
|
24198
|
+
type,
|
|
24199
|
+
x: params[0],
|
|
24200
|
+
y: params[1]
|
|
24201
|
+
};
|
|
24202
|
+
case "H":
|
|
24203
|
+
case "h": return {
|
|
24204
|
+
type,
|
|
24205
|
+
x: params[0]
|
|
24206
|
+
};
|
|
24207
|
+
case "V":
|
|
24208
|
+
case "v": return {
|
|
24209
|
+
type,
|
|
24210
|
+
y: params[0]
|
|
24211
|
+
};
|
|
24212
|
+
case "C":
|
|
24213
|
+
case "c": return {
|
|
24214
|
+
type,
|
|
24215
|
+
x1: params[0],
|
|
24216
|
+
y1: params[1],
|
|
24217
|
+
x2: params[2],
|
|
24218
|
+
y2: params[3],
|
|
24219
|
+
x: params[4],
|
|
24220
|
+
y: params[5]
|
|
24221
|
+
};
|
|
24222
|
+
case "S":
|
|
24223
|
+
case "s": return {
|
|
24224
|
+
type,
|
|
24225
|
+
x2: params[0],
|
|
24226
|
+
y2: params[1],
|
|
24227
|
+
x: params[2],
|
|
24228
|
+
y: params[3]
|
|
24229
|
+
};
|
|
24230
|
+
case "Q":
|
|
24231
|
+
case "q": return {
|
|
24232
|
+
type,
|
|
24233
|
+
x1: params[0],
|
|
24234
|
+
y1: params[1],
|
|
24235
|
+
x: params[2],
|
|
24236
|
+
y: params[3]
|
|
24237
|
+
};
|
|
24238
|
+
case "T":
|
|
24239
|
+
case "t": return {
|
|
24240
|
+
type,
|
|
24241
|
+
x: params[0],
|
|
24242
|
+
y: params[1]
|
|
24243
|
+
};
|
|
24244
|
+
case "A":
|
|
24245
|
+
case "a": return {
|
|
24246
|
+
type,
|
|
24247
|
+
rx: params[0],
|
|
24248
|
+
ry: params[1],
|
|
24249
|
+
xAxisRotation: params[2],
|
|
24250
|
+
largeArcFlag: params[3] !== 0,
|
|
24251
|
+
sweepFlag: params[4] !== 0,
|
|
24252
|
+
x: params[5],
|
|
24253
|
+
y: params[6]
|
|
24254
|
+
};
|
|
24255
|
+
case "Z":
|
|
24256
|
+
case "z": return { type };
|
|
24257
|
+
default: return null;
|
|
24258
|
+
}
|
|
24259
|
+
}
|
|
24260
|
+
|
|
24261
|
+
//#endregion
|
|
24262
|
+
//#region src/svg/path-executor.ts
|
|
24263
|
+
/**
|
|
24264
|
+
* SVG Path Executor
|
|
24265
|
+
*
|
|
24266
|
+
* Executes parsed SVG path commands via a callback interface.
|
|
24267
|
+
* Handles:
|
|
24268
|
+
* - Relative to absolute coordinate conversion
|
|
24269
|
+
* - Smooth curve control point reflection
|
|
24270
|
+
* - Arc to bezier conversion
|
|
24271
|
+
* - Y-axis flipping for PDF coordinate system (optional, default: true)
|
|
24272
|
+
*/
|
|
24273
|
+
/**
|
|
24274
|
+
* Execute SVG path commands via a callback interface.
|
|
24275
|
+
*
|
|
24276
|
+
* The executor handles all coordinate transformations and command
|
|
24277
|
+
* normalization, so the sink receives only absolute coordinates
|
|
24278
|
+
* and standard path operations.
|
|
24279
|
+
*
|
|
24280
|
+
* @param options - Execution options
|
|
24281
|
+
* @param options.commands - Parsed SVG path commands
|
|
24282
|
+
* @param options.sink - Callback interface for path operations
|
|
24283
|
+
* @param options.initialX - Initial X coordinate (default: 0)
|
|
24284
|
+
* @param options.initialY - Initial Y coordinate (default: 0)
|
|
24285
|
+
* @param options.flipY - Flip Y coordinates for PDF (default: true)
|
|
24286
|
+
* @param options.scale - Scale factor (default: 1)
|
|
24287
|
+
* @param options.translateX - X offset after transform (default: 0)
|
|
24288
|
+
* @param options.translateY - Y offset after transform (default: 0)
|
|
24289
|
+
* @returns Final position {x, y} after executing all commands
|
|
24290
|
+
*/
|
|
24291
|
+
function executeSvgPath(options) {
|
|
24292
|
+
const { commands, sink, initialX = 0, initialY = 0, flipY = true, scale = 1, translateX = 0, translateY = 0 } = options;
|
|
24293
|
+
const yFlip = flipY ? -1 : 1;
|
|
24294
|
+
const initialOutputX = initialX + translateX;
|
|
24295
|
+
const initialOutputY = initialY + translateY;
|
|
24296
|
+
const state = {
|
|
24297
|
+
currentX: initialOutputX,
|
|
24298
|
+
currentY: initialOutputY,
|
|
24299
|
+
subpathStartX: initialOutputX,
|
|
24300
|
+
subpathStartY: initialOutputY,
|
|
24301
|
+
lastControlX: initialOutputX,
|
|
24302
|
+
lastControlY: initialOutputY,
|
|
24303
|
+
lastCommand: null,
|
|
24304
|
+
yFlip,
|
|
24305
|
+
scale,
|
|
24306
|
+
translateX,
|
|
24307
|
+
translateY
|
|
24308
|
+
};
|
|
24309
|
+
for (const cmd of commands) executeCommand(cmd, state, sink);
|
|
24310
|
+
return {
|
|
24311
|
+
x: state.currentX,
|
|
24312
|
+
y: state.currentY
|
|
24313
|
+
};
|
|
24314
|
+
}
|
|
24315
|
+
/**
|
|
24316
|
+
* Transform an SVG coordinate to output space.
|
|
24317
|
+
* Applies: scale, Y-flip, then translate.
|
|
24318
|
+
*/
|
|
24319
|
+
function transformX(x, state) {
|
|
24320
|
+
return x * state.scale + state.translateX;
|
|
24321
|
+
}
|
|
24322
|
+
function transformY(y, state) {
|
|
24323
|
+
return y * state.yFlip * state.scale + state.translateY;
|
|
24324
|
+
}
|
|
24325
|
+
/**
|
|
24326
|
+
* Execute a single SVG path command.
|
|
24327
|
+
*/
|
|
24328
|
+
function executeCommand(cmd, state, sink) {
|
|
24329
|
+
switch (cmd.type) {
|
|
24330
|
+
case "M":
|
|
24331
|
+
executeMoveTo({
|
|
24332
|
+
x: cmd.x,
|
|
24333
|
+
y: cmd.y,
|
|
24334
|
+
relative: false,
|
|
24335
|
+
state,
|
|
24336
|
+
sink
|
|
24337
|
+
});
|
|
24338
|
+
break;
|
|
24339
|
+
case "m":
|
|
24340
|
+
executeMoveTo({
|
|
24341
|
+
x: cmd.x,
|
|
24342
|
+
y: cmd.y,
|
|
24343
|
+
relative: true,
|
|
24344
|
+
state,
|
|
24345
|
+
sink
|
|
24346
|
+
});
|
|
24347
|
+
break;
|
|
24348
|
+
case "L":
|
|
24349
|
+
executeLineTo({
|
|
24350
|
+
x: cmd.x,
|
|
24351
|
+
y: cmd.y,
|
|
24352
|
+
relative: false,
|
|
24353
|
+
state,
|
|
24354
|
+
sink
|
|
24355
|
+
});
|
|
24356
|
+
break;
|
|
24357
|
+
case "l":
|
|
24358
|
+
executeLineTo({
|
|
24359
|
+
x: cmd.x,
|
|
24360
|
+
y: cmd.y,
|
|
24361
|
+
relative: true,
|
|
24362
|
+
state,
|
|
24363
|
+
sink
|
|
24364
|
+
});
|
|
24365
|
+
break;
|
|
24366
|
+
case "H":
|
|
24367
|
+
executeHorizontalLine({
|
|
24368
|
+
x: cmd.x,
|
|
24369
|
+
relative: false,
|
|
24370
|
+
state,
|
|
24371
|
+
sink
|
|
24372
|
+
});
|
|
24373
|
+
break;
|
|
24374
|
+
case "h":
|
|
24375
|
+
executeHorizontalLine({
|
|
24376
|
+
x: cmd.x,
|
|
24377
|
+
relative: true,
|
|
24378
|
+
state,
|
|
24379
|
+
sink
|
|
24380
|
+
});
|
|
24381
|
+
break;
|
|
24382
|
+
case "V":
|
|
24383
|
+
executeVerticalLine({
|
|
24384
|
+
y: cmd.y,
|
|
24385
|
+
relative: false,
|
|
24386
|
+
state,
|
|
24387
|
+
sink
|
|
24388
|
+
});
|
|
24389
|
+
break;
|
|
24390
|
+
case "v":
|
|
24391
|
+
executeVerticalLine({
|
|
24392
|
+
y: cmd.y,
|
|
24393
|
+
relative: true,
|
|
24394
|
+
state,
|
|
24395
|
+
sink
|
|
24396
|
+
});
|
|
24397
|
+
break;
|
|
24398
|
+
case "C":
|
|
24399
|
+
executeCubicCurve({
|
|
24400
|
+
x1: cmd.x1,
|
|
24401
|
+
y1: cmd.y1,
|
|
24402
|
+
x2: cmd.x2,
|
|
24403
|
+
y2: cmd.y2,
|
|
24404
|
+
x: cmd.x,
|
|
24405
|
+
y: cmd.y,
|
|
24406
|
+
relative: false,
|
|
24407
|
+
state,
|
|
24408
|
+
sink
|
|
24409
|
+
});
|
|
24410
|
+
break;
|
|
24411
|
+
case "c":
|
|
24412
|
+
executeCubicCurve({
|
|
24413
|
+
x1: cmd.x1,
|
|
24414
|
+
y1: cmd.y1,
|
|
24415
|
+
x2: cmd.x2,
|
|
24416
|
+
y2: cmd.y2,
|
|
24417
|
+
x: cmd.x,
|
|
24418
|
+
y: cmd.y,
|
|
24419
|
+
relative: true,
|
|
24420
|
+
state,
|
|
24421
|
+
sink
|
|
24422
|
+
});
|
|
24423
|
+
break;
|
|
24424
|
+
case "S":
|
|
24425
|
+
executeSmoothCubic({
|
|
24426
|
+
x2: cmd.x2,
|
|
24427
|
+
y2: cmd.y2,
|
|
24428
|
+
x: cmd.x,
|
|
24429
|
+
y: cmd.y,
|
|
24430
|
+
relative: false,
|
|
24431
|
+
state,
|
|
24432
|
+
sink
|
|
24433
|
+
});
|
|
24434
|
+
break;
|
|
24435
|
+
case "s":
|
|
24436
|
+
executeSmoothCubic({
|
|
24437
|
+
x2: cmd.x2,
|
|
24438
|
+
y2: cmd.y2,
|
|
24439
|
+
x: cmd.x,
|
|
24440
|
+
y: cmd.y,
|
|
24441
|
+
relative: true,
|
|
24442
|
+
state,
|
|
24443
|
+
sink
|
|
24444
|
+
});
|
|
24445
|
+
break;
|
|
24446
|
+
case "Q":
|
|
24447
|
+
executeQuadratic({
|
|
24448
|
+
x1: cmd.x1,
|
|
24449
|
+
y1: cmd.y1,
|
|
24450
|
+
x: cmd.x,
|
|
24451
|
+
y: cmd.y,
|
|
24452
|
+
relative: false,
|
|
24453
|
+
state,
|
|
24454
|
+
sink
|
|
24455
|
+
});
|
|
24456
|
+
break;
|
|
24457
|
+
case "q":
|
|
24458
|
+
executeQuadratic({
|
|
24459
|
+
x1: cmd.x1,
|
|
24460
|
+
y1: cmd.y1,
|
|
24461
|
+
x: cmd.x,
|
|
24462
|
+
y: cmd.y,
|
|
24463
|
+
relative: true,
|
|
24464
|
+
state,
|
|
24465
|
+
sink
|
|
24466
|
+
});
|
|
24467
|
+
break;
|
|
24468
|
+
case "T":
|
|
24469
|
+
executeSmoothQuadratic({
|
|
24470
|
+
x: cmd.x,
|
|
24471
|
+
y: cmd.y,
|
|
24472
|
+
relative: false,
|
|
24473
|
+
state,
|
|
24474
|
+
sink
|
|
24475
|
+
});
|
|
24476
|
+
break;
|
|
24477
|
+
case "t":
|
|
24478
|
+
executeSmoothQuadratic({
|
|
24479
|
+
x: cmd.x,
|
|
24480
|
+
y: cmd.y,
|
|
24481
|
+
relative: true,
|
|
24482
|
+
state,
|
|
24483
|
+
sink
|
|
24484
|
+
});
|
|
24485
|
+
break;
|
|
24486
|
+
case "A":
|
|
24487
|
+
executeArc({
|
|
24488
|
+
rx: cmd.rx,
|
|
24489
|
+
ry: cmd.ry,
|
|
24490
|
+
xAxisRotation: cmd.xAxisRotation,
|
|
24491
|
+
largeArcFlag: cmd.largeArcFlag,
|
|
24492
|
+
sweepFlag: cmd.sweepFlag,
|
|
24493
|
+
x: cmd.x,
|
|
24494
|
+
y: cmd.y,
|
|
24495
|
+
relative: false,
|
|
24496
|
+
state,
|
|
24497
|
+
sink
|
|
24498
|
+
});
|
|
24499
|
+
break;
|
|
24500
|
+
case "a":
|
|
24501
|
+
executeArc({
|
|
24502
|
+
rx: cmd.rx,
|
|
24503
|
+
ry: cmd.ry,
|
|
24504
|
+
xAxisRotation: cmd.xAxisRotation,
|
|
24505
|
+
largeArcFlag: cmd.largeArcFlag,
|
|
24506
|
+
sweepFlag: cmd.sweepFlag,
|
|
24507
|
+
x: cmd.x,
|
|
24508
|
+
y: cmd.y,
|
|
24509
|
+
relative: true,
|
|
24510
|
+
state,
|
|
24511
|
+
sink
|
|
24512
|
+
});
|
|
24513
|
+
break;
|
|
24514
|
+
case "Z":
|
|
24515
|
+
case "z":
|
|
24516
|
+
executeClose({
|
|
24517
|
+
state,
|
|
24518
|
+
sink
|
|
24519
|
+
});
|
|
24520
|
+
break;
|
|
24521
|
+
}
|
|
24522
|
+
state.lastCommand = cmd.type;
|
|
24523
|
+
}
|
|
24524
|
+
function executeMoveTo(options) {
|
|
24525
|
+
const { x, y, relative, state, sink } = options;
|
|
24526
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24527
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24528
|
+
sink.moveTo(outX, outY);
|
|
24529
|
+
state.currentX = outX;
|
|
24530
|
+
state.currentY = outY;
|
|
24531
|
+
state.subpathStartX = outX;
|
|
24532
|
+
state.subpathStartY = outY;
|
|
24533
|
+
state.lastControlX = outX;
|
|
24534
|
+
state.lastControlY = outY;
|
|
24535
|
+
}
|
|
24536
|
+
function executeLineTo(options) {
|
|
24537
|
+
const { x, y, relative, state, sink } = options;
|
|
24538
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24539
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24540
|
+
sink.lineTo(outX, outY);
|
|
24541
|
+
state.currentX = outX;
|
|
24542
|
+
state.currentY = outY;
|
|
24543
|
+
state.lastControlX = outX;
|
|
24544
|
+
state.lastControlY = outY;
|
|
24545
|
+
}
|
|
24546
|
+
function executeHorizontalLine(options) {
|
|
24547
|
+
const { x, relative, state, sink } = options;
|
|
24548
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24549
|
+
sink.lineTo(outX, state.currentY);
|
|
24550
|
+
state.currentX = outX;
|
|
24551
|
+
state.lastControlX = outX;
|
|
24552
|
+
state.lastControlY = state.currentY;
|
|
24553
|
+
}
|
|
24554
|
+
function executeVerticalLine(options) {
|
|
24555
|
+
const { y, relative, state, sink } = options;
|
|
24556
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24557
|
+
sink.lineTo(state.currentX, outY);
|
|
24558
|
+
state.currentY = outY;
|
|
24559
|
+
state.lastControlX = state.currentX;
|
|
24560
|
+
state.lastControlY = outY;
|
|
24561
|
+
}
|
|
24562
|
+
function executeCubicCurve(options) {
|
|
24563
|
+
const { x1, y1, x2, y2, x, y, relative, state, sink } = options;
|
|
24564
|
+
const outX1 = relative ? state.currentX + x1 * state.scale : transformX(x1, state);
|
|
24565
|
+
const outY1 = relative ? state.currentY + y1 * state.yFlip * state.scale : transformY(y1, state);
|
|
24566
|
+
const outX2 = relative ? state.currentX + x2 * state.scale : transformX(x2, state);
|
|
24567
|
+
const outY2 = relative ? state.currentY + y2 * state.yFlip * state.scale : transformY(y2, state);
|
|
24568
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24569
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24570
|
+
sink.curveTo(outX1, outY1, outX2, outY2, outX, outY);
|
|
24571
|
+
state.currentX = outX;
|
|
24572
|
+
state.currentY = outY;
|
|
24573
|
+
state.lastControlX = outX2;
|
|
24574
|
+
state.lastControlY = outY2;
|
|
24575
|
+
}
|
|
24576
|
+
function executeSmoothCubic(options) {
|
|
24577
|
+
const { x2, y2, x, y, relative, state, sink } = options;
|
|
24578
|
+
let cp1x;
|
|
24579
|
+
let cp1y;
|
|
24580
|
+
if (state.lastCommand === "C" || state.lastCommand === "c" || state.lastCommand === "S" || state.lastCommand === "s") {
|
|
24581
|
+
cp1x = 2 * state.currentX - state.lastControlX;
|
|
24582
|
+
cp1y = 2 * state.currentY - state.lastControlY;
|
|
24583
|
+
} else {
|
|
24584
|
+
cp1x = state.currentX;
|
|
24585
|
+
cp1y = state.currentY;
|
|
24586
|
+
}
|
|
24587
|
+
const outX2 = relative ? state.currentX + x2 * state.scale : transformX(x2, state);
|
|
24588
|
+
const outY2 = relative ? state.currentY + y2 * state.yFlip * state.scale : transformY(y2, state);
|
|
24589
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24590
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24591
|
+
sink.curveTo(cp1x, cp1y, outX2, outY2, outX, outY);
|
|
24592
|
+
state.currentX = outX;
|
|
24593
|
+
state.currentY = outY;
|
|
24594
|
+
state.lastControlX = outX2;
|
|
24595
|
+
state.lastControlY = outY2;
|
|
24596
|
+
}
|
|
24597
|
+
function executeQuadratic(options) {
|
|
24598
|
+
const { x1, y1, x, y, relative, state, sink } = options;
|
|
24599
|
+
const outCpX = relative ? state.currentX + x1 * state.scale : transformX(x1, state);
|
|
24600
|
+
const outCpY = relative ? state.currentY + y1 * state.yFlip * state.scale : transformY(y1, state);
|
|
24601
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24602
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24603
|
+
sink.quadraticCurveTo(outCpX, outCpY, outX, outY);
|
|
24604
|
+
state.currentX = outX;
|
|
24605
|
+
state.currentY = outY;
|
|
24606
|
+
state.lastControlX = outCpX;
|
|
24607
|
+
state.lastControlY = outCpY;
|
|
24608
|
+
}
|
|
24609
|
+
function executeSmoothQuadratic(options) {
|
|
24610
|
+
const { x, y, relative, state, sink } = options;
|
|
24611
|
+
let cpx;
|
|
24612
|
+
let cpy;
|
|
24613
|
+
if (state.lastCommand === "Q" || state.lastCommand === "q" || state.lastCommand === "T" || state.lastCommand === "t") {
|
|
24614
|
+
cpx = 2 * state.currentX - state.lastControlX;
|
|
24615
|
+
cpy = 2 * state.currentY - state.lastControlY;
|
|
24616
|
+
} else {
|
|
24617
|
+
cpx = state.currentX;
|
|
24618
|
+
cpy = state.currentY;
|
|
24619
|
+
}
|
|
24620
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24621
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24622
|
+
sink.quadraticCurveTo(cpx, cpy, outX, outY);
|
|
24623
|
+
state.currentX = outX;
|
|
24624
|
+
state.currentY = outY;
|
|
24625
|
+
state.lastControlX = cpx;
|
|
24626
|
+
state.lastControlY = cpy;
|
|
24627
|
+
}
|
|
24628
|
+
function executeArc(options) {
|
|
24629
|
+
const { rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y, relative, state, sink } = options;
|
|
24630
|
+
const outX = relative ? state.currentX + x * state.scale : transformX(x, state);
|
|
24631
|
+
const outY = relative ? state.currentY + y * state.yFlip * state.scale : transformY(y, state);
|
|
24632
|
+
const scaledRx = rx * state.scale;
|
|
24633
|
+
const scaledRy = ry * state.scale;
|
|
24634
|
+
const effectiveSweepFlag = state.yFlip === -1 ? !sweepFlag : sweepFlag;
|
|
24635
|
+
const curves = arcToBezier({
|
|
24636
|
+
x1: state.currentX,
|
|
24637
|
+
y1: state.currentY,
|
|
24638
|
+
rx: scaledRx,
|
|
24639
|
+
ry: scaledRy,
|
|
24640
|
+
xAxisRotation,
|
|
24641
|
+
largeArcFlag,
|
|
24642
|
+
sweepFlag: effectiveSweepFlag,
|
|
24643
|
+
x2: outX,
|
|
24644
|
+
y2: outY
|
|
24645
|
+
});
|
|
24646
|
+
for (const curve of curves) sink.curveTo(curve.cp1x, curve.cp1y, curve.cp2x, curve.cp2y, curve.x, curve.y);
|
|
24647
|
+
state.currentX = outX;
|
|
24648
|
+
state.currentY = outY;
|
|
24649
|
+
state.lastControlX = outX;
|
|
24650
|
+
state.lastControlY = outY;
|
|
24651
|
+
}
|
|
24652
|
+
function executeClose(options) {
|
|
24653
|
+
const { state, sink } = options;
|
|
24654
|
+
sink.close();
|
|
24655
|
+
state.currentX = state.subpathStartX;
|
|
24656
|
+
state.currentY = state.subpathStartY;
|
|
24657
|
+
state.lastControlX = state.subpathStartX;
|
|
24658
|
+
state.lastControlY = state.subpathStartY;
|
|
24659
|
+
}
|
|
24660
|
+
/**
|
|
24661
|
+
* Parse and execute an SVG path string.
|
|
24662
|
+
*
|
|
24663
|
+
* This is a convenience function that combines parsing and execution.
|
|
24664
|
+
*
|
|
24665
|
+
* By default, Y coordinates are flipped (negated) to convert from SVG's
|
|
24666
|
+
* top-left origin to PDF's bottom-left origin. Set `flipY: false` in
|
|
24667
|
+
* options to disable this behavior.
|
|
24668
|
+
*
|
|
24669
|
+
* @param options - Execution options
|
|
24670
|
+
* @param options.pathData - SVG path d string
|
|
24671
|
+
* @param options.sink - Callback interface for path operations
|
|
24672
|
+
* @param options.initialX - Initial X coordinate (default: 0)
|
|
24673
|
+
* @param options.initialY - Initial Y coordinate (default: 0)
|
|
24674
|
+
* @param options.flipY - Flip Y coordinates for PDF (default: true)
|
|
24675
|
+
* @param options.scale - Scale factor (default: 1)
|
|
24676
|
+
* @param options.translateX - X offset after transform (default: 0)
|
|
24677
|
+
* @param options.translateY - Y offset after transform (default: 0)
|
|
24678
|
+
* @returns Final position {x, y} after executing all commands
|
|
24679
|
+
*
|
|
24680
|
+
* @example
|
|
24681
|
+
* ```typescript
|
|
24682
|
+
* const sink = {
|
|
24683
|
+
* moveTo: (x, y) => console.log(`M ${x} ${y}`),
|
|
24684
|
+
* lineTo: (x, y) => console.log(`L ${x} ${y}`),
|
|
24685
|
+
* curveTo: (cp1x, cp1y, cp2x, cp2y, x, y) => console.log(`C ...`),
|
|
24686
|
+
* quadraticCurveTo: (cpx, cpy, x, y) => console.log(`Q ...`),
|
|
24687
|
+
* close: () => console.log(`Z`),
|
|
24688
|
+
* };
|
|
24689
|
+
*
|
|
24690
|
+
* executeSvgPathString({ pathData: "M 10 10 L 100 10 L 100 100 Z", sink });
|
|
24691
|
+
* ```
|
|
24692
|
+
*/
|
|
24693
|
+
function executeSvgPathString(options) {
|
|
24694
|
+
const { pathData, sink, initialX, initialY, ...executorOptions } = options;
|
|
24695
|
+
return executeSvgPath({
|
|
24696
|
+
commands: parseSvgPath(pathData),
|
|
24697
|
+
sink,
|
|
24698
|
+
initialX,
|
|
24699
|
+
initialY,
|
|
24700
|
+
...executorOptions
|
|
24701
|
+
});
|
|
24702
|
+
}
|
|
24703
|
+
|
|
23725
24704
|
//#endregion
|
|
23726
24705
|
//#region src/api/drawing/path-builder.ts
|
|
23727
24706
|
/**
|
|
@@ -23848,6 +24827,71 @@ var PathBuilder = class {
|
|
|
23848
24827
|
return this.moveTo(cx - rx, cy).curveTo(cx - rx, cy + ky, cx - kx, cy + ry, cx, cy + ry).curveTo(cx + kx, cy + ry, cx + rx, cy + ky, cx + rx, cy).curveTo(cx + rx, cy - ky, cx + kx, cy - ry, cx, cy - ry).curveTo(cx - kx, cy - ry, cx - rx, cy - ky, cx - rx, cy).close();
|
|
23849
24828
|
}
|
|
23850
24829
|
/**
|
|
24830
|
+
* Append an SVG path string to the current path.
|
|
24831
|
+
*
|
|
24832
|
+
* Parses the SVG path `d` attribute string and adds all commands to this path.
|
|
24833
|
+
* Relative commands (lowercase) are converted to absolute coordinates based on
|
|
24834
|
+
* the current point. Smooth curves (S, T) and arcs (A) are converted to
|
|
24835
|
+
* cubic bezier curves.
|
|
24836
|
+
*
|
|
24837
|
+
* By default, this method does NOT transform coordinates (flipY: false).
|
|
24838
|
+
* Use the options to apply scale, translation, and Y-flip for SVG paths.
|
|
24839
|
+
*
|
|
24840
|
+
* @param pathData - SVG path `d` attribute string
|
|
24841
|
+
* @param options - Execution options (flipY, scale, translate)
|
|
24842
|
+
* @returns This PathBuilder for chaining
|
|
24843
|
+
*
|
|
24844
|
+
* @example
|
|
24845
|
+
* ```typescript
|
|
24846
|
+
* // Simple path (no transform)
|
|
24847
|
+
* page.drawPath()
|
|
24848
|
+
* .appendSvgPath("M 10 10 L 100 10 L 55 90 Z")
|
|
24849
|
+
* .fill({ color: rgb(1, 0, 0) });
|
|
24850
|
+
*
|
|
24851
|
+
* // SVG icon with full transform
|
|
24852
|
+
* page.drawPath()
|
|
24853
|
+
* .appendSvgPath(iconPath, {
|
|
24854
|
+
* flipY: true,
|
|
24855
|
+
* scale: 0.1,
|
|
24856
|
+
* translateX: 100,
|
|
24857
|
+
* translateY: 500,
|
|
24858
|
+
* })
|
|
24859
|
+
* .fill({ color: rgb(0, 0, 0) });
|
|
24860
|
+
* ```
|
|
24861
|
+
*/
|
|
24862
|
+
appendSvgPath(pathData, options = {}) {
|
|
24863
|
+
const executorOptions = {
|
|
24864
|
+
flipY: options.flipY ?? false,
|
|
24865
|
+
scale: options.scale,
|
|
24866
|
+
translateX: options.translateX,
|
|
24867
|
+
translateY: options.translateY
|
|
24868
|
+
};
|
|
24869
|
+
executeSvgPathString({
|
|
24870
|
+
pathData,
|
|
24871
|
+
sink: {
|
|
24872
|
+
moveTo: (x, y) => {
|
|
24873
|
+
this.moveTo(x, y);
|
|
24874
|
+
},
|
|
24875
|
+
lineTo: (x, y) => {
|
|
24876
|
+
this.lineTo(x, y);
|
|
24877
|
+
},
|
|
24878
|
+
curveTo: (cp1x, cp1y, cp2x, cp2y, x, y) => {
|
|
24879
|
+
this.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
|
24880
|
+
},
|
|
24881
|
+
quadraticCurveTo: (cpx, cpy, x, y) => {
|
|
24882
|
+
this.quadraticCurveTo(cpx, cpy, x, y);
|
|
24883
|
+
},
|
|
24884
|
+
close: () => {
|
|
24885
|
+
this.close();
|
|
24886
|
+
}
|
|
24887
|
+
},
|
|
24888
|
+
initialX: this.currentX,
|
|
24889
|
+
initialY: this.currentY,
|
|
24890
|
+
...executorOptions
|
|
24891
|
+
});
|
|
24892
|
+
return this;
|
|
24893
|
+
}
|
|
24894
|
+
/**
|
|
23851
24895
|
* Stroke the path with the given options.
|
|
23852
24896
|
*/
|
|
23853
24897
|
stroke(options = {}) {
|
|
@@ -24839,6 +25883,76 @@ var PDFPage = class PDFPage {
|
|
|
24839
25883
|
drawPath() {
|
|
24840
25884
|
return new PathBuilder((content) => this.appendContent(content), (fillOpacity, strokeOpacity) => this.registerGraphicsStateForOpacity(fillOpacity, strokeOpacity));
|
|
24841
25885
|
}
|
|
25886
|
+
/**
|
|
25887
|
+
* Draw an SVG path on the page.
|
|
25888
|
+
*
|
|
25889
|
+
* This is a convenience method that parses an SVG path `d` attribute string
|
|
25890
|
+
* and draws it with the specified options. For more control, use `drawPath()`
|
|
25891
|
+
* with `appendSvgPath()`.
|
|
25892
|
+
*
|
|
25893
|
+
* By default, the path is filled with black. Specify `borderColor` without
|
|
25894
|
+
* `color` to stroke without filling.
|
|
25895
|
+
*
|
|
25896
|
+
* SVG paths are automatically transformed from SVG coordinate space (Y-down)
|
|
25897
|
+
* to PDF coordinate space (Y-up). Use `x`, `y` to position the path, and
|
|
25898
|
+
* `scale` to resize it.
|
|
25899
|
+
*
|
|
25900
|
+
* @param pathData - SVG path `d` attribute string
|
|
25901
|
+
* @param options - Drawing options (x, y, scale, color, etc.)
|
|
25902
|
+
*
|
|
25903
|
+
* @example
|
|
25904
|
+
* ```typescript
|
|
25905
|
+
* // Draw a Font Awesome heart icon at position (100, 500)
|
|
25906
|
+
* // Icon is 512x512 in SVG, scale to ~50pt
|
|
25907
|
+
* page.drawSvgPath(faHeartPath, {
|
|
25908
|
+
* x: 100,
|
|
25909
|
+
* y: 500,
|
|
25910
|
+
* scale: 0.1,
|
|
25911
|
+
* color: rgb(1, 0, 0),
|
|
25912
|
+
* });
|
|
25913
|
+
*
|
|
25914
|
+
* // Draw a simple triangle at default position (0, 0)
|
|
25915
|
+
* page.drawSvgPath("M 0 0 L 50 0 L 25 40 Z", {
|
|
25916
|
+
* color: rgb(0, 0, 1),
|
|
25917
|
+
* });
|
|
25918
|
+
*
|
|
25919
|
+
* // Stroke a curve
|
|
25920
|
+
* page.drawSvgPath("M 0 0 C 10 10, 30 10, 40 0", {
|
|
25921
|
+
* x: 200,
|
|
25922
|
+
* y: 300,
|
|
25923
|
+
* borderColor: rgb(0, 0, 0),
|
|
25924
|
+
* borderWidth: 2,
|
|
25925
|
+
* });
|
|
25926
|
+
* ```
|
|
25927
|
+
*/
|
|
25928
|
+
drawSvgPath(pathData, options = {}) {
|
|
25929
|
+
const x = options.x ?? 0;
|
|
25930
|
+
const y = options.y ?? 0;
|
|
25931
|
+
const scale = options.scale ?? 1;
|
|
25932
|
+
const flipY = options.flipY ?? true;
|
|
25933
|
+
const builder = this.drawPath();
|
|
25934
|
+
builder.appendSvgPath(pathData, {
|
|
25935
|
+
flipY,
|
|
25936
|
+
scale,
|
|
25937
|
+
translateX: x,
|
|
25938
|
+
translateY: y
|
|
25939
|
+
});
|
|
25940
|
+
const hasFill = options.color !== void 0;
|
|
25941
|
+
const hasStroke = options.borderColor !== void 0;
|
|
25942
|
+
if (hasFill && hasStroke) {
|
|
25943
|
+
builder.fillAndStroke(options);
|
|
25944
|
+
return;
|
|
25945
|
+
}
|
|
25946
|
+
if (hasStroke) {
|
|
25947
|
+
builder.stroke(options);
|
|
25948
|
+
return;
|
|
25949
|
+
}
|
|
25950
|
+
const fillOptions = hasFill ? options : {
|
|
25951
|
+
...options,
|
|
25952
|
+
color: black
|
|
25953
|
+
};
|
|
25954
|
+
builder.fill(fillOptions);
|
|
25955
|
+
}
|
|
24842
25956
|
/** Cached annotations for this page */
|
|
24843
25957
|
_annotationCache = null;
|
|
24844
25958
|
/**
|
|
@@ -25334,9 +26448,12 @@ var PDFPage = class PDFPage {
|
|
|
25334
26448
|
}
|
|
25335
26449
|
/**
|
|
25336
26450
|
* Create and register a content stream.
|
|
26451
|
+
*
|
|
26452
|
+
* Accepts either a string (for ASCII-only content like operator names and numbers)
|
|
26453
|
+
* or raw bytes (for content that may contain non-ASCII data).
|
|
25337
26454
|
*/
|
|
25338
26455
|
createContentStream(content) {
|
|
25339
|
-
const stream = new PdfStream([], new TextEncoder().encode(content));
|
|
26456
|
+
const stream = new PdfStream([], typeof content === "string" ? new TextEncoder().encode(content) : content);
|
|
25340
26457
|
if (this.ctx) return this.ctx.register(stream);
|
|
25341
26458
|
return stream;
|
|
25342
26459
|
}
|
|
@@ -25345,7 +26462,8 @@ var PDFPage = class PDFPage {
|
|
|
25345
26462
|
*/
|
|
25346
26463
|
prependContent(content) {
|
|
25347
26464
|
const existingContents = this.dict.get("Contents");
|
|
25348
|
-
const
|
|
26465
|
+
const contentWithNewline = typeof content === "string" ? `${content}\n` : concatBytes([content, new Uint8Array([10])]);
|
|
26466
|
+
const newContent = this.createContentStream(contentWithNewline);
|
|
25349
26467
|
if (!existingContents) {
|
|
25350
26468
|
this.dict.set("Contents", newContent);
|
|
25351
26469
|
return;
|
|
@@ -25377,7 +26495,8 @@ var PDFPage = class PDFPage {
|
|
|
25377
26495
|
*/
|
|
25378
26496
|
appendContent(content) {
|
|
25379
26497
|
const existingContents = this.dict.get("Contents");
|
|
25380
|
-
const
|
|
26498
|
+
const contentWithNewline = typeof content === "string" ? `\n${content}` : concatBytes([new Uint8Array([10]), content]);
|
|
26499
|
+
const newContent = this.createContentStream(contentWithNewline);
|
|
25381
26500
|
if (!existingContents) {
|
|
25382
26501
|
this.dict.set("Contents", newContent);
|
|
25383
26502
|
return;
|
|
@@ -25453,10 +26572,18 @@ var PDFPage = class PDFPage {
|
|
|
25453
26572
|
}
|
|
25454
26573
|
/**
|
|
25455
26574
|
* Append operators to the page content stream.
|
|
26575
|
+
*
|
|
26576
|
+
* Uses Operator.toBytes() directly to avoid UTF-8 round-trip corruption
|
|
26577
|
+
* of non-ASCII bytes in PdfString operands (e.g., WinAnsi-encoded text).
|
|
25456
26578
|
*/
|
|
25457
26579
|
appendOperators(ops) {
|
|
25458
|
-
const
|
|
25459
|
-
|
|
26580
|
+
const newline = new Uint8Array([10]);
|
|
26581
|
+
const parts = [];
|
|
26582
|
+
for (let i = 0; i < ops.length; i++) {
|
|
26583
|
+
if (i > 0) parts.push(newline);
|
|
26584
|
+
parts.push(ops[i].toBytes());
|
|
26585
|
+
}
|
|
26586
|
+
this.appendContent(concatBytes(parts));
|
|
25460
26587
|
}
|
|
25461
26588
|
/**
|
|
25462
26589
|
* Add a font resource to the page and return its name.
|
|
@@ -25474,7 +26601,12 @@ var PDFPage = class PDFPage {
|
|
|
25474
26601
|
const baseFont = value.get("BaseFont", this.ctx.resolve.bind(this.ctx));
|
|
25475
26602
|
if (baseFont instanceof PdfName && baseFont.value === font) return existingName.value;
|
|
25476
26603
|
}
|
|
25477
|
-
const fontDict = PdfDict.of({
|
|
26604
|
+
const fontDict = isWinAnsiStandard14(font) ? PdfDict.of({
|
|
26605
|
+
Type: PdfName.of("Font"),
|
|
26606
|
+
Subtype: PdfName.of("Type1"),
|
|
26607
|
+
BaseFont: PdfName.of(font),
|
|
26608
|
+
Encoding: PdfName.of("WinAnsiEncoding")
|
|
26609
|
+
}) : PdfDict.of({
|
|
25478
26610
|
Type: PdfName.of("Font"),
|
|
25479
26611
|
Subtype: PdfName.of("Type1"),
|
|
25480
26612
|
BaseFont: PdfName.of(font)
|
|
@@ -25494,9 +26626,20 @@ var PDFPage = class PDFPage {
|
|
|
25494
26626
|
}
|
|
25495
26627
|
/**
|
|
25496
26628
|
* Encode text to a PDF string for the given font.
|
|
26629
|
+
*
|
|
26630
|
+
* Standard 14 fonts use WinAnsiEncoding (or SymbolEncoding/ZapfDingbatsEncoding).
|
|
26631
|
+
* Unencodable characters are substituted with .notdef (byte 0x00).
|
|
26632
|
+
* Embedded fonts use Identity-H encoding with glyph IDs.
|
|
25497
26633
|
*/
|
|
25498
26634
|
encodeTextForFont(text, font) {
|
|
25499
|
-
if (typeof font === "string")
|
|
26635
|
+
if (typeof font === "string") {
|
|
26636
|
+
const encoding = getEncodingForStandard14(font);
|
|
26637
|
+
const codes = [];
|
|
26638
|
+
for (const char of text) if (encoding.canEncode(char)) codes.push(encoding.getCode(char.codePointAt(0)));
|
|
26639
|
+
else codes.push(0);
|
|
26640
|
+
const bytes$1 = new Uint8Array(codes);
|
|
26641
|
+
return PdfString.fromBytes(bytes$1);
|
|
26642
|
+
}
|
|
25500
26643
|
const gids = font.encodeTextToGids(text);
|
|
25501
26644
|
const bytes = new Uint8Array(gids.length * 2);
|
|
25502
26645
|
for (let i = 0; i < gids.length; i++) {
|