fabricjs-document-engine 1.0.2 → 1.2.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 +220 -11
- package/README.zh-CN.md +220 -11
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +31 -17
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +31 -17
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +89 -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 +89 -7
- package/dist/commands/clipboard.cjs +141 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +140 -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/document/document-format.d.cts +15 -0
- package/dist/document/document-format.d.ts +15 -0
- package/dist/document/validate-document.cjs +8 -0
- package/dist/document/validate-document.js +8 -0
- package/dist/engine/create-document-engine.cjs +196 -31
- package/dist/engine/create-document-engine.d.cts +44 -7
- package/dist/engine/create-document-engine.d.ts +44 -7
- package/dist/engine/create-document-engine.js +197 -32
- 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 +21 -1
- package/dist/export/export-options.d.cts +29 -0
- package/dist/export/export-options.d.ts +29 -0
- package/dist/export/export-options.js +21 -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 +57 -4
- package/dist/export/render-export.js +57 -5
- package/dist/export/svg/clip-masks.cjs +39 -0
- package/dist/export/svg/clip-masks.js +37 -0
- package/dist/export/svg/embed-assets.cjs +250 -0
- package/dist/export/svg/embed-assets.js +250 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/raster-object.cjs +82 -0
- package/dist/export/svg/raster-object.js +81 -0
- package/dist/export/svg/text-decorations.cjs +108 -0
- package/dist/export/svg/text-decorations.js +107 -0
- package/dist/export/svg/text-on-path.cjs +242 -0
- package/dist/export/svg/text-on-path.js +240 -0
- package/dist/fabric/fabric-adapter.cjs +49 -6
- package/dist/fabric/fabric-adapter.js +49 -6
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/fabric/page-state.cjs +69 -0
- package/dist/fabric/page-state.js +64 -0
- package/dist/history/apply-state.cjs +19 -2
- package/dist/history/apply-state.js +19 -2
- package/dist/history/create-history.cjs +53 -13
- package/dist/history/create-history.d.cts +8 -0
- package/dist/history/create-history.d.ts +8 -0
- package/dist/history/create-history.js +53 -13
- package/dist/history/snapshot.cjs +10 -1
- package/dist/history/snapshot.js +10 -2
- 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 +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +4 -1
- package/dist/migrations/migrate-document.cjs +2 -1
- package/dist/migrations/migrate-document.js +2 -1
- package/dist/pdf/export-pdf.cjs +381 -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 +381 -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.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/recovery/recovery-controller.cjs +148 -29
- package/dist/recovery/recovery-controller.d.cts +8 -0
- package/dist/recovery/recovery-controller.d.ts +8 -0
- package/dist/recovery/recovery-controller.js +148 -29
- package/dist/security/content-limits.cjs +43 -3
- package/dist/security/content-limits.d.cts +8 -0
- package/dist/security/content-limits.d.ts +8 -0
- package/dist/security/content-limits.js +39 -4
- package/dist/util/concurrency.cjs +41 -0
- package/dist/util/concurrency.js +40 -0
- package/package.json +30 -3
- package/schema/document-v1.schema.json +5 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { walkObjects } from "../../fabric/walk-objects.js";
|
|
2
|
+
//#region src/export/svg/overrides.ts
|
|
3
|
+
/**
|
|
4
|
+
* Runs `work` while chosen objects on the canvas, including objects in
|
|
5
|
+
* groups, write their SVG with `writerFor`. Any toSVG an app set on an
|
|
6
|
+
* object itself is put back afterwards. Returns how many objects were
|
|
7
|
+
* handled.
|
|
8
|
+
*/
|
|
9
|
+
function withSvgOverrides(canvas, writerFor, work) {
|
|
10
|
+
const patched = [];
|
|
11
|
+
walkObjects(canvas.getObjects(), (object) => {
|
|
12
|
+
const writer = writerFor(object);
|
|
13
|
+
if (!writer) return;
|
|
14
|
+
patched.push([object, Object.getOwnPropertyDescriptor(object, "toSVG")]);
|
|
15
|
+
Object.defineProperty(object, "toSVG", {
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: writer
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
return {
|
|
23
|
+
result: work(),
|
|
24
|
+
count: patched.length
|
|
25
|
+
};
|
|
26
|
+
} finally {
|
|
27
|
+
for (const [object, own] of patched) if (own) Object.defineProperty(object, "toSVG", own);
|
|
28
|
+
else delete object.toSVG;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { withSvgOverrides };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require("../../security/content-limits.cjs");
|
|
2
|
+
const require_walk_objects = require("../../fabric/walk-objects.cjs");
|
|
3
|
+
//#region src/export/svg/raster-object.ts
|
|
4
|
+
/**
|
|
5
|
+
* The largest multiplier that keeps a `width × height` area within the
|
|
6
|
+
* canvas limits. A picture over the browser's limit would fail, so a large
|
|
7
|
+
* page is drawn at a lower resolution instead: O(1) arithmetic.
|
|
8
|
+
*/
|
|
9
|
+
function safeMultiplier(multiplier, width, height, limits) {
|
|
10
|
+
const maxPixels = limits?.maxCanvasPixels ?? 67108864;
|
|
11
|
+
const maxSide = limits?.maxCanvasSide ?? 16384;
|
|
12
|
+
const byArea = Math.sqrt(maxPixels / Math.max(1, width * height));
|
|
13
|
+
const bySide = maxSide / Math.max(1, width, height);
|
|
14
|
+
return Math.min(multiplier, byArea, bySide);
|
|
15
|
+
}
|
|
16
|
+
function shadowPadding(object) {
|
|
17
|
+
let padding = 0;
|
|
18
|
+
require_walk_objects.walkObjects([object], (child) => {
|
|
19
|
+
const shadow = child.shadow;
|
|
20
|
+
if (shadow) padding = Math.max(padding, shadow.blur + Math.max(Math.abs(shadow.offsetX), Math.abs(shadow.offsetY)));
|
|
21
|
+
});
|
|
22
|
+
return padding + 2;
|
|
23
|
+
}
|
|
24
|
+
function usesBlendMode(object) {
|
|
25
|
+
let blends = false;
|
|
26
|
+
require_walk_objects.walkObjects([object], (child) => {
|
|
27
|
+
if (child.globalCompositeOperation && child.globalCompositeOperation !== "source-over") blends = true;
|
|
28
|
+
});
|
|
29
|
+
return blends;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* An object as a picture placed where it sits on the canvas. Usually the
|
|
33
|
+
* object alone, so the vectors underneath show through. An object with a
|
|
34
|
+
* blend mode mixes with what lies below it, so its picture includes
|
|
35
|
+
* everything below it within its bounds, which covers those vectors exactly.
|
|
36
|
+
*/
|
|
37
|
+
function rasterMarkup(canvas, object, multiplier, limits) {
|
|
38
|
+
object.setCoords();
|
|
39
|
+
const bounds = object.getBoundingRect();
|
|
40
|
+
const padding = shadowPadding(object);
|
|
41
|
+
const left = Math.max(0, Math.floor(bounds.left - padding));
|
|
42
|
+
const top = Math.max(0, Math.floor(bounds.top - padding));
|
|
43
|
+
const right = Math.min(canvas.getWidth(), Math.ceil(bounds.left + bounds.width + padding));
|
|
44
|
+
const bottom = Math.min(canvas.getHeight(), Math.ceil(bounds.top + bounds.height + padding));
|
|
45
|
+
if (right <= left || bottom <= top) return "";
|
|
46
|
+
const blends = usesBlendMode(object);
|
|
47
|
+
const stack = canvas.getObjects();
|
|
48
|
+
const position = stack.indexOf(object);
|
|
49
|
+
const saved = {
|
|
50
|
+
backgroundColor: canvas.backgroundColor,
|
|
51
|
+
backgroundImage: canvas.backgroundImage,
|
|
52
|
+
overlayColor: canvas.overlayColor,
|
|
53
|
+
overlayImage: canvas.overlayImage
|
|
54
|
+
};
|
|
55
|
+
if (!blends) {
|
|
56
|
+
canvas.backgroundColor = "";
|
|
57
|
+
canvas.backgroundImage = void 0;
|
|
58
|
+
}
|
|
59
|
+
canvas.overlayColor = "";
|
|
60
|
+
canvas.overlayImage = void 0;
|
|
61
|
+
let element;
|
|
62
|
+
try {
|
|
63
|
+
const filter = (candidate) => blends ? stack.indexOf(candidate) <= position : candidate === object;
|
|
64
|
+
const safe = safeMultiplier(multiplier, right - left, bottom - top, limits);
|
|
65
|
+
element = canvas.toCanvasElement(safe, {
|
|
66
|
+
left,
|
|
67
|
+
top,
|
|
68
|
+
width: right - left,
|
|
69
|
+
height: bottom - top,
|
|
70
|
+
filter
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
Object.assign(canvas, saved);
|
|
74
|
+
}
|
|
75
|
+
const url = element.toDataURL("image/png");
|
|
76
|
+
element.width = 0;
|
|
77
|
+
element.height = 0;
|
|
78
|
+
return `<image x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" preserveAspectRatio="none" xlink:href="${url}" />\n`;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
exports.rasterMarkup = rasterMarkup;
|
|
82
|
+
exports.safeMultiplier = safeMultiplier;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import "../../security/content-limits.js";
|
|
2
|
+
import { walkObjects } from "../../fabric/walk-objects.js";
|
|
3
|
+
//#region src/export/svg/raster-object.ts
|
|
4
|
+
/**
|
|
5
|
+
* The largest multiplier that keeps a `width × height` area within the
|
|
6
|
+
* canvas limits. A picture over the browser's limit would fail, so a large
|
|
7
|
+
* page is drawn at a lower resolution instead: O(1) arithmetic.
|
|
8
|
+
*/
|
|
9
|
+
function safeMultiplier(multiplier, width, height, limits) {
|
|
10
|
+
const maxPixels = limits?.maxCanvasPixels ?? 67108864;
|
|
11
|
+
const maxSide = limits?.maxCanvasSide ?? 16384;
|
|
12
|
+
const byArea = Math.sqrt(maxPixels / Math.max(1, width * height));
|
|
13
|
+
const bySide = maxSide / Math.max(1, width, height);
|
|
14
|
+
return Math.min(multiplier, byArea, bySide);
|
|
15
|
+
}
|
|
16
|
+
function shadowPadding(object) {
|
|
17
|
+
let padding = 0;
|
|
18
|
+
walkObjects([object], (child) => {
|
|
19
|
+
const shadow = child.shadow;
|
|
20
|
+
if (shadow) padding = Math.max(padding, shadow.blur + Math.max(Math.abs(shadow.offsetX), Math.abs(shadow.offsetY)));
|
|
21
|
+
});
|
|
22
|
+
return padding + 2;
|
|
23
|
+
}
|
|
24
|
+
function usesBlendMode(object) {
|
|
25
|
+
let blends = false;
|
|
26
|
+
walkObjects([object], (child) => {
|
|
27
|
+
if (child.globalCompositeOperation && child.globalCompositeOperation !== "source-over") blends = true;
|
|
28
|
+
});
|
|
29
|
+
return blends;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* An object as a picture placed where it sits on the canvas. Usually the
|
|
33
|
+
* object alone, so the vectors underneath show through. An object with a
|
|
34
|
+
* blend mode mixes with what lies below it, so its picture includes
|
|
35
|
+
* everything below it within its bounds, which covers those vectors exactly.
|
|
36
|
+
*/
|
|
37
|
+
function rasterMarkup(canvas, object, multiplier, limits) {
|
|
38
|
+
object.setCoords();
|
|
39
|
+
const bounds = object.getBoundingRect();
|
|
40
|
+
const padding = shadowPadding(object);
|
|
41
|
+
const left = Math.max(0, Math.floor(bounds.left - padding));
|
|
42
|
+
const top = Math.max(0, Math.floor(bounds.top - padding));
|
|
43
|
+
const right = Math.min(canvas.getWidth(), Math.ceil(bounds.left + bounds.width + padding));
|
|
44
|
+
const bottom = Math.min(canvas.getHeight(), Math.ceil(bounds.top + bounds.height + padding));
|
|
45
|
+
if (right <= left || bottom <= top) return "";
|
|
46
|
+
const blends = usesBlendMode(object);
|
|
47
|
+
const stack = canvas.getObjects();
|
|
48
|
+
const position = stack.indexOf(object);
|
|
49
|
+
const saved = {
|
|
50
|
+
backgroundColor: canvas.backgroundColor,
|
|
51
|
+
backgroundImage: canvas.backgroundImage,
|
|
52
|
+
overlayColor: canvas.overlayColor,
|
|
53
|
+
overlayImage: canvas.overlayImage
|
|
54
|
+
};
|
|
55
|
+
if (!blends) {
|
|
56
|
+
canvas.backgroundColor = "";
|
|
57
|
+
canvas.backgroundImage = void 0;
|
|
58
|
+
}
|
|
59
|
+
canvas.overlayColor = "";
|
|
60
|
+
canvas.overlayImage = void 0;
|
|
61
|
+
let element;
|
|
62
|
+
try {
|
|
63
|
+
const filter = (candidate) => blends ? stack.indexOf(candidate) <= position : candidate === object;
|
|
64
|
+
const safe = safeMultiplier(multiplier, right - left, bottom - top, limits);
|
|
65
|
+
element = canvas.toCanvasElement(safe, {
|
|
66
|
+
left,
|
|
67
|
+
top,
|
|
68
|
+
width: right - left,
|
|
69
|
+
height: bottom - top,
|
|
70
|
+
filter
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
Object.assign(canvas, saved);
|
|
74
|
+
}
|
|
75
|
+
const url = element.toDataURL("image/png");
|
|
76
|
+
element.width = 0;
|
|
77
|
+
element.height = 0;
|
|
78
|
+
return `<image x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" preserveAspectRatio="none" xlink:href="${url}" />\n`;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { rasterMarkup, safeMultiplier };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//#region src/export/svg/text-decorations.ts
|
|
2
|
+
const DECORATIONS = [
|
|
3
|
+
"underline",
|
|
4
|
+
"overline",
|
|
5
|
+
"linethrough"
|
|
6
|
+
];
|
|
7
|
+
function num(value) {
|
|
8
|
+
return Number.isFinite(value) ? String(Math.round(value * 1e4) / 1e4) : "0";
|
|
9
|
+
}
|
|
10
|
+
function escapeAttribute(value) {
|
|
11
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
12
|
+
}
|
|
13
|
+
function isText(object) {
|
|
14
|
+
const candidate = object;
|
|
15
|
+
return Array.isArray(candidate._textLines) && typeof candidate.getValueOfPropertyAt === "function" && !candidate.path;
|
|
16
|
+
}
|
|
17
|
+
function hasStraightDecorations(object) {
|
|
18
|
+
if (!isText(object)) return false;
|
|
19
|
+
return DECORATIONS.some((type) => object[type] || object.styleHas(type)) || object.styleHas("deltaY");
|
|
20
|
+
}
|
|
21
|
+
function rect(x, y, width, height, color) {
|
|
22
|
+
if (typeof color !== "string" || width <= 0 || height <= 0) return "";
|
|
23
|
+
return `\t\t<rect x="${num(x)}" y="${num(y)}" width="${num(width)}" height="${num(height)}" fill="${escapeAttribute(color)}" />\n`;
|
|
24
|
+
}
|
|
25
|
+
/** The same boxes as Fabric's `_renderTextDecoration` for text that is not on a path. */
|
|
26
|
+
function decorationRects(text, type) {
|
|
27
|
+
if (!text[type] && !text.styleHas(type)) return "";
|
|
28
|
+
let markup = "";
|
|
29
|
+
let topOffset = text._getTopOffset();
|
|
30
|
+
const leftOffset = text._getLeftOffset();
|
|
31
|
+
const charSpacing = text._getWidthOfCharSpacing();
|
|
32
|
+
const aligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0;
|
|
33
|
+
const offsetY = text.offsets[type];
|
|
34
|
+
const at = (line, char, property) => text.getValueOfPropertyAt(line, char, property);
|
|
35
|
+
const colorAt = (line, char) => at(line, char, "textDecorationColor") || at(line, char, "fill");
|
|
36
|
+
const thicknessAt = (line, char) => Number(at(line, char, "textDecorationThickness") ?? 66.667);
|
|
37
|
+
for (let lineIndex = 0; lineIndex < text._textLines.length; lineIndex += 1) {
|
|
38
|
+
const heightOfLine = text.getHeightOfLine(lineIndex);
|
|
39
|
+
if (!text[type] && !text.styleHas(type, lineIndex)) {
|
|
40
|
+
topOffset += heightOfLine;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!text.__charBounds[lineIndex]) text.getLineWidth(lineIndex);
|
|
44
|
+
const line = text._textLines[lineIndex];
|
|
45
|
+
const maxHeight = heightOfLine / text.lineHeight;
|
|
46
|
+
const lineLeftOffset = text._getLineLeftOffset(lineIndex);
|
|
47
|
+
const top = topOffset + maxHeight * (1 - text._fontSizeFraction);
|
|
48
|
+
let boxStart = 0;
|
|
49
|
+
let boxWidth = 0;
|
|
50
|
+
let lastDecoration = at(lineIndex, 0, type);
|
|
51
|
+
let lastFill = at(lineIndex, 0, "fill");
|
|
52
|
+
let lastColor = colorAt(lineIndex, 0);
|
|
53
|
+
let lastThickness = thicknessAt(lineIndex, 0);
|
|
54
|
+
let size = text.getHeightOfChar(lineIndex, 0);
|
|
55
|
+
let dy = Number(at(lineIndex, 0, "deltaY") ?? 0);
|
|
56
|
+
let currentDecoration = lastDecoration;
|
|
57
|
+
let currentColor = lastColor;
|
|
58
|
+
let currentThickness = lastThickness;
|
|
59
|
+
const startOf = (start, width) => {
|
|
60
|
+
const drawStart = leftOffset + lineLeftOffset + start;
|
|
61
|
+
return text.direction === "rtl" ? text.width - drawStart - width : drawStart;
|
|
62
|
+
};
|
|
63
|
+
for (let charIndex = 0; charIndex < line.length; charIndex += 1) {
|
|
64
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
65
|
+
currentDecoration = at(lineIndex, charIndex, type);
|
|
66
|
+
const currentFill = at(lineIndex, charIndex, "fill");
|
|
67
|
+
currentColor = colorAt(lineIndex, charIndex);
|
|
68
|
+
currentThickness = thicknessAt(lineIndex, charIndex);
|
|
69
|
+
const currentSize = text.getHeightOfChar(lineIndex, charIndex);
|
|
70
|
+
const currentDy = Number(at(lineIndex, charIndex, "deltaY") ?? 0);
|
|
71
|
+
if ((currentDecoration !== lastDecoration || currentFill !== lastFill || currentColor !== lastColor || currentSize !== size || currentThickness !== lastThickness || currentDy !== dy) && boxWidth > 0) {
|
|
72
|
+
const thickness = text.fontSize * lastThickness / 1e3;
|
|
73
|
+
if (lastDecoration && lastColor && lastThickness) markup += rect(startOf(boxStart, boxWidth), top + offsetY * size + dy - aligner * thickness, boxWidth, thickness, lastColor);
|
|
74
|
+
boxStart = box.left;
|
|
75
|
+
boxWidth = box.width;
|
|
76
|
+
lastDecoration = currentDecoration;
|
|
77
|
+
lastColor = currentColor;
|
|
78
|
+
lastThickness = currentThickness;
|
|
79
|
+
lastFill = currentFill;
|
|
80
|
+
size = currentSize;
|
|
81
|
+
dy = currentDy;
|
|
82
|
+
} else boxWidth += box.kernedWidth;
|
|
83
|
+
}
|
|
84
|
+
const thickness = text.fontSize * currentThickness / 1e3;
|
|
85
|
+
if (currentDecoration && currentColor && currentThickness) markup += rect(startOf(boxStart, boxWidth), top + offsetY * size + dy - aligner * thickness, boxWidth - charSpacing, thickness, currentColor);
|
|
86
|
+
topOffset += heightOfLine;
|
|
87
|
+
}
|
|
88
|
+
return markup;
|
|
89
|
+
}
|
|
90
|
+
/** SVG for straight text with its decorations as boxes instead of CSS. */
|
|
91
|
+
function decoratedTextToSVG(object, reviver) {
|
|
92
|
+
const text = object;
|
|
93
|
+
const inner = text._toSVG().join("").replace(/\stext-decoration="[^"]*"/g, "").replace(/text-decoration:[^;"]*;?\s*/g, "").replace(/baseline-shift:[^;"]*;?\s*/g, "");
|
|
94
|
+
const markup = [
|
|
95
|
+
decorationRects(text, "underline"),
|
|
96
|
+
inner,
|
|
97
|
+
decorationRects(text, "overline"),
|
|
98
|
+
decorationRects(text, "linethrough")
|
|
99
|
+
];
|
|
100
|
+
return text._createBaseSVGMarkup(markup, {
|
|
101
|
+
reviver,
|
|
102
|
+
noStyle: true,
|
|
103
|
+
withShadow: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
exports.decoratedTextToSVG = decoratedTextToSVG;
|
|
108
|
+
exports.hasStraightDecorations = hasStraightDecorations;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//#region src/export/svg/text-decorations.ts
|
|
2
|
+
const DECORATIONS = [
|
|
3
|
+
"underline",
|
|
4
|
+
"overline",
|
|
5
|
+
"linethrough"
|
|
6
|
+
];
|
|
7
|
+
function num(value) {
|
|
8
|
+
return Number.isFinite(value) ? String(Math.round(value * 1e4) / 1e4) : "0";
|
|
9
|
+
}
|
|
10
|
+
function escapeAttribute(value) {
|
|
11
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
12
|
+
}
|
|
13
|
+
function isText(object) {
|
|
14
|
+
const candidate = object;
|
|
15
|
+
return Array.isArray(candidate._textLines) && typeof candidate.getValueOfPropertyAt === "function" && !candidate.path;
|
|
16
|
+
}
|
|
17
|
+
function hasStraightDecorations(object) {
|
|
18
|
+
if (!isText(object)) return false;
|
|
19
|
+
return DECORATIONS.some((type) => object[type] || object.styleHas(type)) || object.styleHas("deltaY");
|
|
20
|
+
}
|
|
21
|
+
function rect(x, y, width, height, color) {
|
|
22
|
+
if (typeof color !== "string" || width <= 0 || height <= 0) return "";
|
|
23
|
+
return `\t\t<rect x="${num(x)}" y="${num(y)}" width="${num(width)}" height="${num(height)}" fill="${escapeAttribute(color)}" />\n`;
|
|
24
|
+
}
|
|
25
|
+
/** The same boxes as Fabric's `_renderTextDecoration` for text that is not on a path. */
|
|
26
|
+
function decorationRects(text, type) {
|
|
27
|
+
if (!text[type] && !text.styleHas(type)) return "";
|
|
28
|
+
let markup = "";
|
|
29
|
+
let topOffset = text._getTopOffset();
|
|
30
|
+
const leftOffset = text._getLeftOffset();
|
|
31
|
+
const charSpacing = text._getWidthOfCharSpacing();
|
|
32
|
+
const aligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0;
|
|
33
|
+
const offsetY = text.offsets[type];
|
|
34
|
+
const at = (line, char, property) => text.getValueOfPropertyAt(line, char, property);
|
|
35
|
+
const colorAt = (line, char) => at(line, char, "textDecorationColor") || at(line, char, "fill");
|
|
36
|
+
const thicknessAt = (line, char) => Number(at(line, char, "textDecorationThickness") ?? 66.667);
|
|
37
|
+
for (let lineIndex = 0; lineIndex < text._textLines.length; lineIndex += 1) {
|
|
38
|
+
const heightOfLine = text.getHeightOfLine(lineIndex);
|
|
39
|
+
if (!text[type] && !text.styleHas(type, lineIndex)) {
|
|
40
|
+
topOffset += heightOfLine;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!text.__charBounds[lineIndex]) text.getLineWidth(lineIndex);
|
|
44
|
+
const line = text._textLines[lineIndex];
|
|
45
|
+
const maxHeight = heightOfLine / text.lineHeight;
|
|
46
|
+
const lineLeftOffset = text._getLineLeftOffset(lineIndex);
|
|
47
|
+
const top = topOffset + maxHeight * (1 - text._fontSizeFraction);
|
|
48
|
+
let boxStart = 0;
|
|
49
|
+
let boxWidth = 0;
|
|
50
|
+
let lastDecoration = at(lineIndex, 0, type);
|
|
51
|
+
let lastFill = at(lineIndex, 0, "fill");
|
|
52
|
+
let lastColor = colorAt(lineIndex, 0);
|
|
53
|
+
let lastThickness = thicknessAt(lineIndex, 0);
|
|
54
|
+
let size = text.getHeightOfChar(lineIndex, 0);
|
|
55
|
+
let dy = Number(at(lineIndex, 0, "deltaY") ?? 0);
|
|
56
|
+
let currentDecoration = lastDecoration;
|
|
57
|
+
let currentColor = lastColor;
|
|
58
|
+
let currentThickness = lastThickness;
|
|
59
|
+
const startOf = (start, width) => {
|
|
60
|
+
const drawStart = leftOffset + lineLeftOffset + start;
|
|
61
|
+
return text.direction === "rtl" ? text.width - drawStart - width : drawStart;
|
|
62
|
+
};
|
|
63
|
+
for (let charIndex = 0; charIndex < line.length; charIndex += 1) {
|
|
64
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
65
|
+
currentDecoration = at(lineIndex, charIndex, type);
|
|
66
|
+
const currentFill = at(lineIndex, charIndex, "fill");
|
|
67
|
+
currentColor = colorAt(lineIndex, charIndex);
|
|
68
|
+
currentThickness = thicknessAt(lineIndex, charIndex);
|
|
69
|
+
const currentSize = text.getHeightOfChar(lineIndex, charIndex);
|
|
70
|
+
const currentDy = Number(at(lineIndex, charIndex, "deltaY") ?? 0);
|
|
71
|
+
if ((currentDecoration !== lastDecoration || currentFill !== lastFill || currentColor !== lastColor || currentSize !== size || currentThickness !== lastThickness || currentDy !== dy) && boxWidth > 0) {
|
|
72
|
+
const thickness = text.fontSize * lastThickness / 1e3;
|
|
73
|
+
if (lastDecoration && lastColor && lastThickness) markup += rect(startOf(boxStart, boxWidth), top + offsetY * size + dy - aligner * thickness, boxWidth, thickness, lastColor);
|
|
74
|
+
boxStart = box.left;
|
|
75
|
+
boxWidth = box.width;
|
|
76
|
+
lastDecoration = currentDecoration;
|
|
77
|
+
lastColor = currentColor;
|
|
78
|
+
lastThickness = currentThickness;
|
|
79
|
+
lastFill = currentFill;
|
|
80
|
+
size = currentSize;
|
|
81
|
+
dy = currentDy;
|
|
82
|
+
} else boxWidth += box.kernedWidth;
|
|
83
|
+
}
|
|
84
|
+
const thickness = text.fontSize * currentThickness / 1e3;
|
|
85
|
+
if (currentDecoration && currentColor && currentThickness) markup += rect(startOf(boxStart, boxWidth), top + offsetY * size + dy - aligner * thickness, boxWidth - charSpacing, thickness, currentColor);
|
|
86
|
+
topOffset += heightOfLine;
|
|
87
|
+
}
|
|
88
|
+
return markup;
|
|
89
|
+
}
|
|
90
|
+
/** SVG for straight text with its decorations as boxes instead of CSS. */
|
|
91
|
+
function decoratedTextToSVG(object, reviver) {
|
|
92
|
+
const text = object;
|
|
93
|
+
const inner = text._toSVG().join("").replace(/\stext-decoration="[^"]*"/g, "").replace(/text-decoration:[^;"]*;?\s*/g, "").replace(/baseline-shift:[^;"]*;?\s*/g, "");
|
|
94
|
+
const markup = [
|
|
95
|
+
decorationRects(text, "underline"),
|
|
96
|
+
inner,
|
|
97
|
+
decorationRects(text, "overline"),
|
|
98
|
+
decorationRects(text, "linethrough")
|
|
99
|
+
];
|
|
100
|
+
return text._createBaseSVGMarkup(markup, {
|
|
101
|
+
reviver,
|
|
102
|
+
noStyle: true,
|
|
103
|
+
withShadow: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { decoratedTextToSVG, hasStraightDecorations };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const require_walk_objects = require("../../fabric/walk-objects.cjs");
|
|
2
|
+
let fabric = require("fabric");
|
|
3
|
+
//#region src/export/svg/text-on-path.ts
|
|
4
|
+
function isTextOnPath(object) {
|
|
5
|
+
const candidate = object;
|
|
6
|
+
return Boolean(candidate.path) && Array.isArray(candidate._textLines) && typeof candidate.getCompleteStyleDeclaration === "function";
|
|
7
|
+
}
|
|
8
|
+
const DIGITS = 4;
|
|
9
|
+
function num(value) {
|
|
10
|
+
if (!Number.isFinite(value)) return "0";
|
|
11
|
+
const fixed = value.toFixed(DIGITS);
|
|
12
|
+
return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") || "0" : fixed;
|
|
13
|
+
}
|
|
14
|
+
function escapeXml(value) {
|
|
15
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
16
|
+
}
|
|
17
|
+
/** CSS values that could break out of a style attribute are dropped, as Fabric 7 does. */
|
|
18
|
+
function safeToken(value) {
|
|
19
|
+
if (value === void 0 || value === null) return void 0;
|
|
20
|
+
const text = String(value).trim();
|
|
21
|
+
if (text.length === 0 || /[;<>{}\\]|url\(|expression\(|javascript:|data:/i.test(text)) return void 0;
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
function isFiller(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && typeof value.toSVG === "function";
|
|
26
|
+
}
|
|
27
|
+
/** `fill: rgb(...); fill-opacity: a` for a color, `url(#SVGID_id)` for a gradient or pattern. */
|
|
28
|
+
function paint(property, value) {
|
|
29
|
+
if (isFiller(value)) return `${property}: url(#SVGID_${value.id}); `;
|
|
30
|
+
const token = typeof value === "string" ? safeToken(value) : void 0;
|
|
31
|
+
if (token === void 0 || token === "none" || token === "transparent") return `${property}: none; `;
|
|
32
|
+
const color = new fabric.Color(token);
|
|
33
|
+
const alpha = color.getAlpha();
|
|
34
|
+
const rgb = color.toRgb();
|
|
35
|
+
return alpha === 1 ? `${property}: ${rgb}; ` : `${property}: ${rgb}; ${property}-opacity: ${num(alpha)}; `;
|
|
36
|
+
}
|
|
37
|
+
function strokeDetails(text, strokeWidth) {
|
|
38
|
+
let style = `stroke-width: ${num(strokeWidth)}; `;
|
|
39
|
+
if (text.strokeDashArray && text.strokeDashArray.length > 0) style += `stroke-dasharray: ${text.strokeDashArray.map(num).join(" ")}; `;
|
|
40
|
+
const cap = safeToken(text.strokeLineCap);
|
|
41
|
+
const join = safeToken(text.strokeLineJoin);
|
|
42
|
+
if (cap) style += `stroke-linecap: ${cap}; `;
|
|
43
|
+
if (join) style += `stroke-linejoin: ${join}; `;
|
|
44
|
+
if (text.strokeMiterLimit !== void 0) style += `stroke-miterlimit: ${num(text.strokeMiterLimit)}; `;
|
|
45
|
+
return style;
|
|
46
|
+
}
|
|
47
|
+
function fontStyle(style) {
|
|
48
|
+
const family = safeToken(String(style.fontFamily ?? "").replace(/"/g, "'"));
|
|
49
|
+
const weight = safeToken(style.fontWeight);
|
|
50
|
+
const slant = safeToken(style.fontStyle);
|
|
51
|
+
let css = "";
|
|
52
|
+
if (family) css += `font-family: ${family}; `;
|
|
53
|
+
css += `font-size: ${num(style.fontSize ?? 0)}px; `;
|
|
54
|
+
if (weight) css += `font-weight: ${weight}; `;
|
|
55
|
+
if (slant) css += `font-style: ${slant}; `;
|
|
56
|
+
return css;
|
|
57
|
+
}
|
|
58
|
+
function frame(box) {
|
|
59
|
+
const angle = box.angle ?? 0;
|
|
60
|
+
const cos = Math.cos(angle);
|
|
61
|
+
const sin = Math.sin(angle);
|
|
62
|
+
return `matrix(${num(cos)} ${num(sin)} ${num(-sin)} ${num(cos)} ${num(box.renderLeft ?? 0)} ${num(box.renderTop ?? 0)})`;
|
|
63
|
+
}
|
|
64
|
+
let measuringContext;
|
|
65
|
+
function measuring() {
|
|
66
|
+
if (measuringContext === void 0) measuringContext = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
|
|
67
|
+
return measuringContext;
|
|
68
|
+
}
|
|
69
|
+
const baselineShifts = /* @__PURE__ */ new Map();
|
|
70
|
+
/**
|
|
71
|
+
* How far below the path the alphabetic baseline sits when Fabric draws with
|
|
72
|
+
* `pathAlign` (the canvas `textBaseline`). SVG has no portable way to say
|
|
73
|
+
* this, so the shift is measured and written as a plain offset.
|
|
74
|
+
*/
|
|
75
|
+
function baselineShift(font, pathAlign) {
|
|
76
|
+
const baseline = pathAlign === "center" ? "middle" : pathAlign === "ascender" ? "top" : pathAlign === "descender" ? "bottom" : void 0;
|
|
77
|
+
if (baseline === void 0) return 0;
|
|
78
|
+
const key = `${baseline}|${font}`;
|
|
79
|
+
const known = baselineShifts.get(key);
|
|
80
|
+
if (known !== void 0) return known;
|
|
81
|
+
const context = measuring();
|
|
82
|
+
let shift = 0;
|
|
83
|
+
if (context) {
|
|
84
|
+
context.font = font;
|
|
85
|
+
context.textBaseline = baseline;
|
|
86
|
+
shift = -context.measureText("M").alphabeticBaseline;
|
|
87
|
+
if (!Number.isFinite(shift)) shift = 0;
|
|
88
|
+
}
|
|
89
|
+
baselineShifts.set(key, shift);
|
|
90
|
+
return shift;
|
|
91
|
+
}
|
|
92
|
+
function guidePath(text) {
|
|
93
|
+
const path = text.path;
|
|
94
|
+
if (!path || !path.visible || path.opacity === 0) return "";
|
|
95
|
+
const pathData = fabric.util.joinPath(path.path, DIGITS);
|
|
96
|
+
const strokeWidth = path.strokeWidth ?? 1;
|
|
97
|
+
let style = paint("fill", path.fill);
|
|
98
|
+
if (path.fillRule === "evenodd") style += "fill-rule: evenodd; ";
|
|
99
|
+
style += paint("stroke", path.stroke);
|
|
100
|
+
if (path.stroke) style += strokeDetails(path, strokeWidth);
|
|
101
|
+
return `\t\t<path d="${escapeXml(pathData)}" transform="translate(${num(-path.pathOffset.x)} ${num(-path.pathOffset.y)})" style="${style}" />\n`;
|
|
102
|
+
}
|
|
103
|
+
function fillerDefinitions(text) {
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
if (isFiller(text.fill)) seen.add(text.fill.id);
|
|
106
|
+
if (isFiller(text.stroke)) seen.add(text.stroke.id);
|
|
107
|
+
const path = text.path;
|
|
108
|
+
let markup = "";
|
|
109
|
+
const add = (value, owner) => {
|
|
110
|
+
if (!isFiller(value) || seen.has(value.id)) return;
|
|
111
|
+
seen.add(value.id);
|
|
112
|
+
markup += value.toSVG(owner);
|
|
113
|
+
};
|
|
114
|
+
if (path && path.visible && path.opacity !== 0) {
|
|
115
|
+
add(path.fill, path);
|
|
116
|
+
add(path.stroke, path);
|
|
117
|
+
}
|
|
118
|
+
text._textLines.forEach((line, lineIndex) => line.forEach((_, charIndex) => {
|
|
119
|
+
const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
120
|
+
add(style.fill, text);
|
|
121
|
+
add(style.stroke, text);
|
|
122
|
+
}));
|
|
123
|
+
return markup;
|
|
124
|
+
}
|
|
125
|
+
function decorationRects(text, type) {
|
|
126
|
+
const aligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0;
|
|
127
|
+
let markup = "";
|
|
128
|
+
text._textLines.forEach((line, lineIndex) => {
|
|
129
|
+
line.forEach((_, charIndex) => {
|
|
130
|
+
const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
131
|
+
if (!style[type] || !style.fill) return;
|
|
132
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
133
|
+
const thickness = text.fontSize * (style.textDecorationThickness ?? 66.667) / 1e3;
|
|
134
|
+
const size = text.getHeightOfChar(lineIndex, charIndex);
|
|
135
|
+
const top = text.offsets[type] * size + (style.deltaY ?? 0) - aligner * thickness;
|
|
136
|
+
const color = style.textDecorationColor || style.fill;
|
|
137
|
+
markup += `\t\t<rect transform="${frame(box)}" x="${num(-box.kernedWidth / 2)}" y="${num(top)}" width="${num(box.kernedWidth)}" height="${num(thickness)}" style="${paint("fill", color)}" />\n`;
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
return markup;
|
|
141
|
+
}
|
|
142
|
+
function textBackgrounds(text) {
|
|
143
|
+
let markup = "";
|
|
144
|
+
text._textLines.forEach((line, lineIndex) => {
|
|
145
|
+
const height = text.getHeightOfLineImpl(lineIndex);
|
|
146
|
+
line.forEach((_, charIndex) => {
|
|
147
|
+
const color = text.getCompleteStyleDeclaration(lineIndex, charIndex).textBackgroundColor;
|
|
148
|
+
if (!color) return;
|
|
149
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
150
|
+
markup += `\t\t<rect transform="${frame(box)}" x="${num(-box.width / 2)}" y="${num(-height * (1 - text._fontSizeFraction))}" width="${num(box.width)}" height="${num(height)}" style="${paint("fill", color)}" />\n`;
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
return markup;
|
|
154
|
+
}
|
|
155
|
+
/** One pass of characters: Fabric fills every character, then strokes every character (or the reverse). */
|
|
156
|
+
function glyphs(text, pass) {
|
|
157
|
+
const rtl = text.direction === "rtl";
|
|
158
|
+
const nonScaling = text.strokeUniform ? " vector-effect=\"non-scaling-stroke\"" : "";
|
|
159
|
+
let markup = "";
|
|
160
|
+
text._textLines.forEach((line, lineIndex) => {
|
|
161
|
+
line.forEach((grapheme, charIndex) => {
|
|
162
|
+
if (grapheme.trim().length === 0) return;
|
|
163
|
+
const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
164
|
+
const strokeWidth = style.strokeWidth ?? 0;
|
|
165
|
+
if (pass === "fill" && !style.fill) return;
|
|
166
|
+
if (pass === "stroke" && (!style.stroke || strokeWidth === 0)) return;
|
|
167
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
168
|
+
const y = baselineShift(text._getFontDeclaration(style), text.pathAlign) + (style.deltaY ?? 0);
|
|
169
|
+
const paintStyle = pass === "fill" ? `${paint("fill", style.fill)}stroke: none; ` : `fill: none; ${paint("stroke", style.stroke)}${strokeDetails(text, strokeWidth)}`;
|
|
170
|
+
const anchor = rtl ? " text-anchor=\"end\"" : "";
|
|
171
|
+
markup += `\t\t<text xml:space="preserve" transform="${frame(box)}" x="${num(-box.width / 2)}" y="${num(y)}"${anchor}${nonScaling} style="${fontStyle(style)}${paintStyle}white-space: pre;">${escapeXml(grapheme)}</text>\n`;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
return markup;
|
|
175
|
+
}
|
|
176
|
+
function objectStyle(text) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
if (text.opacity !== 1) parts.push(`opacity: ${num(text.opacity)};`);
|
|
179
|
+
if (!text.visible) parts.push("visibility: hidden;");
|
|
180
|
+
return parts.length > 0 ? ` style="${parts.join(" ")}"` : "";
|
|
181
|
+
}
|
|
182
|
+
function boxBackground(text) {
|
|
183
|
+
const color = text.backgroundColor;
|
|
184
|
+
if (!color) return "";
|
|
185
|
+
const { x, y } = text._getNonTransformedDimensions();
|
|
186
|
+
return `\t\t<rect x="${num(-x / 2)}" y="${num(-y / 2)}" width="${num(x)}" height="${num(y)}" style="${paint("fill", color)}" />\n`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Makes sure every character has its place on the path. Fabric measures
|
|
190
|
+
* characters when it first draws them, and not at all when `path` was
|
|
191
|
+
* assigned directly instead of through `set` or the constructor.
|
|
192
|
+
*/
|
|
193
|
+
function layOut(text) {
|
|
194
|
+
const measured = () => text._textLines.every((line, index) => line.length === 0 || text.__charBounds?.[index]?.[0]?.renderLeft !== void 0);
|
|
195
|
+
if (!text.path?.segmentsInfo) {
|
|
196
|
+
text.setPathInfo();
|
|
197
|
+
text.initDimensions();
|
|
198
|
+
}
|
|
199
|
+
text._textLines.forEach((_, index) => {
|
|
200
|
+
if (text.__charBounds?.[index]?.[0]?.renderLeft === void 0) text.getLineWidth(index);
|
|
201
|
+
});
|
|
202
|
+
if (!measured()) {
|
|
203
|
+
text.setPathInfo();
|
|
204
|
+
text.initDimensions();
|
|
205
|
+
text._textLines.forEach((_, index) => text.getLineWidth(index));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** The SVG for one text on a path, in the same order Fabric draws it on the canvas. */
|
|
209
|
+
function textOnPathToSVG(object, reviver) {
|
|
210
|
+
const text = object;
|
|
211
|
+
layOut(text);
|
|
212
|
+
const strokeFirst = text.paintFirst === "stroke";
|
|
213
|
+
const body = [
|
|
214
|
+
fillerDefinitions(text),
|
|
215
|
+
`\t<g${objectStyle(text)}>\n`,
|
|
216
|
+
boxBackground(text),
|
|
217
|
+
guidePath(text),
|
|
218
|
+
textBackgrounds(text),
|
|
219
|
+
decorationRects(text, "underline"),
|
|
220
|
+
strokeFirst ? glyphs(text, "stroke") : glyphs(text, "fill"),
|
|
221
|
+
strokeFirst ? glyphs(text, "fill") : glyphs(text, "stroke"),
|
|
222
|
+
decorationRects(text, "overline"),
|
|
223
|
+
decorationRects(text, "linethrough"),
|
|
224
|
+
" </g>\n"
|
|
225
|
+
];
|
|
226
|
+
return text._createBaseSVGMarkup(body, {
|
|
227
|
+
reviver,
|
|
228
|
+
noStyle: true,
|
|
229
|
+
withShadow: true
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function findTextOnPath(canvas) {
|
|
233
|
+
const found = [];
|
|
234
|
+
require_walk_objects.walkObjects(canvas.getObjects(), (object) => {
|
|
235
|
+
if (isTextOnPath(object)) found.push(object);
|
|
236
|
+
});
|
|
237
|
+
return found;
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
exports.findTextOnPath = findTextOnPath;
|
|
241
|
+
exports.isTextOnPath = isTextOnPath;
|
|
242
|
+
exports.textOnPathToSVG = textOnPathToSVG;
|