fabricjs-document-engine 1.0.1 → 1.1.0

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.
Files changed (89) hide show
  1. package/README.md +277 -36
  2. package/README.zh-CN.md +697 -0
  3. package/dist/assets/asset-manifest.d.cts +3 -0
  4. package/dist/assets/asset-manifest.d.ts +3 -0
  5. package/dist/assets/asset-pipeline.cjs +22 -10
  6. package/dist/assets/asset-pipeline.d.cts +5 -1
  7. package/dist/assets/asset-pipeline.d.ts +5 -1
  8. package/dist/assets/asset-pipeline.js +22 -10
  9. package/dist/assets/font-check.cjs +9 -1
  10. package/dist/assets/font-check.js +9 -1
  11. package/dist/assets/image-check.cjs +88 -6
  12. package/dist/assets/image-check.d.cts +17 -0
  13. package/dist/assets/image-check.d.ts +17 -0
  14. package/dist/assets/image-check.js +88 -7
  15. package/dist/commands/clipboard.cjs +136 -0
  16. package/dist/commands/clipboard.d.cts +47 -0
  17. package/dist/commands/clipboard.d.ts +47 -0
  18. package/dist/commands/clipboard.js +135 -0
  19. package/dist/commands/layers.cjs +97 -0
  20. package/dist/commands/layers.d.cts +39 -0
  21. package/dist/commands/layers.d.ts +39 -0
  22. package/dist/commands/layers.js +92 -0
  23. package/dist/engine/create-document-engine.cjs +85 -14
  24. package/dist/engine/create-document-engine.d.cts +20 -1
  25. package/dist/engine/create-document-engine.d.ts +20 -1
  26. package/dist/engine/create-document-engine.js +86 -15
  27. package/dist/engine/errors.d.cts +1 -1
  28. package/dist/engine/errors.d.ts +1 -1
  29. package/dist/export/batch-render.cjs +172 -0
  30. package/dist/export/batch-render.d.cts +43 -0
  31. package/dist/export/batch-render.d.ts +43 -0
  32. package/dist/export/batch-render.js +171 -0
  33. package/dist/export/export-options.cjs +18 -1
  34. package/dist/export/export-options.d.cts +23 -0
  35. package/dist/export/export-options.d.ts +23 -0
  36. package/dist/export/export-options.js +18 -2
  37. package/dist/export/preflight-export.cjs +11 -2
  38. package/dist/export/preflight-export.d.cts +1 -1
  39. package/dist/export/preflight-export.d.ts +1 -1
  40. package/dist/export/preflight-export.js +11 -2
  41. package/dist/export/render-export.cjs +18 -3
  42. package/dist/export/render-export.js +18 -4
  43. package/dist/export/svg/embed-assets.cjs +249 -0
  44. package/dist/export/svg/embed-assets.js +249 -0
  45. package/dist/export/svg/overrides.cjs +32 -0
  46. package/dist/export/svg/overrides.js +32 -0
  47. package/dist/export/svg/text-on-path.cjs +252 -0
  48. package/dist/export/svg/text-on-path.js +249 -0
  49. package/dist/fabric/fabric-adapter.cjs +46 -5
  50. package/dist/fabric/fabric-adapter.js +46 -5
  51. package/dist/fabric/object-registry.cjs +1 -1
  52. package/dist/fabric/object-registry.js +1 -1
  53. package/dist/import/svg-import.cjs +256 -0
  54. package/dist/import/svg-import.d.cts +50 -0
  55. package/dist/import/svg-import.d.ts +50 -0
  56. package/dist/import/svg-import.js +256 -0
  57. package/dist/index.cjs +12 -0
  58. package/dist/index.d.cts +9 -4
  59. package/dist/index.d.ts +9 -4
  60. package/dist/index.js +4 -1
  61. package/dist/pdf/export-pdf.cjs +440 -0
  62. package/dist/pdf/export-pdf.d.cts +73 -0
  63. package/dist/pdf/export-pdf.d.ts +73 -0
  64. package/dist/pdf/export-pdf.js +440 -0
  65. package/dist/pdf/fonts.cjs +115 -0
  66. package/dist/pdf/fonts.d.cts +12 -0
  67. package/dist/pdf/fonts.d.ts +12 -0
  68. package/dist/pdf/fonts.js +112 -0
  69. package/dist/pdf/page-layout.cjs +79 -0
  70. package/dist/pdf/page-layout.d.cts +51 -0
  71. package/dist/pdf/page-layout.d.ts +51 -0
  72. package/dist/pdf/page-layout.js +76 -0
  73. package/dist/pdf/text-decorations.cjs +108 -0
  74. package/dist/pdf/text-decorations.js +107 -0
  75. package/dist/pdf.cjs +7 -0
  76. package/dist/pdf.d.cts +4 -0
  77. package/dist/pdf.d.ts +4 -0
  78. package/dist/pdf.js +3 -0
  79. package/dist/react/use-layers.cjs +53 -0
  80. package/dist/react/use-layers.d.cts +10 -0
  81. package/dist/react/use-layers.d.ts +10 -0
  82. package/dist/react/use-layers.js +53 -0
  83. package/dist/react.cjs +2 -0
  84. package/dist/react.d.cts +3 -1
  85. package/dist/react.d.ts +3 -1
  86. package/dist/react.js +2 -1
  87. package/dist/util/concurrency.cjs +28 -0
  88. package/dist/util/concurrency.js +27 -0
  89. package/package.json +55 -14
@@ -0,0 +1,440 @@
1
+ import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
2
+ import { readObjectId } from "../fabric/object-ids.js";
3
+ import { walkObjects } from "../fabric/walk-objects.js";
4
+ import { withSvgOverrides } from "../export/svg/overrides.js";
5
+ import { isTextOnPath, textOnPathToSVG } from "../export/svg/text-on-path.js";
6
+ import { repairFabricSvg, withExportView } from "../export/render-export.js";
7
+ import { embedSvgAssets } from "../export/svg/embed-assets.js";
8
+ import { disposeObjects } from "../export/batch-render.js";
9
+ import { jsPdfStyle, loadFonts, registerFonts, standardFamily } from "./fonts.js";
10
+ import { layoutPage } from "./page-layout.js";
11
+ import { decoratedTextToSVG, hasStraightDecorations } from "./text-decorations.js";
12
+ //#region src/pdf/export-pdf.ts
13
+ const RASTER_REASONS = {
14
+ shadow: "it has a shadow, which PDF vectors cannot draw",
15
+ blend: "it uses a blend mode, which PDF vectors cannot draw",
16
+ gradientStroke: "its outline is a gradient or pattern",
17
+ uniformStroke: "it keeps its outline width while scaled (strokeUniform)",
18
+ font: "its font has no TrueType file in fonts",
19
+ characters: "it uses characters the built-in PDF fonts do not have; add a TrueType font that has them"
20
+ };
21
+ function stop(signal) {
22
+ if (signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The PDF export was cancelled");
23
+ }
24
+ async function loadPeers(needsSvg) {
25
+ let jsPDF;
26
+ try {
27
+ jsPDF = (await import("jspdf")).jsPDF;
28
+ } catch (error) {
29
+ throw new DocumentEngineError("PDF_UNAVAILABLE", "PDF export needs the jspdf package. Install it with: npm install jspdf svg2pdf.js", { cause: error });
30
+ }
31
+ if (needsSvg) try {
32
+ await import("svg2pdf.js");
33
+ } catch (error) {
34
+ throw new DocumentEngineError("PDF_UNAVAILABLE", "Vector and hybrid PDF export need the svg2pdf.js package. Install it with: npm install svg2pdf.js, or use mode: \"raster\".", { cause: error });
35
+ }
36
+ return jsPDF;
37
+ }
38
+ function isEngine(source) {
39
+ return typeof source.toDocument === "function" && "canvas" in source;
40
+ }
41
+ function isCanvas(source) {
42
+ const candidate = source;
43
+ return typeof candidate.getObjects === "function" && typeof candidate.toCanvasElement === "function";
44
+ }
45
+ function idsOf(object) {
46
+ const ids = [];
47
+ walkObjects([object], (child) => {
48
+ const id = readObjectId(child);
49
+ if (id !== void 0) ids.push(id);
50
+ });
51
+ return ids.slice(0, 1);
52
+ }
53
+ function familiesOf(object) {
54
+ const text = object;
55
+ const families = /* @__PURE__ */ new Set();
56
+ if (typeof text.fontFamily === "string") families.add(text.fontFamily);
57
+ for (const line of Object.values(text.styles ?? {})) for (const style of Object.values(line ?? {})) if (typeof style?.fontFamily === "string") families.add(style.fontFamily);
58
+ return [...families];
59
+ }
60
+ function resolvesTo(family, registered) {
61
+ const names = family.split(",").map((name) => name.trim().replace(/^['"]|['"]$/g, ""));
62
+ if (names.some((name) => registered.families.has(name.toLowerCase()))) return {
63
+ registered: true,
64
+ standard: false
65
+ };
66
+ return {
67
+ registered: false,
68
+ standard: names.some((name) => standardFamily(name) !== void 0)
69
+ };
70
+ }
71
+ /** Why an object cannot be drawn as PDF vectors, or undefined when it can. */
72
+ function rasterReason(object, registered, missingFonts) {
73
+ let reason;
74
+ walkObjects([object], (child) => {
75
+ if (reason) return;
76
+ if (child.shadow) reason = "shadow";
77
+ else if (child.globalCompositeOperation && child.globalCompositeOperation !== "source-over") reason = "blend";
78
+ else if (typeof child.stroke === "object" && child.stroke !== null && child.strokeWidth > 0) reason = "gradientStroke";
79
+ else if (child.strokeUniform && child.stroke && child.strokeWidth > 0) {
80
+ const { x: scaleX, y: scaleY } = child.getObjectScaling();
81
+ if (Math.abs(scaleX - 1) > 1e-6 || Math.abs(scaleY - 1) > 1e-6) reason = "uniformStroke";
82
+ }
83
+ const text = child.text;
84
+ if (!reason && typeof text === "string") {
85
+ const families = familiesOf(child).map((family) => resolvesTo(family, registered));
86
+ const allRegistered = families.every((family) => family.registered);
87
+ if (!allRegistered && /[^\u0000-ÿ]/.test(text)) reason = "characters";
88
+ else if (!allRegistered && missingFonts === "rasterize" && families.some((family) => !family.registered && !family.standard)) reason = "font";
89
+ }
90
+ });
91
+ return reason;
92
+ }
93
+ function shadowPadding(object) {
94
+ let padding = 0;
95
+ walkObjects([object], (child) => {
96
+ const shadow = child.shadow;
97
+ if (shadow) padding = Math.max(padding, shadow.blur + Math.max(Math.abs(shadow.offsetX), Math.abs(shadow.offsetY)));
98
+ });
99
+ return padding + 2;
100
+ }
101
+ function usesBlendMode(object) {
102
+ let blends = false;
103
+ walkObjects([object], (child) => {
104
+ if (child.globalCompositeOperation && child.globalCompositeOperation !== "source-over") blends = true;
105
+ });
106
+ return blends;
107
+ }
108
+ /**
109
+ * An object as a picture placed where it sits on the canvas. Usually the
110
+ * object alone, so the vectors underneath show through. An object with a
111
+ * blend mode mixes with what lies below it, so its picture includes
112
+ * everything below it within its bounds, which covers those vectors exactly.
113
+ */
114
+ function rasterMarkup(canvas, object, multiplier) {
115
+ object.setCoords();
116
+ const bounds = object.getBoundingRect();
117
+ const padding = shadowPadding(object);
118
+ const left = Math.max(0, Math.floor(bounds.left - padding));
119
+ const top = Math.max(0, Math.floor(bounds.top - padding));
120
+ const right = Math.min(canvas.getWidth(), Math.ceil(bounds.left + bounds.width + padding));
121
+ const bottom = Math.min(canvas.getHeight(), Math.ceil(bounds.top + bounds.height + padding));
122
+ if (right <= left || bottom <= top) return "";
123
+ const blends = usesBlendMode(object);
124
+ const stack = canvas.getObjects();
125
+ const position = stack.indexOf(object);
126
+ const saved = {
127
+ backgroundColor: canvas.backgroundColor,
128
+ backgroundImage: canvas.backgroundImage,
129
+ overlayColor: canvas.overlayColor,
130
+ overlayImage: canvas.overlayImage
131
+ };
132
+ if (!blends) {
133
+ canvas.backgroundColor = "";
134
+ canvas.backgroundImage = void 0;
135
+ }
136
+ canvas.overlayColor = "";
137
+ canvas.overlayImage = void 0;
138
+ let element;
139
+ try {
140
+ const filter = (candidate) => blends ? stack.indexOf(candidate) <= position : candidate === object;
141
+ element = canvas.toCanvasElement(multiplier, {
142
+ left,
143
+ top,
144
+ width: right - left,
145
+ height: bottom - top,
146
+ filter
147
+ });
148
+ } finally {
149
+ Object.assign(canvas, saved);
150
+ }
151
+ const url = element.toDataURL("image/png");
152
+ element.width = 0;
153
+ element.height = 0;
154
+ return `<image x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" preserveAspectRatio="none" xlink:href="${url}" />\n`;
155
+ }
156
+ function quoteFamily(name) {
157
+ return /^[a-z-]+$/i.test(name) ? name : `"${name.replace(/"/g, "")}"`;
158
+ }
159
+ function resolveFamily(list, registered) {
160
+ const names = list.split(",").map((name) => name.trim().replace(/^['"]|['"]$/g, ""));
161
+ for (const name of names) {
162
+ const own = registered.families.get(name.toLowerCase());
163
+ if (own) return quoteFamily(own);
164
+ }
165
+ for (const name of names) {
166
+ const standard = standardFamily(name);
167
+ if (standard) return standard;
168
+ }
169
+ return "helvetica";
170
+ }
171
+ function weightOf(value) {
172
+ const number = Number(value);
173
+ return value === "bold" || value === "bolder" || Number.isFinite(number) && number >= 600 ? "bold" : "normal";
174
+ }
175
+ /**
176
+ * Points every font at a font jsPDF has, and rounds weights to normal or
177
+ * bold. svg2pdf matches family names exactly and otherwise falls back to
178
+ * Times, and it looks for weights such as 500 as styles no font has.
179
+ */
180
+ function normalizeFonts(root, registered) {
181
+ for (const element of [root, ...Array.from(root.getElementsByTagName("*"))]) {
182
+ const family = element.getAttribute("font-family");
183
+ if (family) element.setAttribute("font-family", resolveFamily(family, registered));
184
+ const weight = element.getAttribute("font-weight");
185
+ if (weight) element.setAttribute("font-weight", weightOf(weight));
186
+ const style = element.getAttribute("style");
187
+ if (style && /font-(family|weight)/.test(style)) element.setAttribute("style", style.replace(/font-family:\s*([^;]+)/g, (_, value) => `font-family: ${resolveFamily(value, registered)}`).replace(/font-weight:\s*([^;]+)/g, (_, value) => `font-weight: ${weightOf(value.trim())}`));
188
+ }
189
+ }
190
+ /** Warns once for each family and style the canvas uses that has no file of its own. */
191
+ function warnSubstitutedStyles(canvas, registered, page, warnings, warned) {
192
+ if (registered.substituted.length === 0) return;
193
+ const missing = new Set(registered.substituted.map(({ family, style }) => `${family.toLowerCase()}|${style}`));
194
+ walkObjects(canvas.getObjects(), (object) => {
195
+ const text = object;
196
+ if (typeof text.text !== "string") return;
197
+ const uses = [{
198
+ family: text.fontFamily,
199
+ weight: text.fontWeight,
200
+ style: text.fontStyle
201
+ }];
202
+ for (const line of Object.values(text.styles ?? {})) for (const style of Object.values(line ?? {})) uses.push({
203
+ family: style?.fontFamily ?? text.fontFamily,
204
+ weight: style?.fontWeight ?? text.fontWeight,
205
+ style: style?.fontStyle ?? text.fontStyle
206
+ });
207
+ for (const use of uses) {
208
+ const family = registered.families.get(String(use.family ?? "").toLowerCase());
209
+ if (!family) continue;
210
+ const style = jsPdfStyle(use.weight, use.style);
211
+ const key = `${family.toLowerCase()}|${style}`;
212
+ if (!missing.has(key) || warned.has(key)) continue;
213
+ warned.add(key);
214
+ warnings.push({
215
+ code: "PDF_FONT_SUBSTITUTED",
216
+ message: `No ${style} file was given for "${family}", so its ${style} text uses another file of the same family`,
217
+ page,
218
+ objectIds: idsOf(object),
219
+ family
220
+ });
221
+ }
222
+ });
223
+ }
224
+ function clipTo(context, draw) {
225
+ const clip = context.layout.clip;
226
+ if (!clip) return draw();
227
+ const { document } = context;
228
+ document.saveGraphicsState();
229
+ document.rect(clip.x, clip.y, clip.width, clip.height, null);
230
+ document.clip();
231
+ document.discardPath();
232
+ const finish = () => {
233
+ document.restoreGraphicsState();
234
+ };
235
+ const drawn = draw();
236
+ if (drawn instanceof Promise) return drawn.finally(finish);
237
+ finish();
238
+ }
239
+ async function drawRasterPage(canvas, context) {
240
+ const { layout, options } = context;
241
+ const multiplier = layout.scale * (options.dpi ?? 300) / 72;
242
+ const element = withExportView(canvas, options.background ?? "keep", "png", () => canvas.toCanvasElement(multiplier));
243
+ try {
244
+ await clipTo(context, () => {
245
+ context.document.addImage(element, "PNG", layout.x, layout.y, layout.width, layout.height, void 0, "FAST");
246
+ });
247
+ } finally {
248
+ element.width = 0;
249
+ element.height = 0;
250
+ }
251
+ }
252
+ async function drawVectorPage(canvas, context) {
253
+ const { layout, options, page, warnings, registered } = context;
254
+ const mode = options.mode ?? "hybrid";
255
+ const missingFonts = options.missingFonts ?? (mode === "hybrid" ? "rasterize" : "substitute");
256
+ const multiplier = layout.scale * (options.dpi ?? 300) / 72;
257
+ const width = canvas.getWidth();
258
+ const height = canvas.getHeight();
259
+ const svg = withExportView(canvas, options.background ?? "keep", "svg", () => {
260
+ const rasters = /* @__PURE__ */ new Map();
261
+ for (const object of canvas.getObjects()) {
262
+ const reason = rasterReason(object, registered, missingFonts);
263
+ if (!reason) continue;
264
+ if (mode === "hybrid") {
265
+ rasters.set(object, rasterMarkup(canvas, object, multiplier));
266
+ warnings.push({
267
+ code: "PDF_RASTERIZED",
268
+ message: `An object was drawn as a picture at ${options.dpi ?? 300} dpi because ${RASTER_REASONS[reason]}`,
269
+ page,
270
+ objectIds: idsOf(object)
271
+ });
272
+ } else warnings.push({
273
+ code: reason === "font" || reason === "characters" ? "PDF_FONT_SUBSTITUTED" : "PDF_UNSUPPORTED",
274
+ message: `An object may look different in the PDF because ${RASTER_REASONS[reason]}`,
275
+ page,
276
+ objectIds: idsOf(object)
277
+ });
278
+ }
279
+ const writerFor = (object) => {
280
+ const raster = rasters.get(object);
281
+ if (raster !== void 0) return () => raster;
282
+ if (isTextOnPath(object)) return (reviver) => textOnPathToSVG(object, reviver);
283
+ if (hasStraightDecorations(object)) return (reviver) => decoratedTextToSVG(object, reviver);
284
+ };
285
+ return withSvgOverrides(canvas, writerFor, () => canvas.toSVG({
286
+ viewBox: {
287
+ x: 0,
288
+ y: 0,
289
+ width,
290
+ height
291
+ },
292
+ width: String(width),
293
+ height: String(height)
294
+ })).result;
295
+ });
296
+ const embedded = await embedSvgAssets(repairFabricSvg(svg), canvas, {
297
+ textOnPath: "vector",
298
+ embedImages: true,
299
+ maxEmbeddedImageBytes: Number.MAX_SAFE_INTEGER,
300
+ embedFonts: {}
301
+ }, options.signal);
302
+ stop(options.signal);
303
+ for (const warning of embedded.warnings) if (warning.code === "IMAGE_NOT_EMBEDDED") warnings.push({
304
+ code: "IMAGE_NOT_EMBEDDED",
305
+ message: warning.message,
306
+ page,
307
+ objectIds: warning.objectIds,
308
+ url: warning.url
309
+ });
310
+ const root = new DOMParser().parseFromString(embedded.svg, "image/svg+xml").documentElement;
311
+ normalizeFonts(root, registered);
312
+ await clipTo(context, async () => {
313
+ await context.document.svg(root, {
314
+ x: layout.x,
315
+ y: layout.y,
316
+ width: layout.width,
317
+ height: layout.height
318
+ });
319
+ });
320
+ }
321
+ /**
322
+ * Exports one or more pages as a PDF. Each source becomes one page: an
323
+ * engine's canvas, a Fabric canvas, or a saved document, which is drawn on
324
+ * an off-screen canvas that is freed after its page.
325
+ */
326
+ async function exportPdf(sources, options = {}) {
327
+ const list = Array.isArray(sources) ? [...sources] : [sources];
328
+ if (list.length === 0) throw new DocumentEngineError("INVALID_EXPORT_OPTIONS", "exportPdf needs at least one page");
329
+ const dpi = options.dpi ?? 300;
330
+ if (!(Number.isFinite(dpi) && dpi > 0 && dpi <= 1200)) throw new DocumentEngineError("INVALID_EXPORT_OPTIONS", "dpi must be between 1 and 1200");
331
+ const mode = options.mode ?? "hybrid";
332
+ if (![
333
+ "vector",
334
+ "raster",
335
+ "hybrid"
336
+ ].includes(mode)) throw new DocumentEngineError("INVALID_EXPORT_OPTIONS", "mode must be \"vector\", \"raster\" or \"hybrid\"");
337
+ if (typeof document === "undefined") throw new DocumentEngineError("PDF_UNAVAILABLE", "PDF export needs a browser DOM");
338
+ stop(options.signal);
339
+ const jsPDF = await loadPeers(mode !== "raster");
340
+ const fonts = await loadFonts(options.fonts ?? [], options.signal);
341
+ stop(options.signal);
342
+ let pdf;
343
+ let registered = {
344
+ families: /* @__PURE__ */ new Map(),
345
+ substituted: []
346
+ };
347
+ const warnings = [];
348
+ const warnedStyles = /* @__PURE__ */ new Set();
349
+ let renderer;
350
+ try {
351
+ for (let index = 0; index < list.length; index += 1) {
352
+ stop(options.signal);
353
+ const source = list[index];
354
+ let canvas;
355
+ if (isEngine(source)) canvas = source.canvas;
356
+ else if (isCanvas(source)) canvas = source;
357
+ else {
358
+ renderer ?? (renderer = await createRenderer(options));
359
+ await renderer.engine.loadDocument(source, {
360
+ signal: options.signal,
361
+ discardUnsavedChanges: true
362
+ });
363
+ canvas = renderer.canvas;
364
+ }
365
+ const layout = layoutPage({
366
+ width: canvas.getWidth(),
367
+ height: canvas.getHeight()
368
+ }, options);
369
+ const format = [layout.pageWidth, layout.pageHeight];
370
+ if (!pdf) {
371
+ pdf = new jsPDF({
372
+ unit: "pt",
373
+ format,
374
+ orientation: layout.orientation,
375
+ compress: true,
376
+ putOnlyUsedFonts: true
377
+ });
378
+ registered = registerFonts(pdf, fonts);
379
+ } else pdf.addPage(format, layout.orientation);
380
+ warnSubstitutedStyles(canvas, registered, index + 1, warnings, warnedStyles);
381
+ const context = {
382
+ document: pdf,
383
+ layout,
384
+ options,
385
+ registered,
386
+ page: index + 1,
387
+ warnings
388
+ };
389
+ if (mode === "raster") await drawRasterPage(canvas, context);
390
+ else await drawVectorPage(canvas, context);
391
+ if (renderer && canvas === renderer.canvas) disposeObjects(renderer.canvas);
392
+ }
393
+ stop(options.signal);
394
+ const { metadata } = options;
395
+ pdf.setProperties({
396
+ creator: metadata?.creator ?? "fabricjs-document-engine",
397
+ ...metadata
398
+ });
399
+ return {
400
+ blob: pdf.output("blob"),
401
+ pageCount: list.length,
402
+ warnings
403
+ };
404
+ } catch (error) {
405
+ if (isDocumentEngineError(error)) throw error;
406
+ if (options.signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The PDF export was cancelled", { cause: error });
407
+ const reason = error instanceof Error ? error.message : String(error);
408
+ throw new DocumentEngineError("PDF_FAILED", `The PDF could not be made: ${reason}`, { cause: error });
409
+ } finally {
410
+ await renderer?.release();
411
+ }
412
+ }
413
+ async function createRenderer(options) {
414
+ const [{ StaticCanvas }, { createDocumentEngine }] = await Promise.all([import("fabric"), import("../engine/create-document-engine.js")]);
415
+ const element = document.createElement("canvas");
416
+ const canvas = new StaticCanvas(element, {
417
+ renderOnAddRemove: false,
418
+ enableRetinaScaling: false
419
+ });
420
+ const engine = createDocumentEngine({
421
+ canvas,
422
+ customObjects: options.customObjects,
423
+ assets: options.assets,
424
+ limits: options.limits,
425
+ history: { limit: 1 }
426
+ });
427
+ return {
428
+ canvas,
429
+ engine,
430
+ release: async () => {
431
+ disposeObjects(canvas);
432
+ engine.destroy();
433
+ await canvas.dispose();
434
+ element.width = 0;
435
+ element.height = 0;
436
+ }
437
+ };
438
+ }
439
+ //#endregion
440
+ export { exportPdf };
@@ -0,0 +1,115 @@
1
+ const require_errors = require("../engine/errors.cjs");
2
+ //#region src/pdf/fonts.ts
3
+ const STANDARD = {
4
+ helvetica: "helvetica",
5
+ "helvetica neue": "helvetica",
6
+ arial: "helvetica",
7
+ verdana: "helvetica",
8
+ "sans-serif": "helvetica",
9
+ "system-ui": "helvetica",
10
+ times: "times",
11
+ "times new roman": "times",
12
+ georgia: "times",
13
+ serif: "times",
14
+ courier: "courier",
15
+ "courier new": "courier",
16
+ monospace: "courier"
17
+ };
18
+ /** Families the built-in PDF fonts can stand in for. They cover Latin-1 letters only. */
19
+ function standardFamily(family) {
20
+ return STANDARD[family.trim().replace(/^['"]|['"]$/g, "").toLowerCase()];
21
+ }
22
+ function isBold(weight) {
23
+ if (weight === "bold" || weight === "bolder") return true;
24
+ const number = Number(weight);
25
+ return Number.isFinite(number) && number >= 600;
26
+ }
27
+ function jsPdfStyle(weight, style) {
28
+ const bold = isBold(weight);
29
+ const italic = style === "italic" || style === "oblique";
30
+ return bold && italic ? "bolditalic" : bold ? "bold" : italic ? "italic" : "normal";
31
+ }
32
+ async function bytesOf(source, signal) {
33
+ if (typeof source === "string") {
34
+ const response = await fetch(source, { signal });
35
+ if (!response.ok) throw new Error(`HTTP ${response.status} for ${source}`);
36
+ return new Uint8Array(await response.arrayBuffer());
37
+ }
38
+ if (source instanceof Uint8Array) return source;
39
+ if (source instanceof ArrayBuffer) return new Uint8Array(source);
40
+ return new Uint8Array(await source.arrayBuffer());
41
+ }
42
+ function binaryString(bytes) {
43
+ let binary = "";
44
+ const chunk = 32768;
45
+ for (let index = 0; index < bytes.length; index += chunk) binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
46
+ return binary;
47
+ }
48
+ function refuseFormat(family, bytes) {
49
+ const tag = String.fromCharCode(...bytes.subarray(0, 4));
50
+ const kind = tag === "wOF2" ? "WOFF2" : tag === "wOFF" ? "WOFF" : tag === "OTTO" ? "OpenType with CFF outlines" : void 0;
51
+ if (kind) throw new require_errors.DocumentEngineError("PDF_FAILED", `The font "${family}" is ${kind}. PDF export needs a TrueType (.ttf) file, which most font sites offer next to the web formats.`);
52
+ }
53
+ /** Downloads every font once, so each PDF document can register them without fetching again. */
54
+ async function loadFonts(fonts, signal) {
55
+ return Promise.all(fonts.map(async (font) => {
56
+ let bytes;
57
+ try {
58
+ bytes = await bytesOf(font.source, signal);
59
+ } catch (error) {
60
+ if (signal?.aborted) throw error;
61
+ const reason = error instanceof Error ? error.message : String(error);
62
+ throw new require_errors.DocumentEngineError("PDF_FAILED", `The font "${font.family}" could not be read: ${reason}`, { cause: error });
63
+ }
64
+ refuseFormat(font.family, bytes);
65
+ return {
66
+ family: font.family,
67
+ style: jsPdfStyle(font.weight ?? "normal", font.style ?? "normal"),
68
+ binary: binaryString(bytes)
69
+ };
70
+ }));
71
+ }
72
+ /**
73
+ * Adds the fonts to a jsPDF document. A style with no file of its own uses
74
+ * the closest one given, so bold text in a family with only a regular file
75
+ * stays in that family instead of falling back to Times.
76
+ */
77
+ function registerFonts(document, fonts) {
78
+ const families = /* @__PURE__ */ new Map();
79
+ const byFamily = /* @__PURE__ */ new Map();
80
+ fonts.forEach((font) => {
81
+ const styles = byFamily.get(font.family) ?? /* @__PURE__ */ new Map();
82
+ styles.set(font.style, font);
83
+ byFamily.set(font.family, styles);
84
+ });
85
+ const substituted = [];
86
+ let fileNumber = 0;
87
+ for (const [family, styles] of byFamily) {
88
+ families.set(family.toLowerCase(), family);
89
+ for (const style of [
90
+ "normal",
91
+ "bold",
92
+ "italic",
93
+ "bolditalic"
94
+ ]) {
95
+ const own = styles.get(style);
96
+ const font = own ?? styles.get(style === "bolditalic" ? "bold" : "normal") ?? styles.get("normal") ?? styles.get("bold") ?? styles.get("italic") ?? styles.get("bolditalic");
97
+ if (!own) substituted.push({
98
+ family,
99
+ style
100
+ });
101
+ const fileName = `font-${fileNumber++}.ttf`;
102
+ document.addFileToVFS(fileName, font.binary);
103
+ document.addFont(fileName, family, style);
104
+ }
105
+ }
106
+ return {
107
+ families,
108
+ substituted
109
+ };
110
+ }
111
+ //#endregion
112
+ exports.jsPdfStyle = jsPdfStyle;
113
+ exports.loadFonts = loadFonts;
114
+ exports.registerFonts = registerFonts;
115
+ exports.standardFamily = standardFamily;
@@ -0,0 +1,12 @@
1
+ //#region src/pdf/fonts.d.ts
2
+ export type PdfFontStyle = "normal" | "italic";
3
+ export interface PdfFont {
4
+ /** The font family exactly as the canvas uses it, such as `Inter`. */
5
+ family: string;
6
+ /** A TrueType (.ttf) file: its URL, or its bytes. */
7
+ source: string | ArrayBuffer | Uint8Array | Blob;
8
+ /** `normal` or `bold`, or a number such as 700. Weights from 600 count as bold. Default `normal`. */
9
+ weight?: "normal" | "bold" | number;
10
+ style?: PdfFontStyle;
11
+ }
12
+ //#endregion
@@ -0,0 +1,12 @@
1
+ //#region src/pdf/fonts.d.ts
2
+ export type PdfFontStyle = "normal" | "italic";
3
+ export interface PdfFont {
4
+ /** The font family exactly as the canvas uses it, such as `Inter`. */
5
+ family: string;
6
+ /** A TrueType (.ttf) file: its URL, or its bytes. */
7
+ source: string | ArrayBuffer | Uint8Array | Blob;
8
+ /** `normal` or `bold`, or a number such as 700. Weights from 600 count as bold. Default `normal`. */
9
+ weight?: "normal" | "bold" | number;
10
+ style?: PdfFontStyle;
11
+ }
12
+ //#endregion
@@ -0,0 +1,112 @@
1
+ import { DocumentEngineError } from "../engine/errors.js";
2
+ //#region src/pdf/fonts.ts
3
+ const STANDARD = {
4
+ helvetica: "helvetica",
5
+ "helvetica neue": "helvetica",
6
+ arial: "helvetica",
7
+ verdana: "helvetica",
8
+ "sans-serif": "helvetica",
9
+ "system-ui": "helvetica",
10
+ times: "times",
11
+ "times new roman": "times",
12
+ georgia: "times",
13
+ serif: "times",
14
+ courier: "courier",
15
+ "courier new": "courier",
16
+ monospace: "courier"
17
+ };
18
+ /** Families the built-in PDF fonts can stand in for. They cover Latin-1 letters only. */
19
+ function standardFamily(family) {
20
+ return STANDARD[family.trim().replace(/^['"]|['"]$/g, "").toLowerCase()];
21
+ }
22
+ function isBold(weight) {
23
+ if (weight === "bold" || weight === "bolder") return true;
24
+ const number = Number(weight);
25
+ return Number.isFinite(number) && number >= 600;
26
+ }
27
+ function jsPdfStyle(weight, style) {
28
+ const bold = isBold(weight);
29
+ const italic = style === "italic" || style === "oblique";
30
+ return bold && italic ? "bolditalic" : bold ? "bold" : italic ? "italic" : "normal";
31
+ }
32
+ async function bytesOf(source, signal) {
33
+ if (typeof source === "string") {
34
+ const response = await fetch(source, { signal });
35
+ if (!response.ok) throw new Error(`HTTP ${response.status} for ${source}`);
36
+ return new Uint8Array(await response.arrayBuffer());
37
+ }
38
+ if (source instanceof Uint8Array) return source;
39
+ if (source instanceof ArrayBuffer) return new Uint8Array(source);
40
+ return new Uint8Array(await source.arrayBuffer());
41
+ }
42
+ function binaryString(bytes) {
43
+ let binary = "";
44
+ const chunk = 32768;
45
+ for (let index = 0; index < bytes.length; index += chunk) binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
46
+ return binary;
47
+ }
48
+ function refuseFormat(family, bytes) {
49
+ const tag = String.fromCharCode(...bytes.subarray(0, 4));
50
+ const kind = tag === "wOF2" ? "WOFF2" : tag === "wOFF" ? "WOFF" : tag === "OTTO" ? "OpenType with CFF outlines" : void 0;
51
+ if (kind) throw new DocumentEngineError("PDF_FAILED", `The font "${family}" is ${kind}. PDF export needs a TrueType (.ttf) file, which most font sites offer next to the web formats.`);
52
+ }
53
+ /** Downloads every font once, so each PDF document can register them without fetching again. */
54
+ async function loadFonts(fonts, signal) {
55
+ return Promise.all(fonts.map(async (font) => {
56
+ let bytes;
57
+ try {
58
+ bytes = await bytesOf(font.source, signal);
59
+ } catch (error) {
60
+ if (signal?.aborted) throw error;
61
+ const reason = error instanceof Error ? error.message : String(error);
62
+ throw new DocumentEngineError("PDF_FAILED", `The font "${font.family}" could not be read: ${reason}`, { cause: error });
63
+ }
64
+ refuseFormat(font.family, bytes);
65
+ return {
66
+ family: font.family,
67
+ style: jsPdfStyle(font.weight ?? "normal", font.style ?? "normal"),
68
+ binary: binaryString(bytes)
69
+ };
70
+ }));
71
+ }
72
+ /**
73
+ * Adds the fonts to a jsPDF document. A style with no file of its own uses
74
+ * the closest one given, so bold text in a family with only a regular file
75
+ * stays in that family instead of falling back to Times.
76
+ */
77
+ function registerFonts(document, fonts) {
78
+ const families = /* @__PURE__ */ new Map();
79
+ const byFamily = /* @__PURE__ */ new Map();
80
+ fonts.forEach((font) => {
81
+ const styles = byFamily.get(font.family) ?? /* @__PURE__ */ new Map();
82
+ styles.set(font.style, font);
83
+ byFamily.set(font.family, styles);
84
+ });
85
+ const substituted = [];
86
+ let fileNumber = 0;
87
+ for (const [family, styles] of byFamily) {
88
+ families.set(family.toLowerCase(), family);
89
+ for (const style of [
90
+ "normal",
91
+ "bold",
92
+ "italic",
93
+ "bolditalic"
94
+ ]) {
95
+ const own = styles.get(style);
96
+ const font = own ?? styles.get(style === "bolditalic" ? "bold" : "normal") ?? styles.get("normal") ?? styles.get("bold") ?? styles.get("italic") ?? styles.get("bolditalic");
97
+ if (!own) substituted.push({
98
+ family,
99
+ style
100
+ });
101
+ const fileName = `font-${fileNumber++}.ttf`;
102
+ document.addFileToVFS(fileName, font.binary);
103
+ document.addFont(fileName, family, style);
104
+ }
105
+ }
106
+ return {
107
+ families,
108
+ substituted
109
+ };
110
+ }
111
+ //#endregion
112
+ export { jsPdfStyle, loadFonts, registerFonts, standardFamily };