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.
- package/README.md +277 -36
- package/README.zh-CN.md +697 -0
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +22 -10
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +22 -10
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +88 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +88 -7
- package/dist/commands/clipboard.cjs +136 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +135 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -0
- package/dist/engine/create-document-engine.cjs +85 -14
- package/dist/engine/create-document-engine.d.cts +20 -1
- package/dist/engine/create-document-engine.d.ts +20 -1
- package/dist/engine/create-document-engine.js +86 -15
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +18 -1
- package/dist/export/export-options.d.cts +23 -0
- package/dist/export/export-options.d.ts +23 -0
- package/dist/export/export-options.js +18 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +18 -3
- package/dist/export/render-export.js +18 -4
- package/dist/export/svg/embed-assets.cjs +249 -0
- package/dist/export/svg/embed-assets.js +249 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/text-on-path.cjs +252 -0
- package/dist/export/svg/text-on-path.js +249 -0
- package/dist/fabric/fabric-adapter.cjs +46 -5
- package/dist/fabric/fabric-adapter.js +46 -5
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -1
- package/dist/pdf/export-pdf.cjs +440 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +440 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf/text-decorations.cjs +108 -0
- package/dist/pdf/text-decorations.js +107 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/util/concurrency.cjs +28 -0
- package/dist/util/concurrency.js +27 -0
- 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 };
|