kritzel-stencil 0.4.8 → 0.4.9
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/dist/cjs/index.cjs.js +9 -1
- package/dist/cjs/kritzel-active-users_47.cjs.entry.js +1386 -1250
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{schema.constants-gDeS7USU.js → schema.constants-ByNZ_qAj.js} +61 -0
- package/dist/cjs/stencil.cjs.js +1 -1
- package/dist/collection/classes/objects/image.class.js +9 -0
- package/dist/collection/classes/objects/path.class.js +9 -0
- package/dist/collection/classes/objects/shape.class.js +9 -0
- package/dist/collection/classes/providers/sync/broadcast-sync-provider.class.js +1 -0
- package/dist/collection/classes/providers/sync/hocuspocus-sync-provider.class.js +2 -0
- package/dist/collection/classes/providers/sync/in-memory-sync-provider.class.js +1 -0
- package/dist/collection/classes/providers/sync/indexeddb-sync-provider.class.js +1 -0
- package/dist/collection/classes/providers/sync/websocket-sync-provider.class.js +2 -0
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +197 -67
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +136 -169
- package/dist/collection/components/ui/kritzel-export/kritzel-export.js +1 -1
- package/dist/collection/components/ui/kritzel-loading-overlay/kritzel-loading-overlay.js +1 -1
- package/dist/collection/components/ui/kritzel-login-dialog/kritzel-login-dialog.js +1 -1
- package/dist/collection/components/ui/kritzel-more-menu/kritzel-more-menu.js +90 -4
- package/dist/collection/components/ui/kritzel-settings/kritzel-settings.js +1 -1
- package/dist/collection/components/ui/kritzel-share-dialog/kritzel-share-dialog.js +2 -2
- package/dist/collection/components/ui/kritzel-utility-panel/kritzel-utility-panel.js +1 -1
- package/dist/collection/components/ui/kritzel-zoom-panel/kritzel-zoom-panel.js +1 -1
- package/dist/collection/constants/version.js +1 -1
- package/dist/collection/helpers/object.helper.js +11 -0
- package/dist/collection/index.js +1 -0
- package/dist/collection/interfaces/object-export.interface.js +1 -0
- package/dist/collection/interfaces/sync-config.interface.js +21 -1
- package/dist/components/index.js +10 -3
- package/dist/components/kritzel-awareness-cursors.js +1 -1
- package/dist/components/kritzel-controls.js +1 -1
- package/dist/components/kritzel-editor.js +175 -78
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-export.js +1 -1
- package/dist/components/kritzel-loading-overlay.js +1 -1
- package/dist/components/kritzel-login-dialog.js +1 -1
- package/dist/components/kritzel-menu-item.js +1 -1
- package/dist/components/kritzel-menu.js +1 -1
- package/dist/components/kritzel-more-menu.js +1 -1
- package/dist/components/kritzel-portal.js +1 -1
- package/dist/components/kritzel-settings.js +1 -1
- package/dist/components/kritzel-share-dialog.js +1 -1
- package/dist/components/kritzel-split-button.js +1 -1
- package/dist/components/kritzel-utility-panel.js +1 -1
- package/dist/components/kritzel-workspace-manager.js +1 -1
- package/dist/components/kritzel-zoom-panel.js +1 -1
- package/dist/components/{p-DwY5pghH.js → p-6rQICkGr.js} +1 -1
- package/dist/components/{p-BknTMcbz.js → p-8ScektIm.js} +1 -1
- package/dist/components/{p-DkY7pIg5.js → p-BRGufCki.js} +95 -6
- package/dist/components/{p-BKOsmXB5.js → p-BqWiEmYI.js} +1 -1
- package/dist/components/{p-BLOPBDUF.js → p-C2CeSrTm.js} +1 -1
- package/dist/components/{p-DDbO5q-e.js → p-CXJZxJvp.js} +4 -4
- package/dist/components/{p-7Rt7ml3v.js → p-Cj0NElII.js} +1 -1
- package/dist/components/{p-BVEGQLBD.js → p-CoY7emAj.js} +1 -1
- package/dist/components/{p-DgA865Tp.js → p-D7H8oxZb.js} +2 -2
- package/dist/components/{p-Dd-pMvlr.js → p-DPONTran.js} +11 -0
- package/dist/components/{p-BMRln59F.js → p-DW7WW4EN.js} +2 -2
- package/dist/components/{p-D8TOEhPY.js → p-DuVkk58V.js} +2 -2
- package/dist/components/{p-OLfoATNk.js → p-U9FjWrp-.js} +1 -1
- package/dist/components/{p-D8bHNNEm.js → p-ZPPtEla5.js} +1227 -1218
- package/dist/components/{p-COjsYPir.js → p-dVfh-Sy9.js} +1 -1
- package/dist/components/{p-0H4HOJjy.js → p-lQ7nfhOy.js} +10 -1
- package/dist/esm/index.js +9 -2
- package/dist/esm/kritzel-active-users_47.entry.js +1386 -1250
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{schema.constants-Rpm1NA9u.js → schema.constants-1t45c2_U.js} +61 -1
- package/dist/esm/stencil.js +1 -1
- package/dist/stencil/index.esm.js +9 -2
- package/dist/stencil/{p-Rpm1NA9u.js → p-1t45c2_U.js} +61 -1
- package/dist/stencil/{p-8c1b7b08.entry.js → p-f5f088ea.entry.js} +1386 -1250
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/objects/image.class.d.ts +3 -0
- package/dist/types/classes/objects/path.class.d.ts +3 -0
- package/dist/types/classes/objects/shape.class.d.ts +3 -0
- package/dist/types/classes/providers/sync/broadcast-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/hocuspocus-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/in-memory-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/indexeddb-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/websocket-sync-provider.class.d.ts +1 -0
- package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +15 -3
- package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +16 -34
- package/dist/types/components/ui/kritzel-more-menu/kritzel-more-menu.d.ts +10 -1
- package/dist/types/components.d.ts +27 -26
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/helpers/object.helper.d.ts +4 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/interfaces/menu-item.interface.d.ts +1 -1
- package/dist/types/interfaces/object-export.interface.d.ts +20 -0
- package/dist/types/interfaces/sync-config.interface.d.ts +5 -0
- package/dist/types/interfaces/sync-provider.interface.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { p as proxyCustomElement, H, c as createEvent, h, d as Host, t as transformTag } from './p-B43upypT.js';
|
|
2
2
|
import { a as KritzelMouseButton, d as defineCustomElement$5 } from './p-Cm33td2O.js';
|
|
3
|
-
import { K as KritzelBaseObject, e as KritzelMathHelper, f as KritzelGeometryHelper, a as KritzelPath, b as KritzelLine, d as defineCustomElement$6 } from './p-
|
|
3
|
+
import { K as KritzelBaseObject, e as KritzelMathHelper, f as KritzelGeometryHelper, a as KritzelPath, b as KritzelLine, d as defineCustomElement$6 } from './p-lQ7nfhOy.js';
|
|
4
4
|
import { c as commonjsGlobal } from './p-BFTU3MAI.js';
|
|
5
5
|
import { a as KritzelColorHelper, D as DEFAULT_COLOR_PALETTE, K as KritzelThemeManager } from './p-CURiiQfq.js';
|
|
6
6
|
import { K as KritzelDevicesHelper } from './p-jGOpkGDl.js';
|
|
@@ -8,11 +8,11 @@ import { K as KritzelFontRegistry, r as resolveTextToolAvailableFonts } from './
|
|
|
8
8
|
import { A as ABSOLUTE_SCALE_MAX, a as ABSOLUTE_SCALE_MIN, d as defineCustomElement$1 } from './p-CwuGA058.js';
|
|
9
9
|
import { K as KritzelWorkspace, W as WORKSPACE_EXPORT_VERSION } from './p-DAwPPDMS.js';
|
|
10
10
|
import { K as KritzelHTMLHelper } from './p-WUdQqYnu.js';
|
|
11
|
-
import { O as ObjectHelper } from './p-
|
|
11
|
+
import { O as ObjectHelper } from './p-DPONTran.js';
|
|
12
12
|
import * as Y from 'yjs';
|
|
13
13
|
import { a as CURRENT_WORKSPACE_SCHEMA_VERSION, C as CURRENT_APP_STATE_SCHEMA_VERSION } from './p-CW-VyJgK.js';
|
|
14
14
|
import { K as KritzelIconRegistry, d as defineCustomElement$4 } from './p-CdR76C4L.js';
|
|
15
|
-
import { d as defineCustomElement$3 } from './p-
|
|
15
|
+
import { d as defineCustomElement$3 } from './p-CoY7emAj.js';
|
|
16
16
|
import { d as defineCustomElement$2 } from './p-CM1duewg.js';
|
|
17
17
|
|
|
18
18
|
class KritzelKeyboardHelper {
|
|
@@ -15453,6 +15453,15 @@ class KritzelImage extends KritzelBaseObject {
|
|
|
15453
15453
|
}
|
|
15454
15454
|
return { scaledWidth, scaledHeight };
|
|
15455
15455
|
}
|
|
15456
|
+
getSupportedExportFormats() {
|
|
15457
|
+
return ['png'];
|
|
15458
|
+
}
|
|
15459
|
+
async exportAs(format, context) {
|
|
15460
|
+
if (format !== 'png') {
|
|
15461
|
+
return null;
|
|
15462
|
+
}
|
|
15463
|
+
return context.getObjectAsPngDataUrl(this);
|
|
15464
|
+
}
|
|
15456
15465
|
}
|
|
15457
15466
|
|
|
15458
15467
|
class KritzelClassHelper {
|
|
@@ -16660,6 +16669,15 @@ class KritzelShape extends KritzelBaseObject {
|
|
|
16660
16669
|
return `M ${this.x} ${this.y} L ${this.x + w} ${this.y} L ${this.x + w} ${this.y + h} L ${this.x} ${this.y + h} Z`;
|
|
16661
16670
|
}
|
|
16662
16671
|
}
|
|
16672
|
+
getSupportedExportFormats() {
|
|
16673
|
+
return ['svg'];
|
|
16674
|
+
}
|
|
16675
|
+
async exportAs(format, context) {
|
|
16676
|
+
if (format !== 'svg') {
|
|
16677
|
+
return null;
|
|
16678
|
+
}
|
|
16679
|
+
return context.getObjectAsSvgDataUrl(this);
|
|
16680
|
+
}
|
|
16663
16681
|
}
|
|
16664
16682
|
|
|
16665
16683
|
const KRITZEL_RENDERER_REGISTRY_KEY = '__kritzelCustomElementRendererRegistry__';
|
|
@@ -47003,995 +47021,343 @@ class KritzelCore {
|
|
|
47003
47021
|
}
|
|
47004
47022
|
}
|
|
47005
47023
|
|
|
47006
|
-
|
|
47007
|
-
|
|
47008
|
-
|
|
47009
|
-
|
|
47010
|
-
class KritzelSvgExportHelper {
|
|
47011
|
-
/**
|
|
47012
|
-
* Calculates the combined bounding box that encompasses all objects.
|
|
47013
|
-
* Uses rotatedBoundingBox to account for object rotation.
|
|
47014
|
-
* @param objects - Array of objects to calculate bounds for
|
|
47015
|
-
* @returns Combined bounding box, or null if no objects provided
|
|
47016
|
-
*/
|
|
47017
|
-
static calculateCombinedBounds(objects) {
|
|
47018
|
-
if (!objects || objects.length === 0) {
|
|
47019
|
-
return null;
|
|
47020
|
-
}
|
|
47021
|
-
let minX = Infinity;
|
|
47022
|
-
let minY = Infinity;
|
|
47023
|
-
let maxX = -Infinity;
|
|
47024
|
-
let maxY = -Infinity;
|
|
47025
|
-
for (const obj of objects) {
|
|
47026
|
-
const bbox = obj.rotatedBoundingBox;
|
|
47027
|
-
minX = Math.min(minX, bbox.x);
|
|
47028
|
-
minY = Math.min(minY, bbox.y);
|
|
47029
|
-
maxX = Math.max(maxX, bbox.x + bbox.width);
|
|
47030
|
-
maxY = Math.max(maxY, bbox.y + bbox.height);
|
|
47031
|
-
}
|
|
47032
|
-
return {
|
|
47033
|
-
x: minX,
|
|
47034
|
-
y: minY,
|
|
47035
|
-
z: 1,
|
|
47036
|
-
width: maxX - minX,
|
|
47037
|
-
height: maxY - minY,
|
|
47038
|
-
};
|
|
47024
|
+
function resolveUrl(url, baseUrl) {
|
|
47025
|
+
// url is absolute already
|
|
47026
|
+
if (url.match(/^[a-z]+:\/\//i)) {
|
|
47027
|
+
return url;
|
|
47039
47028
|
}
|
|
47040
|
-
|
|
47041
|
-
|
|
47042
|
-
|
|
47043
|
-
|
|
47044
|
-
|
|
47045
|
-
|
|
47046
|
-
|
|
47047
|
-
|
|
47048
|
-
|
|
47049
|
-
|
|
47050
|
-
|
|
47051
|
-
|
|
47052
|
-
|
|
47053
|
-
|
|
47054
|
-
|
|
47055
|
-
|
|
47029
|
+
// url is absolute already, without protocol
|
|
47030
|
+
if (url.match(/^\/\//)) {
|
|
47031
|
+
return window.location.protocol + url;
|
|
47032
|
+
}
|
|
47033
|
+
// dataURI, mailto:, tel:, etc.
|
|
47034
|
+
if (url.match(/^[a-z]+:/i)) {
|
|
47035
|
+
return url;
|
|
47036
|
+
}
|
|
47037
|
+
const doc = document.implementation.createHTMLDocument();
|
|
47038
|
+
const base = doc.createElement('base');
|
|
47039
|
+
const a = doc.createElement('a');
|
|
47040
|
+
doc.head.appendChild(base);
|
|
47041
|
+
doc.body.appendChild(a);
|
|
47042
|
+
if (baseUrl) {
|
|
47043
|
+
base.href = baseUrl;
|
|
47044
|
+
}
|
|
47045
|
+
a.href = url;
|
|
47046
|
+
return a.href;
|
|
47047
|
+
}
|
|
47048
|
+
const uuid = (() => {
|
|
47049
|
+
// generate uuid for className of pseudo elements.
|
|
47050
|
+
// We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
|
|
47051
|
+
let counter = 0;
|
|
47052
|
+
// ref: http://stackoverflow.com/a/6248722/2519373
|
|
47053
|
+
const random = () =>
|
|
47054
|
+
// eslint-disable-next-line no-bitwise
|
|
47055
|
+
`0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
|
|
47056
|
+
return () => {
|
|
47057
|
+
counter += 1;
|
|
47058
|
+
return `u${random()}${counter}`;
|
|
47059
|
+
};
|
|
47060
|
+
})();
|
|
47061
|
+
function toArray(arrayLike) {
|
|
47062
|
+
const arr = [];
|
|
47063
|
+
for (let i = 0, l = arrayLike.length; i < l; i++) {
|
|
47064
|
+
arr.push(arrayLike[i]);
|
|
47065
|
+
}
|
|
47066
|
+
return arr;
|
|
47067
|
+
}
|
|
47068
|
+
let styleProps = null;
|
|
47069
|
+
function getStyleProperties(options = {}) {
|
|
47070
|
+
if (styleProps) {
|
|
47071
|
+
return styleProps;
|
|
47072
|
+
}
|
|
47073
|
+
if (options.includeStyleProperties) {
|
|
47074
|
+
styleProps = options.includeStyleProperties;
|
|
47075
|
+
return styleProps;
|
|
47076
|
+
}
|
|
47077
|
+
styleProps = toArray(window.getComputedStyle(document.documentElement));
|
|
47078
|
+
return styleProps;
|
|
47079
|
+
}
|
|
47080
|
+
function px(node, styleProperty) {
|
|
47081
|
+
const win = node.ownerDocument.defaultView || window;
|
|
47082
|
+
const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
|
|
47083
|
+
return val ? parseFloat(val.replace('px', '')) : 0;
|
|
47084
|
+
}
|
|
47085
|
+
function getNodeWidth(node) {
|
|
47086
|
+
const leftBorder = px(node, 'border-left-width');
|
|
47087
|
+
const rightBorder = px(node, 'border-right-width');
|
|
47088
|
+
return node.clientWidth + leftBorder + rightBorder;
|
|
47089
|
+
}
|
|
47090
|
+
function getNodeHeight(node) {
|
|
47091
|
+
const topBorder = px(node, 'border-top-width');
|
|
47092
|
+
const bottomBorder = px(node, 'border-bottom-width');
|
|
47093
|
+
return node.clientHeight + topBorder + bottomBorder;
|
|
47094
|
+
}
|
|
47095
|
+
function getImageSize(targetNode, options = {}) {
|
|
47096
|
+
const width = options.width || getNodeWidth(targetNode);
|
|
47097
|
+
const height = options.height || getNodeHeight(targetNode);
|
|
47098
|
+
return { width, height };
|
|
47099
|
+
}
|
|
47100
|
+
function getPixelRatio() {
|
|
47101
|
+
let ratio;
|
|
47102
|
+
let FINAL_PROCESS;
|
|
47103
|
+
try {
|
|
47104
|
+
FINAL_PROCESS = process;
|
|
47105
|
+
}
|
|
47106
|
+
catch (e) {
|
|
47107
|
+
// pass
|
|
47108
|
+
}
|
|
47109
|
+
const val = FINAL_PROCESS && FINAL_PROCESS.env
|
|
47110
|
+
? FINAL_PROCESS.env.devicePixelRatio
|
|
47111
|
+
: null;
|
|
47112
|
+
if (val) {
|
|
47113
|
+
ratio = parseInt(val, 10);
|
|
47114
|
+
if (Number.isNaN(ratio)) {
|
|
47115
|
+
ratio = 1;
|
|
47056
47116
|
}
|
|
47057
|
-
// Add padding to bounds
|
|
47058
|
-
const viewBoxX = bounds.x - padding;
|
|
47059
|
-
const viewBoxY = bounds.y - padding;
|
|
47060
|
-
const viewBoxWidth = bounds.width + padding * 2;
|
|
47061
|
-
const viewBoxHeight = bounds.height + padding * 2;
|
|
47062
|
-
// Collect all defs (markers, patterns, etc.)
|
|
47063
|
-
const defs = this.collectDefs(allObjects, theme);
|
|
47064
|
-
// Generate SVG elements for each object
|
|
47065
|
-
const elements = objects
|
|
47066
|
-
.sort((a, b) => a.zIndex - b.zIndex)
|
|
47067
|
-
.map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
|
|
47068
|
-
.filter(Boolean)
|
|
47069
|
-
.join('\n ');
|
|
47070
|
-
const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
|
|
47071
|
-
const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
|
|
47072
|
-
return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
|
|
47073
|
-
${elements}
|
|
47074
|
-
</svg>`;
|
|
47075
47117
|
}
|
|
47076
|
-
|
|
47077
|
-
|
|
47078
|
-
|
|
47079
|
-
|
|
47080
|
-
|
|
47081
|
-
|
|
47082
|
-
|
|
47083
|
-
|
|
47084
|
-
|
|
47085
|
-
|
|
47118
|
+
return ratio || window.devicePixelRatio || 1;
|
|
47119
|
+
}
|
|
47120
|
+
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
|
47121
|
+
const canvasDimensionLimit = 16384;
|
|
47122
|
+
function checkCanvasDimensions(canvas) {
|
|
47123
|
+
if (canvas.width > canvasDimensionLimit ||
|
|
47124
|
+
canvas.height > canvasDimensionLimit) {
|
|
47125
|
+
if (canvas.width > canvasDimensionLimit &&
|
|
47126
|
+
canvas.height > canvasDimensionLimit) {
|
|
47127
|
+
if (canvas.width > canvas.height) {
|
|
47128
|
+
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
47129
|
+
canvas.width = canvasDimensionLimit;
|
|
47086
47130
|
}
|
|
47087
47131
|
else {
|
|
47088
|
-
|
|
47132
|
+
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
47133
|
+
canvas.height = canvasDimensionLimit;
|
|
47089
47134
|
}
|
|
47090
47135
|
}
|
|
47091
|
-
|
|
47092
|
-
|
|
47093
|
-
|
|
47094
|
-
* Resolves every image referenced by the given objects (including those
|
|
47095
|
-
* nested inside groups) to an inlined `data:` URL, keyed by object id.
|
|
47096
|
-
*
|
|
47097
|
-
* This is required for export flows that rasterize the SVG (e.g. PNG
|
|
47098
|
-
* export): an SVG loaded into an `<img>` element will NOT load external
|
|
47099
|
-
* or `blob:` resources, so a `<image href="blob:...">` renders as a
|
|
47100
|
-
* broken-image placeholder. Embedding the bytes as a base64 `data:` URL
|
|
47101
|
-
* makes the image self-contained and survive rasterization.
|
|
47102
|
-
*
|
|
47103
|
-
* Resolution is best-effort: images that fail to resolve are simply
|
|
47104
|
-
* omitted from the map, and the SVG falls back to whatever `href` the
|
|
47105
|
-
* synchronous path would have produced.
|
|
47106
|
-
* @param objects - Objects to inline images for
|
|
47107
|
-
* @param assetResolver - Resolver used to fetch asset bytes; optional
|
|
47108
|
-
* @returns Map of image object id -> inlined `data:` URL
|
|
47109
|
-
*/
|
|
47110
|
-
static async resolveImageDataUrls(objects, assetResolver) {
|
|
47111
|
-
const dataUrls = new Map();
|
|
47112
|
-
if (!objects || objects.length === 0) {
|
|
47113
|
-
return dataUrls;
|
|
47136
|
+
else if (canvas.width > canvasDimensionLimit) {
|
|
47137
|
+
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
47138
|
+
canvas.width = canvasDimensionLimit;
|
|
47114
47139
|
}
|
|
47115
|
-
|
|
47116
|
-
|
|
47117
|
-
|
|
47118
|
-
|
|
47119
|
-
|
|
47120
|
-
|
|
47121
|
-
|
|
47122
|
-
|
|
47123
|
-
|
|
47124
|
-
|
|
47125
|
-
|
|
47126
|
-
|
|
47127
|
-
|
|
47128
|
-
|
|
47129
|
-
|
|
47140
|
+
else {
|
|
47141
|
+
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
47142
|
+
canvas.height = canvasDimensionLimit;
|
|
47143
|
+
}
|
|
47144
|
+
}
|
|
47145
|
+
}
|
|
47146
|
+
function createImage(url) {
|
|
47147
|
+
return new Promise((resolve, reject) => {
|
|
47148
|
+
const img = new Image();
|
|
47149
|
+
img.onload = () => {
|
|
47150
|
+
img.decode().then(() => {
|
|
47151
|
+
requestAnimationFrame(() => resolve(img));
|
|
47152
|
+
});
|
|
47153
|
+
};
|
|
47154
|
+
img.onerror = reject;
|
|
47155
|
+
img.crossOrigin = 'anonymous';
|
|
47156
|
+
img.decoding = 'async';
|
|
47157
|
+
img.src = url;
|
|
47158
|
+
});
|
|
47159
|
+
}
|
|
47160
|
+
async function svgToDataURL(svg) {
|
|
47161
|
+
return Promise.resolve()
|
|
47162
|
+
.then(() => new XMLSerializer().serializeToString(svg))
|
|
47163
|
+
.then(encodeURIComponent)
|
|
47164
|
+
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
|
|
47165
|
+
}
|
|
47166
|
+
async function nodeToDataURL(node, width, height) {
|
|
47167
|
+
const xmlns = 'http://www.w3.org/2000/svg';
|
|
47168
|
+
const svg = document.createElementNS(xmlns, 'svg');
|
|
47169
|
+
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
|
47170
|
+
svg.setAttribute('width', `${width}`);
|
|
47171
|
+
svg.setAttribute('height', `${height}`);
|
|
47172
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
47173
|
+
foreignObject.setAttribute('width', '100%');
|
|
47174
|
+
foreignObject.setAttribute('height', '100%');
|
|
47175
|
+
foreignObject.setAttribute('x', '0');
|
|
47176
|
+
foreignObject.setAttribute('y', '0');
|
|
47177
|
+
foreignObject.setAttribute('externalResourcesRequired', 'true');
|
|
47178
|
+
svg.appendChild(foreignObject);
|
|
47179
|
+
foreignObject.appendChild(node);
|
|
47180
|
+
return svgToDataURL(svg);
|
|
47181
|
+
}
|
|
47182
|
+
const isInstanceOfElement = (node, instance) => {
|
|
47183
|
+
if (node instanceof instance)
|
|
47184
|
+
return true;
|
|
47185
|
+
const nodePrototype = Object.getPrototypeOf(node);
|
|
47186
|
+
if (nodePrototype === null)
|
|
47187
|
+
return false;
|
|
47188
|
+
return (nodePrototype.constructor.name === instance.name ||
|
|
47189
|
+
isInstanceOfElement(nodePrototype, instance));
|
|
47190
|
+
};
|
|
47191
|
+
|
|
47192
|
+
function formatCSSText(style) {
|
|
47193
|
+
const content = style.getPropertyValue('content');
|
|
47194
|
+
return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
|
|
47195
|
+
}
|
|
47196
|
+
function formatCSSProperties(style, options) {
|
|
47197
|
+
return getStyleProperties(options)
|
|
47198
|
+
.map((name) => {
|
|
47199
|
+
const value = style.getPropertyValue(name);
|
|
47200
|
+
const priority = style.getPropertyPriority(name);
|
|
47201
|
+
return `${name}: ${value}${priority ? ' !important' : ''};`;
|
|
47202
|
+
})
|
|
47203
|
+
.join(' ');
|
|
47204
|
+
}
|
|
47205
|
+
function getPseudoElementStyle(className, pseudo, style, options) {
|
|
47206
|
+
const selector = `.${className}:${pseudo}`;
|
|
47207
|
+
const cssText = style.cssText
|
|
47208
|
+
? formatCSSText(style)
|
|
47209
|
+
: formatCSSProperties(style, options);
|
|
47210
|
+
return document.createTextNode(`${selector}{${cssText}}`);
|
|
47211
|
+
}
|
|
47212
|
+
function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
|
|
47213
|
+
const style = window.getComputedStyle(nativeNode, pseudo);
|
|
47214
|
+
const content = style.getPropertyValue('content');
|
|
47215
|
+
if (content === '' || content === 'none') {
|
|
47216
|
+
return;
|
|
47217
|
+
}
|
|
47218
|
+
const className = uuid();
|
|
47219
|
+
try {
|
|
47220
|
+
clonedNode.className = `${clonedNode.className} ${className}`;
|
|
47221
|
+
}
|
|
47222
|
+
catch (err) {
|
|
47223
|
+
return;
|
|
47224
|
+
}
|
|
47225
|
+
const styleElement = document.createElement('style');
|
|
47226
|
+
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
|
|
47227
|
+
clonedNode.appendChild(styleElement);
|
|
47228
|
+
}
|
|
47229
|
+
function clonePseudoElements(nativeNode, clonedNode, options) {
|
|
47230
|
+
clonePseudoElement(nativeNode, clonedNode, ':before', options);
|
|
47231
|
+
clonePseudoElement(nativeNode, clonedNode, ':after', options);
|
|
47232
|
+
}
|
|
47233
|
+
|
|
47234
|
+
const WOFF = 'application/font-woff';
|
|
47235
|
+
const JPEG = 'image/jpeg';
|
|
47236
|
+
const mimes = {
|
|
47237
|
+
woff: WOFF,
|
|
47238
|
+
woff2: WOFF,
|
|
47239
|
+
ttf: 'application/font-truetype',
|
|
47240
|
+
eot: 'application/vnd.ms-fontobject',
|
|
47241
|
+
png: 'image/png',
|
|
47242
|
+
jpg: JPEG,
|
|
47243
|
+
jpeg: JPEG,
|
|
47244
|
+
gif: 'image/gif',
|
|
47245
|
+
tiff: 'image/tiff',
|
|
47246
|
+
svg: 'image/svg+xml',
|
|
47247
|
+
webp: 'image/webp',
|
|
47248
|
+
};
|
|
47249
|
+
function getExtension(url) {
|
|
47250
|
+
const match = /\.([^./]*?)$/g.exec(url);
|
|
47251
|
+
return match ? match[1] : '';
|
|
47252
|
+
}
|
|
47253
|
+
function getMimeType(url) {
|
|
47254
|
+
const extension = getExtension(url).toLowerCase();
|
|
47255
|
+
return mimes[extension] || '';
|
|
47256
|
+
}
|
|
47257
|
+
|
|
47258
|
+
function getContentFromDataUrl(dataURL) {
|
|
47259
|
+
return dataURL.split(/,/)[1];
|
|
47260
|
+
}
|
|
47261
|
+
function isDataUrl(url) {
|
|
47262
|
+
return url.search(/^(data:)/) !== -1;
|
|
47263
|
+
}
|
|
47264
|
+
function makeDataUrl(content, mimeType) {
|
|
47265
|
+
return `data:${mimeType};base64,${content}`;
|
|
47266
|
+
}
|
|
47267
|
+
async function fetchAsDataURL(url, init, process) {
|
|
47268
|
+
const res = await fetch(url, init);
|
|
47269
|
+
if (res.status === 404) {
|
|
47270
|
+
throw new Error(`Resource "${res.url}" not found`);
|
|
47271
|
+
}
|
|
47272
|
+
const blob = await res.blob();
|
|
47273
|
+
return new Promise((resolve, reject) => {
|
|
47274
|
+
const reader = new FileReader();
|
|
47275
|
+
reader.onerror = reject;
|
|
47276
|
+
reader.onloadend = () => {
|
|
47130
47277
|
try {
|
|
47131
|
-
|
|
47132
|
-
dataUrls.set(image.id, await this.blobToDataUrl(blob));
|
|
47278
|
+
resolve(process({ res, result: reader.result }));
|
|
47133
47279
|
}
|
|
47134
|
-
catch (
|
|
47135
|
-
|
|
47280
|
+
catch (error) {
|
|
47281
|
+
reject(error);
|
|
47136
47282
|
}
|
|
47137
|
-
}
|
|
47138
|
-
|
|
47283
|
+
};
|
|
47284
|
+
reader.readAsDataURL(blob);
|
|
47285
|
+
});
|
|
47286
|
+
}
|
|
47287
|
+
const cache = {};
|
|
47288
|
+
function getCacheKey(url, contentType, includeQueryParams) {
|
|
47289
|
+
let key = url.replace(/\?.*/, '');
|
|
47290
|
+
if (includeQueryParams) {
|
|
47291
|
+
key = url;
|
|
47139
47292
|
}
|
|
47140
|
-
|
|
47141
|
-
|
|
47142
|
-
|
|
47143
|
-
|
|
47144
|
-
|
|
47145
|
-
|
|
47146
|
-
|
|
47147
|
-
|
|
47148
|
-
|
|
47149
|
-
|
|
47150
|
-
|
|
47151
|
-
|
|
47152
|
-
|
|
47153
|
-
|
|
47154
|
-
|
|
47155
|
-
|
|
47156
|
-
|
|
47157
|
-
|
|
47158
|
-
|
|
47159
|
-
|
|
47160
|
-
|
|
47161
|
-
|
|
47162
|
-
}
|
|
47163
|
-
catch {
|
|
47164
|
-
// Fall through to the ArrayBuffer-based encoder.
|
|
47293
|
+
// font resource
|
|
47294
|
+
if (/ttf|otf|eot|woff2?/i.test(key)) {
|
|
47295
|
+
key = key.replace(/.*\//, '');
|
|
47296
|
+
}
|
|
47297
|
+
return contentType ? `[${contentType}]${key}` : key;
|
|
47298
|
+
}
|
|
47299
|
+
async function resourceToDataURL(resourceUrl, contentType, options) {
|
|
47300
|
+
const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
|
|
47301
|
+
if (cache[cacheKey] != null) {
|
|
47302
|
+
return cache[cacheKey];
|
|
47303
|
+
}
|
|
47304
|
+
// ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
|
|
47305
|
+
if (options.cacheBust) {
|
|
47306
|
+
// eslint-disable-next-line no-param-reassign
|
|
47307
|
+
resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
|
|
47308
|
+
}
|
|
47309
|
+
let dataURL;
|
|
47310
|
+
try {
|
|
47311
|
+
const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
|
|
47312
|
+
if (!contentType) {
|
|
47313
|
+
// eslint-disable-next-line no-param-reassign
|
|
47314
|
+
contentType = res.headers.get('Content-Type') || '';
|
|
47165
47315
|
}
|
|
47166
|
-
|
|
47167
|
-
|
|
47168
|
-
|
|
47169
|
-
return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
|
|
47316
|
+
return getContentFromDataUrl(result);
|
|
47317
|
+
});
|
|
47318
|
+
dataURL = makeDataUrl(content, contentType);
|
|
47170
47319
|
}
|
|
47171
|
-
|
|
47172
|
-
|
|
47173
|
-
|
|
47174
|
-
|
|
47175
|
-
|
|
47176
|
-
return Buffer.from(bytes).toString('base64');
|
|
47177
|
-
}
|
|
47178
|
-
let binary = '';
|
|
47179
|
-
const chunkSize = 0x8000;
|
|
47180
|
-
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
47181
|
-
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
47182
|
-
binary += String.fromCharCode(...chunk);
|
|
47320
|
+
catch (error) {
|
|
47321
|
+
dataURL = options.imagePlaceholder || '';
|
|
47322
|
+
let msg = `Failed to fetch resource: ${resourceUrl}`;
|
|
47323
|
+
if (error) {
|
|
47324
|
+
msg = typeof error === 'string' ? error : error.message;
|
|
47183
47325
|
}
|
|
47184
|
-
if (
|
|
47185
|
-
|
|
47326
|
+
if (msg) {
|
|
47327
|
+
console.warn(msg);
|
|
47186
47328
|
}
|
|
47187
|
-
throw new Error('No base64 encoder available in this runtime');
|
|
47188
47329
|
}
|
|
47189
|
-
|
|
47190
|
-
|
|
47191
|
-
|
|
47192
|
-
|
|
47193
|
-
|
|
47194
|
-
|
|
47195
|
-
|
|
47196
|
-
|
|
47197
|
-
|
|
47198
|
-
|
|
47199
|
-
|
|
47200
|
-
|
|
47201
|
-
|
|
47202
|
-
|
|
47203
|
-
|
|
47204
|
-
|
|
47205
|
-
|
|
47206
|
-
|
|
47207
|
-
|
|
47208
|
-
|
|
47209
|
-
|
|
47210
|
-
|
|
47211
|
-
|
|
47212
|
-
|
|
47213
|
-
|
|
47214
|
-
|
|
47215
|
-
|
|
47216
|
-
|
|
47217
|
-
|
|
47218
|
-
|
|
47219
|
-
|
|
47220
|
-
const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
|
|
47221
|
-
const path = line.getArrowPath(style);
|
|
47222
|
-
const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
|
|
47223
|
-
const refX = 0;
|
|
47224
|
-
const refY = size / 2;
|
|
47225
|
-
return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
|
|
47226
|
-
<path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
|
|
47227
|
-
</marker>`;
|
|
47228
|
-
}
|
|
47229
|
-
/**
|
|
47230
|
-
* Converts a Kritzel object to its SVG element representation.
|
|
47231
|
-
* @param object - The object to convert
|
|
47232
|
-
* @param theme - Theme for color resolution
|
|
47233
|
-
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
47234
|
-
* @returns SVG element string, or empty string if object type is not supported
|
|
47235
|
-
*/
|
|
47236
|
-
static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
47237
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
|
|
47238
|
-
return this.pathToSvg(object, theme);
|
|
47239
|
-
}
|
|
47240
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
|
|
47241
|
-
return this.lineToSvg(object, theme);
|
|
47242
|
-
}
|
|
47243
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
|
|
47244
|
-
return this.shapeToSvg(object, theme);
|
|
47245
|
-
}
|
|
47246
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
|
|
47247
|
-
return this.textToSvg(object, theme, rasterSafeForCanvas);
|
|
47248
|
-
}
|
|
47249
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
|
|
47250
|
-
return this.imageToSvg(object, imageDataUrls);
|
|
47251
|
-
}
|
|
47252
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
|
|
47253
|
-
return this.customElementToSvg(object, rasterSafeForCanvas);
|
|
47254
|
-
}
|
|
47255
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
47256
|
-
return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
|
|
47257
|
-
}
|
|
47258
|
-
return '';
|
|
47259
|
-
}
|
|
47260
|
-
/**
|
|
47261
|
-
* Converts a KritzelPath to SVG.
|
|
47262
|
-
* @param path - The path object
|
|
47263
|
-
* @param theme - Theme for color resolution
|
|
47264
|
-
* @returns SVG element string
|
|
47265
|
-
*/
|
|
47266
|
-
static pathToSvg(path, theme) {
|
|
47267
|
-
const transform = this.buildTransform(path);
|
|
47268
|
-
const fill = KritzelColorHelper.resolveThemeColor(path.fill, theme);
|
|
47269
|
-
const stroke = KritzelColorHelper.resolveThemeColor(path.stroke, theme);
|
|
47270
|
-
const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
|
|
47271
|
-
const scale = this.getObjectScale(path);
|
|
47272
|
-
// Path needs to be wrapped in a g with translation since path.d uses local coordinates
|
|
47273
|
-
return `<g transform="${transform}"${opacity}>
|
|
47274
|
-
<svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
|
|
47275
|
-
<path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
|
|
47276
|
-
</svg>
|
|
47277
|
-
</g>`;
|
|
47278
|
-
}
|
|
47279
|
-
/**
|
|
47280
|
-
* Converts a KritzelLine to SVG.
|
|
47281
|
-
* @param line - The line object
|
|
47282
|
-
* @param theme - Theme for color resolution
|
|
47283
|
-
* @returns SVG element string
|
|
47284
|
-
*/
|
|
47285
|
-
static lineToSvg(line, theme) {
|
|
47286
|
-
const transform = this.buildTransform(line);
|
|
47287
|
-
const stroke = KritzelColorHelper.resolveThemeColor(line.stroke, theme);
|
|
47288
|
-
const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
|
|
47289
|
-
const scale = this.getObjectScale(line);
|
|
47290
|
-
const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
|
|
47291
|
-
const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
|
|
47292
|
-
return `<g transform="${transform}"${opacity}>
|
|
47293
|
-
<svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
|
|
47294
|
-
<path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
|
|
47295
|
-
</svg>
|
|
47296
|
-
</g>`;
|
|
47297
|
-
}
|
|
47298
|
-
/**
|
|
47299
|
-
* Converts a KritzelShape to SVG.
|
|
47300
|
-
* @param shape - The shape object
|
|
47301
|
-
* @param theme - Theme for color resolution
|
|
47302
|
-
* @returns SVG element string
|
|
47303
|
-
*/
|
|
47304
|
-
static shapeToSvg(shape, theme) {
|
|
47305
|
-
const transform = this.buildTransform(shape);
|
|
47306
|
-
const fill = KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
|
|
47307
|
-
const stroke = KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
|
|
47308
|
-
const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
|
|
47309
|
-
const scale = this.getObjectScale(shape);
|
|
47310
|
-
// Get the SVG path for the shape
|
|
47311
|
-
const pathD = shape.getSvgPath();
|
|
47312
|
-
// Handle text content if present
|
|
47313
|
-
let textContent = '';
|
|
47314
|
-
if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
|
|
47315
|
-
else if (shape.content) {
|
|
47316
|
-
const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
|
|
47317
|
-
if (textHtml) {
|
|
47318
|
-
const fontColor = KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
|
|
47319
|
-
textContent = `
|
|
47320
|
-
<foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
|
|
47321
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-family: ${shape.fontFamily}; font-size: ${shape.fontSize}px; color: ${fontColor}; overflow: hidden;">
|
|
47322
|
-
${textHtml}
|
|
47323
|
-
</div>
|
|
47324
|
-
</foreignObject>`;
|
|
47325
|
-
}
|
|
47326
|
-
}
|
|
47327
|
-
return `<g transform="${transform}"${opacity}>
|
|
47328
|
-
<svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
|
|
47329
|
-
<path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
|
|
47330
|
-
</svg>
|
|
47331
|
-
</g>`;
|
|
47332
|
-
}
|
|
47333
|
-
/**
|
|
47334
|
-
* Converts a KritzelText to SVG using foreignObject for rich text support.
|
|
47335
|
-
* @param text - The text object
|
|
47336
|
-
* @param theme - Theme for color resolution
|
|
47337
|
-
* @returns SVG element string
|
|
47338
|
-
*/
|
|
47339
|
-
static textToSvg(text, theme, rasterSafeForCanvas = false) {
|
|
47340
|
-
if (rasterSafeForCanvas) {
|
|
47341
|
-
return this.textToSvgRasterSafe(text, theme);
|
|
47342
|
-
}
|
|
47343
|
-
const transform = this.buildTransform(text);
|
|
47344
|
-
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
47345
|
-
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
47346
|
-
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
47347
|
-
const scale = this.getObjectScale(text);
|
|
47348
|
-
// Convert ProseMirror content to HTML
|
|
47349
|
-
const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
|
|
47350
|
-
// Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
|
|
47351
|
-
const width = text.totalWidth / scale;
|
|
47352
|
-
const height = text.totalHeight / scale;
|
|
47353
|
-
// Convert the font size from pt to SVG world units (px equivalent).
|
|
47354
|
-
// text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
|
|
47355
|
-
// We also bake in scaleFactor (user resize) and divide by the object scale so
|
|
47356
|
-
// that the rendered font aligns with the world-unit dimensions above.
|
|
47357
|
-
// The CSS transform: scale(scaleFactor) is removed from the div because the
|
|
47358
|
-
// adjusted font-size already accounts for it – keeping both would double-scale
|
|
47359
|
-
// the content and overflow the foreignObject.
|
|
47360
|
-
const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
|
|
47361
|
-
return `<g transform="${transform}"${opacity}>
|
|
47362
|
-
<foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
|
|
47363
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: ${text.fontFamily}; font-size: ${svgFontSizePx}px; color: ${fontColor}; background-color: ${bgColor || 'transparent'}; white-space: pre-wrap; word-wrap: break-word; width: 100%; margin: 0; padding: 0; box-sizing: border-box;">${htmlContent.trim()}</div>
|
|
47364
|
-
</foreignObject>
|
|
47365
|
-
</g>`;
|
|
47366
|
-
}
|
|
47367
|
-
/**
|
|
47368
|
-
* Converts a KritzelText to SVG using pure SVG text primitives.
|
|
47369
|
-
* This path avoids `<foreignObject>`, which taints canvases when the
|
|
47370
|
-
* generated SVG is rasterized through `<img>` and `drawImage`.
|
|
47371
|
-
*/
|
|
47372
|
-
static textToSvgRasterSafe(text, theme) {
|
|
47373
|
-
const transform = this.buildTransform(text);
|
|
47374
|
-
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
47375
|
-
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
47376
|
-
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
47377
|
-
const scale = this.getObjectScale(text);
|
|
47378
|
-
const width = text.totalWidth / scale;
|
|
47379
|
-
const height = text.totalHeight / scale;
|
|
47380
|
-
const lines = this.prosemirrorToPlainTextLines(text.content);
|
|
47381
|
-
const resolvedLines = lines.length > 0 ? lines : [''];
|
|
47382
|
-
// Convert the font size from pt to SVG world units (px equivalent).
|
|
47383
|
-
// text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
|
|
47384
|
-
// We also bake in scaleFactor (user resize) and divide by the object scale so
|
|
47385
|
-
// that the rendered lines align with the world-unit bounding box dimensions above.
|
|
47386
|
-
const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
|
|
47387
|
-
const lineHeight = resolvedFontSizePx * 1.2;
|
|
47388
|
-
const startY = resolvedFontSizePx;
|
|
47389
|
-
const bgRect = bgColor && bgColor !== 'transparent'
|
|
47390
|
-
? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
|
|
47391
|
-
: '';
|
|
47392
|
-
const tspans = resolvedLines
|
|
47393
|
-
.map((line, index) => {
|
|
47394
|
-
const y = startY + index * lineHeight;
|
|
47395
|
-
return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
|
|
47396
|
-
})
|
|
47397
|
-
.join('');
|
|
47398
|
-
return `<g transform="${transform}"${opacity}>
|
|
47399
|
-
${bgRect}
|
|
47400
|
-
<text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
|
|
47401
|
-
</g>`;
|
|
47402
|
-
}
|
|
47403
|
-
/**
|
|
47404
|
-
* Converts a KritzelImage to SVG.
|
|
47405
|
-
*
|
|
47406
|
-
* Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
|
|
47407
|
-
* id) when provided, so the embedded bytes survive rasterization to
|
|
47408
|
-
* PNG. Falls back to `resolvedSrc` (populated by the renderer via the
|
|
47409
|
-
* asset resolver) and finally the legacy inline `src` field for
|
|
47410
|
-
* documents persisted before the asset layer existed.
|
|
47411
|
-
*
|
|
47412
|
-
* Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
|
|
47413
|
-
* a broken-image placeholder once the SVG is loaded into an `<img>` and
|
|
47414
|
-
* drawn to a canvas. Callers that rasterize the SVG must pass
|
|
47415
|
-
* `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
|
|
47416
|
-
* @param image - The image object
|
|
47417
|
-
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
47418
|
-
* @returns SVG element string
|
|
47419
|
-
*/
|
|
47420
|
-
static imageToSvg(image, imageDataUrls) {
|
|
47421
|
-
const transform = this.buildTransform(image);
|
|
47422
|
-
const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
|
|
47423
|
-
const scale = this.getObjectScale(image);
|
|
47424
|
-
// Calculate dimensions
|
|
47425
|
-
const width = image.totalWidth / scale;
|
|
47426
|
-
const height = image.totalHeight / scale;
|
|
47427
|
-
const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
|
|
47428
|
-
return `<g transform="${transform}"${opacity}>
|
|
47429
|
-
<image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
|
|
47430
|
-
</g>`;
|
|
47431
|
-
}
|
|
47432
|
-
/**
|
|
47433
|
-
* Converts a KritzelCustomElement to SVG.
|
|
47434
|
-
* SVG mode: embeds HTML in foreignObject (best-effort).
|
|
47435
|
-
* PNG/raster mode: renders placeholder rect with warning.
|
|
47436
|
-
* @param obj - The custom element object
|
|
47437
|
-
* @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
|
|
47438
|
-
* @returns SVG element string
|
|
47439
|
-
*/
|
|
47440
|
-
static customElementToSvg(obj, rasterSafeForCanvas = false) {
|
|
47441
|
-
const transform = this.buildTransform(obj);
|
|
47442
|
-
const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
|
|
47443
|
-
const scale = this.getObjectScale(obj);
|
|
47444
|
-
const width = obj.totalWidth / scale;
|
|
47445
|
-
const height = obj.totalHeight / scale;
|
|
47446
|
-
if (rasterSafeForCanvas) {
|
|
47447
|
-
// PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
|
|
47448
|
-
console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
|
|
47449
|
-
return `<g transform="${transform}"${opacity}>
|
|
47450
|
-
<rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
|
|
47451
|
-
<text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
|
|
47452
|
-
</g>`;
|
|
47453
|
-
}
|
|
47454
|
-
// SVG export: use foreignObject to embed HTML
|
|
47455
|
-
const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
|
|
47456
|
-
return `<g transform="${transform}"${opacity}>
|
|
47457
|
-
<foreignObject x="0" y="0" width="${width}" height="${height}">
|
|
47458
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
|
|
47459
|
-
${html}
|
|
47460
|
-
</div>
|
|
47461
|
-
</foreignObject>
|
|
47462
|
-
</g>`;
|
|
47463
|
-
}
|
|
47464
|
-
/**
|
|
47465
|
-
* Converts a KritzelGroup to SVG by recursively converting children.
|
|
47466
|
-
* @param group - The group object
|
|
47467
|
-
* @param theme - Theme for color resolution
|
|
47468
|
-
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
47469
|
-
* @returns SVG element string
|
|
47470
|
-
*/
|
|
47471
|
-
static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
47472
|
-
const children = group.children
|
|
47473
|
-
.sort((a, b) => a.zIndex - b.zIndex)
|
|
47474
|
-
.map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
|
|
47475
|
-
.filter(Boolean)
|
|
47476
|
-
.join('\n ');
|
|
47477
|
-
if (!children) {
|
|
47478
|
-
return '';
|
|
47479
|
-
}
|
|
47480
|
-
const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
|
|
47481
|
-
return `<g${opacity}>
|
|
47482
|
-
${children}
|
|
47483
|
-
</g>`;
|
|
47484
|
-
}
|
|
47485
|
-
/**
|
|
47486
|
-
* Builds a transform attribute string for an object.
|
|
47487
|
-
* Combines translation and rotation.
|
|
47488
|
-
* @param object - The object to build transform for
|
|
47489
|
-
* @returns Transform attribute value string
|
|
47490
|
-
*/
|
|
47491
|
-
static buildTransform(object) {
|
|
47492
|
-
const transforms = [];
|
|
47493
|
-
const scale = this.getObjectScale(object);
|
|
47494
|
-
// Translation
|
|
47495
|
-
transforms.push(`translate(${object.translateX}, ${object.translateY})`);
|
|
47496
|
-
// Rotation around center
|
|
47497
|
-
if (object.rotation !== 0) {
|
|
47498
|
-
const centerX = object.totalWidth / 2 / scale;
|
|
47499
|
-
const centerY = object.totalHeight / 2 / scale;
|
|
47500
|
-
const degrees = object.rotation * (180 / Math.PI);
|
|
47501
|
-
transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
|
|
47502
|
-
}
|
|
47503
|
-
return transforms.join(' ');
|
|
47504
|
-
}
|
|
47505
|
-
/**
|
|
47506
|
-
* Returns a safe non-zero scale for export geometry calculations.
|
|
47507
|
-
*/
|
|
47508
|
-
static getObjectScale(object) {
|
|
47509
|
-
const scale = object.scale ?? 1;
|
|
47510
|
-
return scale === 0 ? 1 : scale;
|
|
47511
|
-
}
|
|
47512
|
-
/**
|
|
47513
|
-
* Converts ProseMirror JSON content to HTML string.
|
|
47514
|
-
* @param content - ProseMirror document JSON
|
|
47515
|
-
* @param context - The text or shape object for styling context
|
|
47516
|
-
* @param theme - Theme for color resolution
|
|
47517
|
-
* @returns HTML string
|
|
47518
|
-
*/
|
|
47519
|
-
static prosemirrorToHtml(content, _context, theme) {
|
|
47520
|
-
if (!content || !content.content) {
|
|
47521
|
-
return '';
|
|
47522
|
-
}
|
|
47523
|
-
return content.content
|
|
47524
|
-
.map((node) => this.nodeToHtml(node, theme))
|
|
47525
|
-
.join('');
|
|
47526
|
-
}
|
|
47527
|
-
/**
|
|
47528
|
-
* Converts a ProseMirror node to HTML.
|
|
47529
|
-
* @param node - ProseMirror node JSON
|
|
47530
|
-
* @param theme - Theme for color resolution
|
|
47531
|
-
* @returns HTML string
|
|
47532
|
-
*/
|
|
47533
|
-
static nodeToHtml(node, theme) {
|
|
47534
|
-
if (!node)
|
|
47535
|
-
return '';
|
|
47536
|
-
switch (node.type) {
|
|
47537
|
-
case 'paragraph':
|
|
47538
|
-
const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
47539
|
-
return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
|
|
47540
|
-
case 'text':
|
|
47541
|
-
let text = this.escapeHtml(node.text || '');
|
|
47542
|
-
if (node.marks) {
|
|
47543
|
-
for (const mark of node.marks) {
|
|
47544
|
-
text = this.applyMark(text, mark, theme);
|
|
47545
|
-
}
|
|
47546
|
-
}
|
|
47547
|
-
return text;
|
|
47548
|
-
case 'hard_break':
|
|
47549
|
-
return '<br/>';
|
|
47550
|
-
case 'bullet_list':
|
|
47551
|
-
const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
47552
|
-
return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
|
|
47553
|
-
case 'ordered_list':
|
|
47554
|
-
const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
47555
|
-
return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
|
|
47556
|
-
case 'list_item':
|
|
47557
|
-
const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
47558
|
-
return `<li>${liContent}</li>`;
|
|
47559
|
-
default:
|
|
47560
|
-
// For unknown node types, try to render content
|
|
47561
|
-
if (node.content) {
|
|
47562
|
-
return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
|
|
47563
|
-
}
|
|
47564
|
-
return '';
|
|
47565
|
-
}
|
|
47566
|
-
}
|
|
47567
|
-
/**
|
|
47568
|
-
* Converts ProseMirror JSON content to plain-text lines.
|
|
47569
|
-
* Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
|
|
47570
|
-
*/
|
|
47571
|
-
static prosemirrorToPlainTextLines(content) {
|
|
47572
|
-
if (!content || !Array.isArray(content.content)) {
|
|
47573
|
-
return [];
|
|
47574
|
-
}
|
|
47575
|
-
const plainText = content.content
|
|
47576
|
-
.map((node) => this.nodeToPlainText(node))
|
|
47577
|
-
.join('')
|
|
47578
|
-
.replace(/\r\n/g, '\n')
|
|
47579
|
-
.replace(/\n+$/, '');
|
|
47580
|
-
if (!plainText) {
|
|
47581
|
-
return [];
|
|
47582
|
-
}
|
|
47583
|
-
return plainText.split('\n');
|
|
47584
|
-
}
|
|
47585
|
-
/**
|
|
47586
|
-
* Converts a ProseMirror node tree into plain text.
|
|
47587
|
-
*/
|
|
47588
|
-
static nodeToPlainText(node) {
|
|
47589
|
-
if (!node) {
|
|
47590
|
-
return '';
|
|
47591
|
-
}
|
|
47592
|
-
switch (node.type) {
|
|
47593
|
-
case 'text':
|
|
47594
|
-
return node.text || '';
|
|
47595
|
-
case 'hard_break':
|
|
47596
|
-
return '\n';
|
|
47597
|
-
case 'paragraph': {
|
|
47598
|
-
const paragraph = Array.isArray(node.content)
|
|
47599
|
-
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
47600
|
-
: '';
|
|
47601
|
-
return `${paragraph}\n`;
|
|
47602
|
-
}
|
|
47603
|
-
case 'list_item': {
|
|
47604
|
-
const item = Array.isArray(node.content)
|
|
47605
|
-
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
47606
|
-
: '';
|
|
47607
|
-
return `- ${item.trim()}\n`;
|
|
47608
|
-
}
|
|
47609
|
-
default:
|
|
47610
|
-
return Array.isArray(node.content)
|
|
47611
|
-
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
47612
|
-
: '';
|
|
47613
|
-
}
|
|
47614
|
-
}
|
|
47615
|
-
/**
|
|
47616
|
-
* Applies a ProseMirror mark to text.
|
|
47617
|
-
* @param text - The text to wrap
|
|
47618
|
-
* @param mark - The mark to apply
|
|
47619
|
-
* @param theme - Theme for color resolution
|
|
47620
|
-
* @returns Wrapped HTML string
|
|
47621
|
-
*/
|
|
47622
|
-
static applyMark(text, mark, _theme) {
|
|
47623
|
-
switch (mark.type) {
|
|
47624
|
-
case 'bold':
|
|
47625
|
-
case 'strong':
|
|
47626
|
-
return `<strong>${text}</strong>`;
|
|
47627
|
-
case 'italic':
|
|
47628
|
-
case 'em':
|
|
47629
|
-
return `<em>${text}</em>`;
|
|
47630
|
-
case 'underline':
|
|
47631
|
-
return `<u>${text}</u>`;
|
|
47632
|
-
case 'strike':
|
|
47633
|
-
return `<s>${text}</s>`;
|
|
47634
|
-
case 'code':
|
|
47635
|
-
return `<code>${text}</code>`;
|
|
47636
|
-
case 'link':
|
|
47637
|
-
const href = mark.attrs?.href || '#';
|
|
47638
|
-
return `<a href="${this.escapeHtml(href)}">${text}</a>`;
|
|
47639
|
-
default:
|
|
47640
|
-
return text;
|
|
47641
|
-
}
|
|
47642
|
-
}
|
|
47643
|
-
/**
|
|
47644
|
-
* Escapes HTML special characters.
|
|
47645
|
-
* @param str - String to escape
|
|
47646
|
-
* @returns Escaped string
|
|
47647
|
-
*/
|
|
47648
|
-
static escapeHtml(str) {
|
|
47649
|
-
return str
|
|
47650
|
-
.replace(/&/g, '&')
|
|
47651
|
-
.replace(/</g, '<')
|
|
47652
|
-
.replace(/>/g, '>')
|
|
47653
|
-
.replace(/"/g, '"')
|
|
47654
|
-
.replace(/'/g, ''');
|
|
47655
|
-
}
|
|
47656
|
-
}
|
|
47657
|
-
|
|
47658
|
-
function resolveUrl(url, baseUrl) {
|
|
47659
|
-
// url is absolute already
|
|
47660
|
-
if (url.match(/^[a-z]+:\/\//i)) {
|
|
47661
|
-
return url;
|
|
47662
|
-
}
|
|
47663
|
-
// url is absolute already, without protocol
|
|
47664
|
-
if (url.match(/^\/\//)) {
|
|
47665
|
-
return window.location.protocol + url;
|
|
47666
|
-
}
|
|
47667
|
-
// dataURI, mailto:, tel:, etc.
|
|
47668
|
-
if (url.match(/^[a-z]+:/i)) {
|
|
47669
|
-
return url;
|
|
47670
|
-
}
|
|
47671
|
-
const doc = document.implementation.createHTMLDocument();
|
|
47672
|
-
const base = doc.createElement('base');
|
|
47673
|
-
const a = doc.createElement('a');
|
|
47674
|
-
doc.head.appendChild(base);
|
|
47675
|
-
doc.body.appendChild(a);
|
|
47676
|
-
if (baseUrl) {
|
|
47677
|
-
base.href = baseUrl;
|
|
47678
|
-
}
|
|
47679
|
-
a.href = url;
|
|
47680
|
-
return a.href;
|
|
47681
|
-
}
|
|
47682
|
-
const uuid = (() => {
|
|
47683
|
-
// generate uuid for className of pseudo elements.
|
|
47684
|
-
// We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
|
|
47685
|
-
let counter = 0;
|
|
47686
|
-
// ref: http://stackoverflow.com/a/6248722/2519373
|
|
47687
|
-
const random = () =>
|
|
47688
|
-
// eslint-disable-next-line no-bitwise
|
|
47689
|
-
`0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
|
|
47690
|
-
return () => {
|
|
47691
|
-
counter += 1;
|
|
47692
|
-
return `u${random()}${counter}`;
|
|
47693
|
-
};
|
|
47694
|
-
})();
|
|
47695
|
-
function toArray(arrayLike) {
|
|
47696
|
-
const arr = [];
|
|
47697
|
-
for (let i = 0, l = arrayLike.length; i < l; i++) {
|
|
47698
|
-
arr.push(arrayLike[i]);
|
|
47699
|
-
}
|
|
47700
|
-
return arr;
|
|
47701
|
-
}
|
|
47702
|
-
let styleProps = null;
|
|
47703
|
-
function getStyleProperties(options = {}) {
|
|
47704
|
-
if (styleProps) {
|
|
47705
|
-
return styleProps;
|
|
47706
|
-
}
|
|
47707
|
-
if (options.includeStyleProperties) {
|
|
47708
|
-
styleProps = options.includeStyleProperties;
|
|
47709
|
-
return styleProps;
|
|
47710
|
-
}
|
|
47711
|
-
styleProps = toArray(window.getComputedStyle(document.documentElement));
|
|
47712
|
-
return styleProps;
|
|
47713
|
-
}
|
|
47714
|
-
function px(node, styleProperty) {
|
|
47715
|
-
const win = node.ownerDocument.defaultView || window;
|
|
47716
|
-
const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
|
|
47717
|
-
return val ? parseFloat(val.replace('px', '')) : 0;
|
|
47718
|
-
}
|
|
47719
|
-
function getNodeWidth(node) {
|
|
47720
|
-
const leftBorder = px(node, 'border-left-width');
|
|
47721
|
-
const rightBorder = px(node, 'border-right-width');
|
|
47722
|
-
return node.clientWidth + leftBorder + rightBorder;
|
|
47723
|
-
}
|
|
47724
|
-
function getNodeHeight(node) {
|
|
47725
|
-
const topBorder = px(node, 'border-top-width');
|
|
47726
|
-
const bottomBorder = px(node, 'border-bottom-width');
|
|
47727
|
-
return node.clientHeight + topBorder + bottomBorder;
|
|
47728
|
-
}
|
|
47729
|
-
function getImageSize(targetNode, options = {}) {
|
|
47730
|
-
const width = options.width || getNodeWidth(targetNode);
|
|
47731
|
-
const height = options.height || getNodeHeight(targetNode);
|
|
47732
|
-
return { width, height };
|
|
47733
|
-
}
|
|
47734
|
-
function getPixelRatio() {
|
|
47735
|
-
let ratio;
|
|
47736
|
-
let FINAL_PROCESS;
|
|
47737
|
-
try {
|
|
47738
|
-
FINAL_PROCESS = process;
|
|
47739
|
-
}
|
|
47740
|
-
catch (e) {
|
|
47741
|
-
// pass
|
|
47742
|
-
}
|
|
47743
|
-
const val = FINAL_PROCESS && FINAL_PROCESS.env
|
|
47744
|
-
? FINAL_PROCESS.env.devicePixelRatio
|
|
47745
|
-
: null;
|
|
47746
|
-
if (val) {
|
|
47747
|
-
ratio = parseInt(val, 10);
|
|
47748
|
-
if (Number.isNaN(ratio)) {
|
|
47749
|
-
ratio = 1;
|
|
47750
|
-
}
|
|
47751
|
-
}
|
|
47752
|
-
return ratio || window.devicePixelRatio || 1;
|
|
47753
|
-
}
|
|
47754
|
-
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
|
47755
|
-
const canvasDimensionLimit = 16384;
|
|
47756
|
-
function checkCanvasDimensions(canvas) {
|
|
47757
|
-
if (canvas.width > canvasDimensionLimit ||
|
|
47758
|
-
canvas.height > canvasDimensionLimit) {
|
|
47759
|
-
if (canvas.width > canvasDimensionLimit &&
|
|
47760
|
-
canvas.height > canvasDimensionLimit) {
|
|
47761
|
-
if (canvas.width > canvas.height) {
|
|
47762
|
-
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
47763
|
-
canvas.width = canvasDimensionLimit;
|
|
47764
|
-
}
|
|
47765
|
-
else {
|
|
47766
|
-
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
47767
|
-
canvas.height = canvasDimensionLimit;
|
|
47768
|
-
}
|
|
47769
|
-
}
|
|
47770
|
-
else if (canvas.width > canvasDimensionLimit) {
|
|
47771
|
-
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
47772
|
-
canvas.width = canvasDimensionLimit;
|
|
47773
|
-
}
|
|
47774
|
-
else {
|
|
47775
|
-
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
47776
|
-
canvas.height = canvasDimensionLimit;
|
|
47777
|
-
}
|
|
47778
|
-
}
|
|
47779
|
-
}
|
|
47780
|
-
function createImage(url) {
|
|
47781
|
-
return new Promise((resolve, reject) => {
|
|
47782
|
-
const img = new Image();
|
|
47783
|
-
img.onload = () => {
|
|
47784
|
-
img.decode().then(() => {
|
|
47785
|
-
requestAnimationFrame(() => resolve(img));
|
|
47786
|
-
});
|
|
47787
|
-
};
|
|
47788
|
-
img.onerror = reject;
|
|
47789
|
-
img.crossOrigin = 'anonymous';
|
|
47790
|
-
img.decoding = 'async';
|
|
47791
|
-
img.src = url;
|
|
47792
|
-
});
|
|
47793
|
-
}
|
|
47794
|
-
async function svgToDataURL(svg) {
|
|
47795
|
-
return Promise.resolve()
|
|
47796
|
-
.then(() => new XMLSerializer().serializeToString(svg))
|
|
47797
|
-
.then(encodeURIComponent)
|
|
47798
|
-
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
|
|
47799
|
-
}
|
|
47800
|
-
async function nodeToDataURL(node, width, height) {
|
|
47801
|
-
const xmlns = 'http://www.w3.org/2000/svg';
|
|
47802
|
-
const svg = document.createElementNS(xmlns, 'svg');
|
|
47803
|
-
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
|
47804
|
-
svg.setAttribute('width', `${width}`);
|
|
47805
|
-
svg.setAttribute('height', `${height}`);
|
|
47806
|
-
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
47807
|
-
foreignObject.setAttribute('width', '100%');
|
|
47808
|
-
foreignObject.setAttribute('height', '100%');
|
|
47809
|
-
foreignObject.setAttribute('x', '0');
|
|
47810
|
-
foreignObject.setAttribute('y', '0');
|
|
47811
|
-
foreignObject.setAttribute('externalResourcesRequired', 'true');
|
|
47812
|
-
svg.appendChild(foreignObject);
|
|
47813
|
-
foreignObject.appendChild(node);
|
|
47814
|
-
return svgToDataURL(svg);
|
|
47815
|
-
}
|
|
47816
|
-
const isInstanceOfElement = (node, instance) => {
|
|
47817
|
-
if (node instanceof instance)
|
|
47818
|
-
return true;
|
|
47819
|
-
const nodePrototype = Object.getPrototypeOf(node);
|
|
47820
|
-
if (nodePrototype === null)
|
|
47821
|
-
return false;
|
|
47822
|
-
return (nodePrototype.constructor.name === instance.name ||
|
|
47823
|
-
isInstanceOfElement(nodePrototype, instance));
|
|
47824
|
-
};
|
|
47825
|
-
|
|
47826
|
-
function formatCSSText(style) {
|
|
47827
|
-
const content = style.getPropertyValue('content');
|
|
47828
|
-
return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
|
|
47829
|
-
}
|
|
47830
|
-
function formatCSSProperties(style, options) {
|
|
47831
|
-
return getStyleProperties(options)
|
|
47832
|
-
.map((name) => {
|
|
47833
|
-
const value = style.getPropertyValue(name);
|
|
47834
|
-
const priority = style.getPropertyPriority(name);
|
|
47835
|
-
return `${name}: ${value}${priority ? ' !important' : ''};`;
|
|
47836
|
-
})
|
|
47837
|
-
.join(' ');
|
|
47838
|
-
}
|
|
47839
|
-
function getPseudoElementStyle(className, pseudo, style, options) {
|
|
47840
|
-
const selector = `.${className}:${pseudo}`;
|
|
47841
|
-
const cssText = style.cssText
|
|
47842
|
-
? formatCSSText(style)
|
|
47843
|
-
: formatCSSProperties(style, options);
|
|
47844
|
-
return document.createTextNode(`${selector}{${cssText}}`);
|
|
47845
|
-
}
|
|
47846
|
-
function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
|
|
47847
|
-
const style = window.getComputedStyle(nativeNode, pseudo);
|
|
47848
|
-
const content = style.getPropertyValue('content');
|
|
47849
|
-
if (content === '' || content === 'none') {
|
|
47850
|
-
return;
|
|
47851
|
-
}
|
|
47852
|
-
const className = uuid();
|
|
47853
|
-
try {
|
|
47854
|
-
clonedNode.className = `${clonedNode.className} ${className}`;
|
|
47855
|
-
}
|
|
47856
|
-
catch (err) {
|
|
47857
|
-
return;
|
|
47858
|
-
}
|
|
47859
|
-
const styleElement = document.createElement('style');
|
|
47860
|
-
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
|
|
47861
|
-
clonedNode.appendChild(styleElement);
|
|
47862
|
-
}
|
|
47863
|
-
function clonePseudoElements(nativeNode, clonedNode, options) {
|
|
47864
|
-
clonePseudoElement(nativeNode, clonedNode, ':before', options);
|
|
47865
|
-
clonePseudoElement(nativeNode, clonedNode, ':after', options);
|
|
47866
|
-
}
|
|
47867
|
-
|
|
47868
|
-
const WOFF = 'application/font-woff';
|
|
47869
|
-
const JPEG = 'image/jpeg';
|
|
47870
|
-
const mimes = {
|
|
47871
|
-
woff: WOFF,
|
|
47872
|
-
woff2: WOFF,
|
|
47873
|
-
ttf: 'application/font-truetype',
|
|
47874
|
-
eot: 'application/vnd.ms-fontobject',
|
|
47875
|
-
png: 'image/png',
|
|
47876
|
-
jpg: JPEG,
|
|
47877
|
-
jpeg: JPEG,
|
|
47878
|
-
gif: 'image/gif',
|
|
47879
|
-
tiff: 'image/tiff',
|
|
47880
|
-
svg: 'image/svg+xml',
|
|
47881
|
-
webp: 'image/webp',
|
|
47882
|
-
};
|
|
47883
|
-
function getExtension(url) {
|
|
47884
|
-
const match = /\.([^./]*?)$/g.exec(url);
|
|
47885
|
-
return match ? match[1] : '';
|
|
47886
|
-
}
|
|
47887
|
-
function getMimeType(url) {
|
|
47888
|
-
const extension = getExtension(url).toLowerCase();
|
|
47889
|
-
return mimes[extension] || '';
|
|
47890
|
-
}
|
|
47891
|
-
|
|
47892
|
-
function getContentFromDataUrl(dataURL) {
|
|
47893
|
-
return dataURL.split(/,/)[1];
|
|
47894
|
-
}
|
|
47895
|
-
function isDataUrl(url) {
|
|
47896
|
-
return url.search(/^(data:)/) !== -1;
|
|
47897
|
-
}
|
|
47898
|
-
function makeDataUrl(content, mimeType) {
|
|
47899
|
-
return `data:${mimeType};base64,${content}`;
|
|
47900
|
-
}
|
|
47901
|
-
async function fetchAsDataURL(url, init, process) {
|
|
47902
|
-
const res = await fetch(url, init);
|
|
47903
|
-
if (res.status === 404) {
|
|
47904
|
-
throw new Error(`Resource "${res.url}" not found`);
|
|
47905
|
-
}
|
|
47906
|
-
const blob = await res.blob();
|
|
47907
|
-
return new Promise((resolve, reject) => {
|
|
47908
|
-
const reader = new FileReader();
|
|
47909
|
-
reader.onerror = reject;
|
|
47910
|
-
reader.onloadend = () => {
|
|
47911
|
-
try {
|
|
47912
|
-
resolve(process({ res, result: reader.result }));
|
|
47913
|
-
}
|
|
47914
|
-
catch (error) {
|
|
47915
|
-
reject(error);
|
|
47916
|
-
}
|
|
47917
|
-
};
|
|
47918
|
-
reader.readAsDataURL(blob);
|
|
47919
|
-
});
|
|
47920
|
-
}
|
|
47921
|
-
const cache = {};
|
|
47922
|
-
function getCacheKey(url, contentType, includeQueryParams) {
|
|
47923
|
-
let key = url.replace(/\?.*/, '');
|
|
47924
|
-
if (includeQueryParams) {
|
|
47925
|
-
key = url;
|
|
47926
|
-
}
|
|
47927
|
-
// font resource
|
|
47928
|
-
if (/ttf|otf|eot|woff2?/i.test(key)) {
|
|
47929
|
-
key = key.replace(/.*\//, '');
|
|
47930
|
-
}
|
|
47931
|
-
return contentType ? `[${contentType}]${key}` : key;
|
|
47932
|
-
}
|
|
47933
|
-
async function resourceToDataURL(resourceUrl, contentType, options) {
|
|
47934
|
-
const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
|
|
47935
|
-
if (cache[cacheKey] != null) {
|
|
47936
|
-
return cache[cacheKey];
|
|
47937
|
-
}
|
|
47938
|
-
// ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
|
|
47939
|
-
if (options.cacheBust) {
|
|
47940
|
-
// eslint-disable-next-line no-param-reassign
|
|
47941
|
-
resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
|
|
47942
|
-
}
|
|
47943
|
-
let dataURL;
|
|
47944
|
-
try {
|
|
47945
|
-
const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
|
|
47946
|
-
if (!contentType) {
|
|
47947
|
-
// eslint-disable-next-line no-param-reassign
|
|
47948
|
-
contentType = res.headers.get('Content-Type') || '';
|
|
47949
|
-
}
|
|
47950
|
-
return getContentFromDataUrl(result);
|
|
47951
|
-
});
|
|
47952
|
-
dataURL = makeDataUrl(content, contentType);
|
|
47953
|
-
}
|
|
47954
|
-
catch (error) {
|
|
47955
|
-
dataURL = options.imagePlaceholder || '';
|
|
47956
|
-
let msg = `Failed to fetch resource: ${resourceUrl}`;
|
|
47957
|
-
if (error) {
|
|
47958
|
-
msg = typeof error === 'string' ? error : error.message;
|
|
47959
|
-
}
|
|
47960
|
-
if (msg) {
|
|
47961
|
-
console.warn(msg);
|
|
47962
|
-
}
|
|
47963
|
-
}
|
|
47964
|
-
cache[cacheKey] = dataURL;
|
|
47965
|
-
return dataURL;
|
|
47966
|
-
}
|
|
47967
|
-
|
|
47968
|
-
async function cloneCanvasElement(canvas) {
|
|
47969
|
-
const dataURL = canvas.toDataURL();
|
|
47970
|
-
if (dataURL === 'data:,') {
|
|
47971
|
-
return canvas.cloneNode(false);
|
|
47972
|
-
}
|
|
47973
|
-
return createImage(dataURL);
|
|
47974
|
-
}
|
|
47975
|
-
async function cloneVideoElement(video, options) {
|
|
47976
|
-
if (video.currentSrc) {
|
|
47977
|
-
const canvas = document.createElement('canvas');
|
|
47978
|
-
const ctx = canvas.getContext('2d');
|
|
47979
|
-
canvas.width = video.clientWidth;
|
|
47980
|
-
canvas.height = video.clientHeight;
|
|
47981
|
-
ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
47982
|
-
const dataURL = canvas.toDataURL();
|
|
47983
|
-
return createImage(dataURL);
|
|
47984
|
-
}
|
|
47985
|
-
const poster = video.poster;
|
|
47986
|
-
const contentType = getMimeType(poster);
|
|
47987
|
-
const dataURL = await resourceToDataURL(poster, contentType, options);
|
|
47988
|
-
return createImage(dataURL);
|
|
47989
|
-
}
|
|
47990
|
-
async function cloneIFrameElement(iframe, options) {
|
|
47991
|
-
var _a;
|
|
47992
|
-
try {
|
|
47993
|
-
if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
|
|
47994
|
-
return (await cloneNode(iframe.contentDocument.body, options, true));
|
|
47330
|
+
cache[cacheKey] = dataURL;
|
|
47331
|
+
return dataURL;
|
|
47332
|
+
}
|
|
47333
|
+
|
|
47334
|
+
async function cloneCanvasElement(canvas) {
|
|
47335
|
+
const dataURL = canvas.toDataURL();
|
|
47336
|
+
if (dataURL === 'data:,') {
|
|
47337
|
+
return canvas.cloneNode(false);
|
|
47338
|
+
}
|
|
47339
|
+
return createImage(dataURL);
|
|
47340
|
+
}
|
|
47341
|
+
async function cloneVideoElement(video, options) {
|
|
47342
|
+
if (video.currentSrc) {
|
|
47343
|
+
const canvas = document.createElement('canvas');
|
|
47344
|
+
const ctx = canvas.getContext('2d');
|
|
47345
|
+
canvas.width = video.clientWidth;
|
|
47346
|
+
canvas.height = video.clientHeight;
|
|
47347
|
+
ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
47348
|
+
const dataURL = canvas.toDataURL();
|
|
47349
|
+
return createImage(dataURL);
|
|
47350
|
+
}
|
|
47351
|
+
const poster = video.poster;
|
|
47352
|
+
const contentType = getMimeType(poster);
|
|
47353
|
+
const dataURL = await resourceToDataURL(poster, contentType, options);
|
|
47354
|
+
return createImage(dataURL);
|
|
47355
|
+
}
|
|
47356
|
+
async function cloneIFrameElement(iframe, options) {
|
|
47357
|
+
var _a;
|
|
47358
|
+
try {
|
|
47359
|
+
if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
|
|
47360
|
+
return (await cloneNode(iframe.contentDocument.body, options, true));
|
|
47995
47361
|
}
|
|
47996
47362
|
}
|
|
47997
47363
|
catch (_b) {
|
|
@@ -48488,160 +47854,812 @@ async function embedWebFonts(clonedNode, options) {
|
|
|
48488
47854
|
if (clonedNode.firstChild) {
|
|
48489
47855
|
clonedNode.insertBefore(styleNode, clonedNode.firstChild);
|
|
48490
47856
|
}
|
|
48491
|
-
else {
|
|
48492
|
-
clonedNode.appendChild(styleNode);
|
|
47857
|
+
else {
|
|
47858
|
+
clonedNode.appendChild(styleNode);
|
|
47859
|
+
}
|
|
47860
|
+
}
|
|
47861
|
+
}
|
|
47862
|
+
|
|
47863
|
+
async function toSvg(node, options = {}) {
|
|
47864
|
+
const { width, height } = getImageSize(node, options);
|
|
47865
|
+
const clonedNode = (await cloneNode(node, options, true));
|
|
47866
|
+
await embedWebFonts(clonedNode, options);
|
|
47867
|
+
await embedImages(clonedNode, options);
|
|
47868
|
+
applyStyle(clonedNode, options);
|
|
47869
|
+
const datauri = await nodeToDataURL(clonedNode, width, height);
|
|
47870
|
+
return datauri;
|
|
47871
|
+
}
|
|
47872
|
+
async function toCanvas(node, options = {}) {
|
|
47873
|
+
const { width, height } = getImageSize(node, options);
|
|
47874
|
+
const svg = await toSvg(node, options);
|
|
47875
|
+
const img = await createImage(svg);
|
|
47876
|
+
const canvas = document.createElement('canvas');
|
|
47877
|
+
const context = canvas.getContext('2d');
|
|
47878
|
+
const ratio = options.pixelRatio || getPixelRatio();
|
|
47879
|
+
const canvasWidth = options.canvasWidth || width;
|
|
47880
|
+
const canvasHeight = options.canvasHeight || height;
|
|
47881
|
+
canvas.width = canvasWidth * ratio;
|
|
47882
|
+
canvas.height = canvasHeight * ratio;
|
|
47883
|
+
if (!options.skipAutoScale) {
|
|
47884
|
+
checkCanvasDimensions(canvas);
|
|
47885
|
+
}
|
|
47886
|
+
canvas.style.width = `${canvasWidth}`;
|
|
47887
|
+
canvas.style.height = `${canvasHeight}`;
|
|
47888
|
+
if (options.backgroundColor) {
|
|
47889
|
+
context.fillStyle = options.backgroundColor;
|
|
47890
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
47891
|
+
}
|
|
47892
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
47893
|
+
return canvas;
|
|
47894
|
+
}
|
|
47895
|
+
async function toPng(node, options = {}) {
|
|
47896
|
+
const canvas = await toCanvas(node, options);
|
|
47897
|
+
return canvas.toDataURL();
|
|
47898
|
+
}
|
|
47899
|
+
|
|
47900
|
+
class KritzelWheelHelper {
|
|
47901
|
+
static shouldKeepWheelInCustomElement(ev, customElement) {
|
|
47902
|
+
// Keep Ctrl+wheel available for viewport zoom gestures.
|
|
47903
|
+
if (ev.ctrlKey || customElement.isSelected !== true) {
|
|
47904
|
+
return false;
|
|
47905
|
+
}
|
|
47906
|
+
const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
|
|
47907
|
+
const scrollContainer = this.getScrollableContainerFromEventPath(path, customElement);
|
|
47908
|
+
// No scroll container found inside the custom element: let the wheel
|
|
47909
|
+
// event pass through so the viewport can continue handling it.
|
|
47910
|
+
if (!scrollContainer) {
|
|
47911
|
+
return false;
|
|
47912
|
+
}
|
|
47913
|
+
return this.canElementConsumeWheel(scrollContainer, ev.deltaX, ev.deltaY);
|
|
47914
|
+
}
|
|
47915
|
+
static getScrollableContainerFromEventPath(path, customElement) {
|
|
47916
|
+
const potentialRoots = [];
|
|
47917
|
+
if (customElement.element instanceof HTMLElement) {
|
|
47918
|
+
potentialRoots.push(customElement.element);
|
|
47919
|
+
}
|
|
47920
|
+
if (customElement.elementRef instanceof HTMLElement) {
|
|
47921
|
+
potentialRoots.push(customElement.elementRef);
|
|
47922
|
+
}
|
|
47923
|
+
const legacyElementRef = customElement.elmeentRef;
|
|
47924
|
+
if (legacyElementRef instanceof HTMLElement) {
|
|
47925
|
+
potentialRoots.push(legacyElementRef);
|
|
47926
|
+
}
|
|
47927
|
+
for (const root of potentialRoots) {
|
|
47928
|
+
const rootIndex = path.findIndex(entry => entry === root);
|
|
47929
|
+
if (rootIndex === -1) {
|
|
47930
|
+
continue;
|
|
47931
|
+
}
|
|
47932
|
+
for (const entry of path.slice(0, rootIndex + 1)) {
|
|
47933
|
+
if (!(entry instanceof HTMLElement)) {
|
|
47934
|
+
continue;
|
|
47935
|
+
}
|
|
47936
|
+
if (this.isScrollableElement(entry)) {
|
|
47937
|
+
return entry;
|
|
47938
|
+
}
|
|
47939
|
+
}
|
|
47940
|
+
}
|
|
47941
|
+
// Fallback for synthetic/custom wheel events where composedPath is absent
|
|
47942
|
+
// or does not include the custom-element subtree.
|
|
47943
|
+
for (const root of potentialRoots) {
|
|
47944
|
+
if (this.isScrollableElement(root)) {
|
|
47945
|
+
return root;
|
|
47946
|
+
}
|
|
47947
|
+
const firstChild = root.firstElementChild;
|
|
47948
|
+
if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
|
|
47949
|
+
return firstChild;
|
|
47950
|
+
}
|
|
47951
|
+
}
|
|
47952
|
+
return null;
|
|
47953
|
+
}
|
|
47954
|
+
static isScrollableElement(element) {
|
|
47955
|
+
const canScrollVertically = element.scrollHeight - element.clientHeight > 0;
|
|
47956
|
+
const canScrollHorizontally = element.scrollWidth - element.clientWidth > 0;
|
|
47957
|
+
if (!canScrollVertically && !canScrollHorizontally) {
|
|
47958
|
+
return false;
|
|
47959
|
+
}
|
|
47960
|
+
const computedStyle = window.getComputedStyle(element);
|
|
47961
|
+
const overflowX = computedStyle.overflowX;
|
|
47962
|
+
const overflowY = computedStyle.overflowY;
|
|
47963
|
+
const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
|
|
47964
|
+
const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
|
|
47965
|
+
return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
|
|
47966
|
+
}
|
|
47967
|
+
static canElementConsumeWheel(element, deltaX, deltaY) {
|
|
47968
|
+
const canConsumeVertical = this.canElementConsumeVerticalWheel(element, deltaY);
|
|
47969
|
+
const canConsumeHorizontal = this.canElementConsumeHorizontalWheel(element, deltaX);
|
|
47970
|
+
// Keep wheel events inside the custom element whenever at least one
|
|
47971
|
+
// requested axis can still be consumed by its scroll container.
|
|
47972
|
+
return canConsumeVertical || canConsumeHorizontal;
|
|
47973
|
+
}
|
|
47974
|
+
static canElementConsumeVerticalWheel(element, deltaY) {
|
|
47975
|
+
if (deltaY === 0) {
|
|
47976
|
+
return false;
|
|
47977
|
+
}
|
|
47978
|
+
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
47979
|
+
if (maxScrollTop === 0) {
|
|
47980
|
+
return false;
|
|
47981
|
+
}
|
|
47982
|
+
const epsilon = 1;
|
|
47983
|
+
const scrollingDown = deltaY > 0;
|
|
47984
|
+
const scrollingUp = deltaY < 0;
|
|
47985
|
+
if (scrollingDown) {
|
|
47986
|
+
return element.scrollTop < maxScrollTop - epsilon;
|
|
47987
|
+
}
|
|
47988
|
+
if (scrollingUp) {
|
|
47989
|
+
return element.scrollTop > epsilon;
|
|
47990
|
+
}
|
|
47991
|
+
return false;
|
|
47992
|
+
}
|
|
47993
|
+
static canElementConsumeHorizontalWheel(element, deltaX) {
|
|
47994
|
+
if (deltaX === 0) {
|
|
47995
|
+
return false;
|
|
47996
|
+
}
|
|
47997
|
+
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
|
|
47998
|
+
if (maxScrollLeft === 0) {
|
|
47999
|
+
return false;
|
|
48000
|
+
}
|
|
48001
|
+
const epsilon = 1;
|
|
48002
|
+
const scrollingRight = deltaX > 0;
|
|
48003
|
+
const scrollingLeft = deltaX < 0;
|
|
48004
|
+
if (scrollingRight) {
|
|
48005
|
+
return element.scrollLeft < maxScrollLeft - epsilon;
|
|
48006
|
+
}
|
|
48007
|
+
if (scrollingLeft) {
|
|
48008
|
+
return element.scrollLeft > epsilon;
|
|
48009
|
+
}
|
|
48010
|
+
return false;
|
|
48011
|
+
}
|
|
48012
|
+
}
|
|
48013
|
+
|
|
48014
|
+
/**
|
|
48015
|
+
* Helper class for exporting Kritzel objects to SVG format.
|
|
48016
|
+
* Generates clean, standards-compliant SVG markup from canvas objects.
|
|
48017
|
+
*/
|
|
48018
|
+
class KritzelSvgExportHelper {
|
|
48019
|
+
/**
|
|
48020
|
+
* Calculates the combined bounding box that encompasses all objects.
|
|
48021
|
+
* Uses rotatedBoundingBox to account for object rotation.
|
|
48022
|
+
* @param objects - Array of objects to calculate bounds for
|
|
48023
|
+
* @returns Combined bounding box, or null if no objects provided
|
|
48024
|
+
*/
|
|
48025
|
+
static calculateCombinedBounds(objects) {
|
|
48026
|
+
if (!objects || objects.length === 0) {
|
|
48027
|
+
return null;
|
|
48028
|
+
}
|
|
48029
|
+
let minX = Infinity;
|
|
48030
|
+
let minY = Infinity;
|
|
48031
|
+
let maxX = -Infinity;
|
|
48032
|
+
let maxY = -Infinity;
|
|
48033
|
+
for (const obj of objects) {
|
|
48034
|
+
const bbox = obj.rotatedBoundingBox;
|
|
48035
|
+
minX = Math.min(minX, bbox.x);
|
|
48036
|
+
minY = Math.min(minY, bbox.y);
|
|
48037
|
+
maxX = Math.max(maxX, bbox.x + bbox.width);
|
|
48038
|
+
maxY = Math.max(maxY, bbox.y + bbox.height);
|
|
48039
|
+
}
|
|
48040
|
+
return {
|
|
48041
|
+
x: minX,
|
|
48042
|
+
y: minY,
|
|
48043
|
+
z: 1,
|
|
48044
|
+
width: maxX - minX,
|
|
48045
|
+
height: maxY - minY,
|
|
48046
|
+
};
|
|
48047
|
+
}
|
|
48048
|
+
/**
|
|
48049
|
+
* Generates a complete SVG document string from the given objects.
|
|
48050
|
+
* @param objects - Objects to export
|
|
48051
|
+
* @param options - Export options
|
|
48052
|
+
* @returns SVG document string
|
|
48053
|
+
*/
|
|
48054
|
+
static generateSvg(objects, options = {}) {
|
|
48055
|
+
const { theme, padding = 0, includeXmlDeclaration = true, rasterSafeForCanvas = false, imageDataUrls } = options;
|
|
48056
|
+
if (!objects || objects.length === 0) {
|
|
48057
|
+
return '';
|
|
48058
|
+
}
|
|
48059
|
+
// Flatten groups to get all objects including nested children
|
|
48060
|
+
const allObjects = this.flattenObjects(objects);
|
|
48061
|
+
const bounds = this.calculateCombinedBounds(allObjects);
|
|
48062
|
+
if (!bounds) {
|
|
48063
|
+
return '';
|
|
48064
|
+
}
|
|
48065
|
+
// Add padding to bounds
|
|
48066
|
+
const viewBoxX = bounds.x - padding;
|
|
48067
|
+
const viewBoxY = bounds.y - padding;
|
|
48068
|
+
const viewBoxWidth = bounds.width + padding * 2;
|
|
48069
|
+
const viewBoxHeight = bounds.height + padding * 2;
|
|
48070
|
+
// Collect all defs (markers, patterns, etc.)
|
|
48071
|
+
const defs = this.collectDefs(allObjects, theme);
|
|
48072
|
+
// Generate SVG elements for each object
|
|
48073
|
+
const elements = objects
|
|
48074
|
+
.sort((a, b) => a.zIndex - b.zIndex)
|
|
48075
|
+
.map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
|
|
48076
|
+
.filter(Boolean)
|
|
48077
|
+
.join('\n ');
|
|
48078
|
+
const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
|
|
48079
|
+
const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
|
|
48080
|
+
return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
|
|
48081
|
+
${elements}
|
|
48082
|
+
</svg>`;
|
|
48083
|
+
}
|
|
48084
|
+
/**
|
|
48085
|
+
* Flattens a list of objects, expanding groups into their children recursively.
|
|
48086
|
+
* @param objects - Objects to flatten
|
|
48087
|
+
* @returns Flat array of all objects including group children
|
|
48088
|
+
*/
|
|
48089
|
+
static flattenObjects(objects) {
|
|
48090
|
+
const result = [];
|
|
48091
|
+
for (const obj of objects) {
|
|
48092
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
|
|
48093
|
+
result.push(...this.flattenObjects(obj.children));
|
|
48094
|
+
}
|
|
48095
|
+
else {
|
|
48096
|
+
result.push(obj);
|
|
48097
|
+
}
|
|
48098
|
+
}
|
|
48099
|
+
return result;
|
|
48100
|
+
}
|
|
48101
|
+
/**
|
|
48102
|
+
* Resolves every image referenced by the given objects (including those
|
|
48103
|
+
* nested inside groups) to an inlined `data:` URL, keyed by object id.
|
|
48104
|
+
*
|
|
48105
|
+
* This is required for export flows that rasterize the SVG (e.g. PNG
|
|
48106
|
+
* export): an SVG loaded into an `<img>` element will NOT load external
|
|
48107
|
+
* or `blob:` resources, so a `<image href="blob:...">` renders as a
|
|
48108
|
+
* broken-image placeholder. Embedding the bytes as a base64 `data:` URL
|
|
48109
|
+
* makes the image self-contained and survive rasterization.
|
|
48110
|
+
*
|
|
48111
|
+
* Resolution is best-effort: images that fail to resolve are simply
|
|
48112
|
+
* omitted from the map, and the SVG falls back to whatever `href` the
|
|
48113
|
+
* synchronous path would have produced.
|
|
48114
|
+
* @param objects - Objects to inline images for
|
|
48115
|
+
* @param assetResolver - Resolver used to fetch asset bytes; optional
|
|
48116
|
+
* @returns Map of image object id -> inlined `data:` URL
|
|
48117
|
+
*/
|
|
48118
|
+
static async resolveImageDataUrls(objects, assetResolver) {
|
|
48119
|
+
const dataUrls = new Map();
|
|
48120
|
+
if (!objects || objects.length === 0) {
|
|
48121
|
+
return dataUrls;
|
|
48122
|
+
}
|
|
48123
|
+
const images = this.flattenObjects(objects).filter((obj) => KritzelClassHelper.isInstanceOf(obj, 'KritzelImage'));
|
|
48124
|
+
await Promise.all(images.map(async (image) => {
|
|
48125
|
+
// Legacy inline data URL is already embeddable as-is.
|
|
48126
|
+
if (image.src?.startsWith('data:')) {
|
|
48127
|
+
dataUrls.set(image.id, image.src);
|
|
48128
|
+
return;
|
|
48129
|
+
}
|
|
48130
|
+
// A resolvedSrc that is itself a data URL can be embedded directly.
|
|
48131
|
+
if (image.resolvedSrc?.startsWith('data:')) {
|
|
48132
|
+
dataUrls.set(image.id, image.resolvedSrc);
|
|
48133
|
+
return;
|
|
48134
|
+
}
|
|
48135
|
+
if (!image.assetId || !assetResolver) {
|
|
48136
|
+
return;
|
|
48137
|
+
}
|
|
48138
|
+
try {
|
|
48139
|
+
const blob = await assetResolver.fetchBlob(image.assetId);
|
|
48140
|
+
dataUrls.set(image.id, await this.blobToDataUrl(blob));
|
|
48141
|
+
}
|
|
48142
|
+
catch (err) {
|
|
48143
|
+
console.warn(`[KritzelSvgExportHelper] Failed to inline image asset ${image.assetId} for export:`, err);
|
|
48144
|
+
}
|
|
48145
|
+
}));
|
|
48146
|
+
return dataUrls;
|
|
48147
|
+
}
|
|
48148
|
+
/**
|
|
48149
|
+
* Reads a Blob into a base64 `data:` URL.
|
|
48150
|
+
* @param blob - The blob to encode
|
|
48151
|
+
* @returns Promise resolving to the data URL string
|
|
48152
|
+
*/
|
|
48153
|
+
static async blobToDataUrl(blob) {
|
|
48154
|
+
// Prefer FileReader in browser-like runtimes for efficiency.
|
|
48155
|
+
const hasFileReader = typeof FileReader !== 'undefined';
|
|
48156
|
+
if (hasFileReader) {
|
|
48157
|
+
try {
|
|
48158
|
+
return await new Promise((resolve, reject) => {
|
|
48159
|
+
const reader = new FileReader();
|
|
48160
|
+
reader.onload = () => {
|
|
48161
|
+
if (typeof reader.result === 'string') {
|
|
48162
|
+
resolve(reader.result);
|
|
48163
|
+
return;
|
|
48164
|
+
}
|
|
48165
|
+
reject(new Error('FileReader result is not a string'));
|
|
48166
|
+
};
|
|
48167
|
+
reader.onerror = () => reject(reader.error ?? new Error('Failed to read blob as data URL'));
|
|
48168
|
+
reader.readAsDataURL(blob);
|
|
48169
|
+
});
|
|
48170
|
+
}
|
|
48171
|
+
catch {
|
|
48172
|
+
// Fall through to the ArrayBuffer-based encoder.
|
|
48173
|
+
}
|
|
48174
|
+
}
|
|
48175
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
48176
|
+
const mimeType = blob.type || 'application/octet-stream';
|
|
48177
|
+
return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
|
|
48178
|
+
}
|
|
48179
|
+
/**
|
|
48180
|
+
* Encodes a byte array to base64 across browser and Node-like runtimes.
|
|
48181
|
+
*/
|
|
48182
|
+
static uint8ToBase64(bytes) {
|
|
48183
|
+
if (typeof Buffer !== 'undefined') {
|
|
48184
|
+
return Buffer.from(bytes).toString('base64');
|
|
48185
|
+
}
|
|
48186
|
+
let binary = '';
|
|
48187
|
+
const chunkSize = 0x8000;
|
|
48188
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
48189
|
+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
48190
|
+
binary += String.fromCharCode(...chunk);
|
|
48191
|
+
}
|
|
48192
|
+
if (typeof btoa === 'function') {
|
|
48193
|
+
return btoa(binary);
|
|
48194
|
+
}
|
|
48195
|
+
throw new Error('No base64 encoder available in this runtime');
|
|
48196
|
+
}
|
|
48197
|
+
/**
|
|
48198
|
+
* Collects SVG defs (markers, patterns, etc.) needed by the objects.
|
|
48199
|
+
* @param objects - Objects to collect defs for
|
|
48200
|
+
* @param theme - Theme for color resolution
|
|
48201
|
+
* @returns Array of def element strings
|
|
48202
|
+
*/
|
|
48203
|
+
static collectDefs(objects, theme) {
|
|
48204
|
+
const defs = [];
|
|
48205
|
+
for (const obj of objects) {
|
|
48206
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
|
|
48207
|
+
if (obj.hasStartArrow) {
|
|
48208
|
+
defs.push(this.generateArrowMarker(obj, 'start', theme));
|
|
48209
|
+
}
|
|
48210
|
+
if (obj.hasEndArrow) {
|
|
48211
|
+
defs.push(this.generateArrowMarker(obj, 'end', theme));
|
|
48212
|
+
}
|
|
48213
|
+
}
|
|
48214
|
+
}
|
|
48215
|
+
return defs;
|
|
48216
|
+
}
|
|
48217
|
+
/**
|
|
48218
|
+
* Generates an SVG marker element for line arrows.
|
|
48219
|
+
* @param line - The line object
|
|
48220
|
+
* @param end - Which end of the line ('start' or 'end')
|
|
48221
|
+
* @param theme - Theme for color resolution
|
|
48222
|
+
* @returns SVG marker element string
|
|
48223
|
+
*/
|
|
48224
|
+
static generateArrowMarker(line, end, _theme) {
|
|
48225
|
+
const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
|
|
48226
|
+
const size = line.getArrowSize(end);
|
|
48227
|
+
const fill = line.getArrowFill(end);
|
|
48228
|
+
const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
|
|
48229
|
+
const path = line.getArrowPath(style);
|
|
48230
|
+
const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
|
|
48231
|
+
const refX = 0;
|
|
48232
|
+
const refY = size / 2;
|
|
48233
|
+
return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
|
|
48234
|
+
<path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
|
|
48235
|
+
</marker>`;
|
|
48236
|
+
}
|
|
48237
|
+
/**
|
|
48238
|
+
* Converts a Kritzel object to its SVG element representation.
|
|
48239
|
+
* @param object - The object to convert
|
|
48240
|
+
* @param theme - Theme for color resolution
|
|
48241
|
+
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
48242
|
+
* @returns SVG element string, or empty string if object type is not supported
|
|
48243
|
+
*/
|
|
48244
|
+
static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
48245
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
|
|
48246
|
+
return this.pathToSvg(object, theme);
|
|
48247
|
+
}
|
|
48248
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
|
|
48249
|
+
return this.lineToSvg(object, theme);
|
|
48250
|
+
}
|
|
48251
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
|
|
48252
|
+
return this.shapeToSvg(object, theme);
|
|
48253
|
+
}
|
|
48254
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
|
|
48255
|
+
return this.textToSvg(object, theme, rasterSafeForCanvas);
|
|
48256
|
+
}
|
|
48257
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
|
|
48258
|
+
return this.imageToSvg(object, imageDataUrls);
|
|
48259
|
+
}
|
|
48260
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
|
|
48261
|
+
return this.customElementToSvg(object, rasterSafeForCanvas);
|
|
48262
|
+
}
|
|
48263
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
48264
|
+
return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
|
|
48265
|
+
}
|
|
48266
|
+
return '';
|
|
48267
|
+
}
|
|
48268
|
+
/**
|
|
48269
|
+
* Converts a KritzelPath to SVG.
|
|
48270
|
+
* @param path - The path object
|
|
48271
|
+
* @param theme - Theme for color resolution
|
|
48272
|
+
* @returns SVG element string
|
|
48273
|
+
*/
|
|
48274
|
+
static pathToSvg(path, theme) {
|
|
48275
|
+
const transform = this.buildTransform(path);
|
|
48276
|
+
const fill = KritzelColorHelper.resolveThemeColor(path.fill, theme);
|
|
48277
|
+
const stroke = KritzelColorHelper.resolveThemeColor(path.stroke, theme);
|
|
48278
|
+
const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
|
|
48279
|
+
const scale = this.getObjectScale(path);
|
|
48280
|
+
// Path needs to be wrapped in a g with translation since path.d uses local coordinates
|
|
48281
|
+
return `<g transform="${transform}"${opacity}>
|
|
48282
|
+
<svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
|
|
48283
|
+
<path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
|
|
48284
|
+
</svg>
|
|
48285
|
+
</g>`;
|
|
48286
|
+
}
|
|
48287
|
+
/**
|
|
48288
|
+
* Converts a KritzelLine to SVG.
|
|
48289
|
+
* @param line - The line object
|
|
48290
|
+
* @param theme - Theme for color resolution
|
|
48291
|
+
* @returns SVG element string
|
|
48292
|
+
*/
|
|
48293
|
+
static lineToSvg(line, theme) {
|
|
48294
|
+
const transform = this.buildTransform(line);
|
|
48295
|
+
const stroke = KritzelColorHelper.resolveThemeColor(line.stroke, theme);
|
|
48296
|
+
const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
|
|
48297
|
+
const scale = this.getObjectScale(line);
|
|
48298
|
+
const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
|
|
48299
|
+
const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
|
|
48300
|
+
return `<g transform="${transform}"${opacity}>
|
|
48301
|
+
<svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
|
|
48302
|
+
<path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
|
|
48303
|
+
</svg>
|
|
48304
|
+
</g>`;
|
|
48305
|
+
}
|
|
48306
|
+
/**
|
|
48307
|
+
* Converts a KritzelShape to SVG.
|
|
48308
|
+
* @param shape - The shape object
|
|
48309
|
+
* @param theme - Theme for color resolution
|
|
48310
|
+
* @returns SVG element string
|
|
48311
|
+
*/
|
|
48312
|
+
static shapeToSvg(shape, theme) {
|
|
48313
|
+
const transform = this.buildTransform(shape);
|
|
48314
|
+
const fill = KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
|
|
48315
|
+
const stroke = KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
|
|
48316
|
+
const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
|
|
48317
|
+
const scale = this.getObjectScale(shape);
|
|
48318
|
+
// Get the SVG path for the shape
|
|
48319
|
+
const pathD = shape.getSvgPath();
|
|
48320
|
+
// Handle text content if present
|
|
48321
|
+
let textContent = '';
|
|
48322
|
+
if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
|
|
48323
|
+
else if (shape.content) {
|
|
48324
|
+
const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
|
|
48325
|
+
if (textHtml) {
|
|
48326
|
+
const fontColor = KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
|
|
48327
|
+
textContent = `
|
|
48328
|
+
<foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
|
|
48329
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-family: ${shape.fontFamily}; font-size: ${shape.fontSize}px; color: ${fontColor}; overflow: hidden;">
|
|
48330
|
+
${textHtml}
|
|
48331
|
+
</div>
|
|
48332
|
+
</foreignObject>`;
|
|
48333
|
+
}
|
|
48334
|
+
}
|
|
48335
|
+
return `<g transform="${transform}"${opacity}>
|
|
48336
|
+
<svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
|
|
48337
|
+
<path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
|
|
48338
|
+
</svg>
|
|
48339
|
+
</g>`;
|
|
48340
|
+
}
|
|
48341
|
+
/**
|
|
48342
|
+
* Converts a KritzelText to SVG using foreignObject for rich text support.
|
|
48343
|
+
* @param text - The text object
|
|
48344
|
+
* @param theme - Theme for color resolution
|
|
48345
|
+
* @returns SVG element string
|
|
48346
|
+
*/
|
|
48347
|
+
static textToSvg(text, theme, rasterSafeForCanvas = false) {
|
|
48348
|
+
if (rasterSafeForCanvas) {
|
|
48349
|
+
return this.textToSvgRasterSafe(text, theme);
|
|
48493
48350
|
}
|
|
48351
|
+
const transform = this.buildTransform(text);
|
|
48352
|
+
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
48353
|
+
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
48354
|
+
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
48355
|
+
const scale = this.getObjectScale(text);
|
|
48356
|
+
// Convert ProseMirror content to HTML
|
|
48357
|
+
const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
|
|
48358
|
+
// Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
|
|
48359
|
+
const width = text.totalWidth / scale;
|
|
48360
|
+
const height = text.totalHeight / scale;
|
|
48361
|
+
// Convert the font size from pt to SVG world units (px equivalent).
|
|
48362
|
+
// text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
|
|
48363
|
+
// We also bake in scaleFactor (user resize) and divide by the object scale so
|
|
48364
|
+
// that the rendered font aligns with the world-unit dimensions above.
|
|
48365
|
+
// The CSS transform: scale(scaleFactor) is removed from the div because the
|
|
48366
|
+
// adjusted font-size already accounts for it – keeping both would double-scale
|
|
48367
|
+
// the content and overflow the foreignObject.
|
|
48368
|
+
const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
|
|
48369
|
+
return `<g transform="${transform}"${opacity}>
|
|
48370
|
+
<foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
|
|
48371
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: ${text.fontFamily}; font-size: ${svgFontSizePx}px; color: ${fontColor}; background-color: ${bgColor || 'transparent'}; white-space: pre-wrap; word-wrap: break-word; width: 100%; margin: 0; padding: 0; box-sizing: border-box;">${htmlContent.trim()}</div>
|
|
48372
|
+
</foreignObject>
|
|
48373
|
+
</g>`;
|
|
48494
48374
|
}
|
|
48495
|
-
|
|
48496
|
-
|
|
48497
|
-
|
|
48498
|
-
|
|
48499
|
-
|
|
48500
|
-
|
|
48501
|
-
|
|
48502
|
-
|
|
48503
|
-
|
|
48504
|
-
|
|
48505
|
-
|
|
48506
|
-
|
|
48507
|
-
|
|
48508
|
-
|
|
48509
|
-
|
|
48510
|
-
|
|
48511
|
-
|
|
48512
|
-
|
|
48513
|
-
|
|
48514
|
-
|
|
48515
|
-
|
|
48516
|
-
|
|
48517
|
-
|
|
48518
|
-
|
|
48375
|
+
/**
|
|
48376
|
+
* Converts a KritzelText to SVG using pure SVG text primitives.
|
|
48377
|
+
* This path avoids `<foreignObject>`, which taints canvases when the
|
|
48378
|
+
* generated SVG is rasterized through `<img>` and `drawImage`.
|
|
48379
|
+
*/
|
|
48380
|
+
static textToSvgRasterSafe(text, theme) {
|
|
48381
|
+
const transform = this.buildTransform(text);
|
|
48382
|
+
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
48383
|
+
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
48384
|
+
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
48385
|
+
const scale = this.getObjectScale(text);
|
|
48386
|
+
const width = text.totalWidth / scale;
|
|
48387
|
+
const height = text.totalHeight / scale;
|
|
48388
|
+
const lines = this.prosemirrorToPlainTextLines(text.content);
|
|
48389
|
+
const resolvedLines = lines.length > 0 ? lines : [''];
|
|
48390
|
+
// Convert the font size from pt to SVG world units (px equivalent).
|
|
48391
|
+
// text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
|
|
48392
|
+
// We also bake in scaleFactor (user resize) and divide by the object scale so
|
|
48393
|
+
// that the rendered lines align with the world-unit bounding box dimensions above.
|
|
48394
|
+
const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
|
|
48395
|
+
const lineHeight = resolvedFontSizePx * 1.2;
|
|
48396
|
+
const startY = resolvedFontSizePx;
|
|
48397
|
+
const bgRect = bgColor && bgColor !== 'transparent'
|
|
48398
|
+
? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
|
|
48399
|
+
: '';
|
|
48400
|
+
const tspans = resolvedLines
|
|
48401
|
+
.map((line, index) => {
|
|
48402
|
+
const y = startY + index * lineHeight;
|
|
48403
|
+
return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
|
|
48404
|
+
})
|
|
48405
|
+
.join('');
|
|
48406
|
+
return `<g transform="${transform}"${opacity}>
|
|
48407
|
+
${bgRect}
|
|
48408
|
+
<text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
|
|
48409
|
+
</g>`;
|
|
48519
48410
|
}
|
|
48520
|
-
|
|
48521
|
-
|
|
48522
|
-
|
|
48523
|
-
|
|
48524
|
-
|
|
48411
|
+
/**
|
|
48412
|
+
* Converts a KritzelImage to SVG.
|
|
48413
|
+
*
|
|
48414
|
+
* Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
|
|
48415
|
+
* id) when provided, so the embedded bytes survive rasterization to
|
|
48416
|
+
* PNG. Falls back to `resolvedSrc` (populated by the renderer via the
|
|
48417
|
+
* asset resolver) and finally the legacy inline `src` field for
|
|
48418
|
+
* documents persisted before the asset layer existed.
|
|
48419
|
+
*
|
|
48420
|
+
* Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
|
|
48421
|
+
* a broken-image placeholder once the SVG is loaded into an `<img>` and
|
|
48422
|
+
* drawn to a canvas. Callers that rasterize the SVG must pass
|
|
48423
|
+
* `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
|
|
48424
|
+
* @param image - The image object
|
|
48425
|
+
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
48426
|
+
* @returns SVG element string
|
|
48427
|
+
*/
|
|
48428
|
+
static imageToSvg(image, imageDataUrls) {
|
|
48429
|
+
const transform = this.buildTransform(image);
|
|
48430
|
+
const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
|
|
48431
|
+
const scale = this.getObjectScale(image);
|
|
48432
|
+
// Calculate dimensions
|
|
48433
|
+
const width = image.totalWidth / scale;
|
|
48434
|
+
const height = image.totalHeight / scale;
|
|
48435
|
+
const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
|
|
48436
|
+
return `<g transform="${transform}"${opacity}>
|
|
48437
|
+
<image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
|
|
48438
|
+
</g>`;
|
|
48525
48439
|
}
|
|
48526
|
-
|
|
48527
|
-
|
|
48528
|
-
|
|
48529
|
-
|
|
48530
|
-
|
|
48531
|
-
|
|
48532
|
-
|
|
48533
|
-
|
|
48534
|
-
|
|
48535
|
-
|
|
48536
|
-
|
|
48537
|
-
|
|
48538
|
-
|
|
48539
|
-
|
|
48540
|
-
|
|
48541
|
-
|
|
48542
|
-
|
|
48543
|
-
|
|
48544
|
-
|
|
48545
|
-
|
|
48440
|
+
/**
|
|
48441
|
+
* Converts a KritzelCustomElement to SVG.
|
|
48442
|
+
* SVG mode: embeds HTML in foreignObject (best-effort).
|
|
48443
|
+
* PNG/raster mode: renders placeholder rect with warning.
|
|
48444
|
+
* @param obj - The custom element object
|
|
48445
|
+
* @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
|
|
48446
|
+
* @returns SVG element string
|
|
48447
|
+
*/
|
|
48448
|
+
static customElementToSvg(obj, rasterSafeForCanvas = false) {
|
|
48449
|
+
const transform = this.buildTransform(obj);
|
|
48450
|
+
const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
|
|
48451
|
+
const scale = this.getObjectScale(obj);
|
|
48452
|
+
const width = obj.totalWidth / scale;
|
|
48453
|
+
const height = obj.totalHeight / scale;
|
|
48454
|
+
if (rasterSafeForCanvas) {
|
|
48455
|
+
// PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
|
|
48456
|
+
console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
|
|
48457
|
+
return `<g transform="${transform}"${opacity}>
|
|
48458
|
+
<rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
|
|
48459
|
+
<text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
|
|
48460
|
+
</g>`;
|
|
48546
48461
|
}
|
|
48547
|
-
|
|
48462
|
+
// SVG export: use foreignObject to embed HTML
|
|
48463
|
+
const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
|
|
48464
|
+
return `<g transform="${transform}"${opacity}>
|
|
48465
|
+
<foreignObject x="0" y="0" width="${width}" height="${height}">
|
|
48466
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
|
|
48467
|
+
${html}
|
|
48468
|
+
</div>
|
|
48469
|
+
</foreignObject>
|
|
48470
|
+
</g>`;
|
|
48548
48471
|
}
|
|
48549
|
-
|
|
48550
|
-
|
|
48551
|
-
|
|
48552
|
-
|
|
48553
|
-
|
|
48554
|
-
|
|
48555
|
-
|
|
48556
|
-
|
|
48557
|
-
const
|
|
48558
|
-
|
|
48559
|
-
|
|
48560
|
-
|
|
48561
|
-
|
|
48562
|
-
|
|
48563
|
-
|
|
48564
|
-
continue;
|
|
48565
|
-
}
|
|
48566
|
-
for (const entry of path.slice(0, rootIndex + 1)) {
|
|
48567
|
-
if (!(entry instanceof HTMLElement)) {
|
|
48568
|
-
continue;
|
|
48569
|
-
}
|
|
48570
|
-
if (this.isScrollableElement(entry)) {
|
|
48571
|
-
return entry;
|
|
48572
|
-
}
|
|
48573
|
-
}
|
|
48574
|
-
}
|
|
48575
|
-
// Fallback for synthetic/custom wheel events where composedPath is absent
|
|
48576
|
-
// or does not include the custom-element subtree.
|
|
48577
|
-
for (const root of potentialRoots) {
|
|
48578
|
-
if (this.isScrollableElement(root)) {
|
|
48579
|
-
return root;
|
|
48580
|
-
}
|
|
48581
|
-
const firstChild = root.firstElementChild;
|
|
48582
|
-
if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
|
|
48583
|
-
return firstChild;
|
|
48584
|
-
}
|
|
48472
|
+
/**
|
|
48473
|
+
* Converts a KritzelGroup to SVG by recursively converting children.
|
|
48474
|
+
* @param group - The group object
|
|
48475
|
+
* @param theme - Theme for color resolution
|
|
48476
|
+
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
48477
|
+
* @returns SVG element string
|
|
48478
|
+
*/
|
|
48479
|
+
static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
48480
|
+
const children = group.children
|
|
48481
|
+
.sort((a, b) => a.zIndex - b.zIndex)
|
|
48482
|
+
.map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
|
|
48483
|
+
.filter(Boolean)
|
|
48484
|
+
.join('\n ');
|
|
48485
|
+
if (!children) {
|
|
48486
|
+
return '';
|
|
48585
48487
|
}
|
|
48586
|
-
|
|
48488
|
+
const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
|
|
48489
|
+
return `<g${opacity}>
|
|
48490
|
+
${children}
|
|
48491
|
+
</g>`;
|
|
48587
48492
|
}
|
|
48588
|
-
|
|
48589
|
-
|
|
48590
|
-
|
|
48591
|
-
|
|
48592
|
-
|
|
48493
|
+
/**
|
|
48494
|
+
* Builds a transform attribute string for an object.
|
|
48495
|
+
* Combines translation and rotation.
|
|
48496
|
+
* @param object - The object to build transform for
|
|
48497
|
+
* @returns Transform attribute value string
|
|
48498
|
+
*/
|
|
48499
|
+
static buildTransform(object) {
|
|
48500
|
+
const transforms = [];
|
|
48501
|
+
const scale = this.getObjectScale(object);
|
|
48502
|
+
// Translation
|
|
48503
|
+
transforms.push(`translate(${object.translateX}, ${object.translateY})`);
|
|
48504
|
+
// Rotation around center
|
|
48505
|
+
if (object.rotation !== 0) {
|
|
48506
|
+
const centerX = object.totalWidth / 2 / scale;
|
|
48507
|
+
const centerY = object.totalHeight / 2 / scale;
|
|
48508
|
+
const degrees = object.rotation * (180 / Math.PI);
|
|
48509
|
+
transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
|
|
48593
48510
|
}
|
|
48594
|
-
|
|
48595
|
-
const overflowX = computedStyle.overflowX;
|
|
48596
|
-
const overflowY = computedStyle.overflowY;
|
|
48597
|
-
const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
|
|
48598
|
-
const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
|
|
48599
|
-
return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
|
|
48511
|
+
return transforms.join(' ');
|
|
48600
48512
|
}
|
|
48601
|
-
|
|
48602
|
-
|
|
48603
|
-
|
|
48604
|
-
|
|
48605
|
-
|
|
48606
|
-
return
|
|
48513
|
+
/**
|
|
48514
|
+
* Returns a safe non-zero scale for export geometry calculations.
|
|
48515
|
+
*/
|
|
48516
|
+
static getObjectScale(object) {
|
|
48517
|
+
const scale = object.scale ?? 1;
|
|
48518
|
+
return scale === 0 ? 1 : scale;
|
|
48607
48519
|
}
|
|
48608
|
-
|
|
48609
|
-
|
|
48610
|
-
|
|
48520
|
+
/**
|
|
48521
|
+
* Converts ProseMirror JSON content to HTML string.
|
|
48522
|
+
* @param content - ProseMirror document JSON
|
|
48523
|
+
* @param context - The text or shape object for styling context
|
|
48524
|
+
* @param theme - Theme for color resolution
|
|
48525
|
+
* @returns HTML string
|
|
48526
|
+
*/
|
|
48527
|
+
static prosemirrorToHtml(content, _context, theme) {
|
|
48528
|
+
if (!content || !content.content) {
|
|
48529
|
+
return '';
|
|
48611
48530
|
}
|
|
48612
|
-
|
|
48613
|
-
|
|
48614
|
-
|
|
48531
|
+
return content.content
|
|
48532
|
+
.map((node) => this.nodeToHtml(node, theme))
|
|
48533
|
+
.join('');
|
|
48534
|
+
}
|
|
48535
|
+
/**
|
|
48536
|
+
* Converts a ProseMirror node to HTML.
|
|
48537
|
+
* @param node - ProseMirror node JSON
|
|
48538
|
+
* @param theme - Theme for color resolution
|
|
48539
|
+
* @returns HTML string
|
|
48540
|
+
*/
|
|
48541
|
+
static nodeToHtml(node, theme) {
|
|
48542
|
+
if (!node)
|
|
48543
|
+
return '';
|
|
48544
|
+
switch (node.type) {
|
|
48545
|
+
case 'paragraph':
|
|
48546
|
+
const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
48547
|
+
return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
|
|
48548
|
+
case 'text':
|
|
48549
|
+
let text = this.escapeHtml(node.text || '');
|
|
48550
|
+
if (node.marks) {
|
|
48551
|
+
for (const mark of node.marks) {
|
|
48552
|
+
text = this.applyMark(text, mark, theme);
|
|
48553
|
+
}
|
|
48554
|
+
}
|
|
48555
|
+
return text;
|
|
48556
|
+
case 'hard_break':
|
|
48557
|
+
return '<br/>';
|
|
48558
|
+
case 'bullet_list':
|
|
48559
|
+
const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
48560
|
+
return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
|
|
48561
|
+
case 'ordered_list':
|
|
48562
|
+
const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
48563
|
+
return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
|
|
48564
|
+
case 'list_item':
|
|
48565
|
+
const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
48566
|
+
return `<li>${liContent}</li>`;
|
|
48567
|
+
default:
|
|
48568
|
+
// For unknown node types, try to render content
|
|
48569
|
+
if (node.content) {
|
|
48570
|
+
return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
|
|
48571
|
+
}
|
|
48572
|
+
return '';
|
|
48615
48573
|
}
|
|
48616
|
-
|
|
48617
|
-
|
|
48618
|
-
|
|
48619
|
-
|
|
48620
|
-
|
|
48574
|
+
}
|
|
48575
|
+
/**
|
|
48576
|
+
* Converts ProseMirror JSON content to plain-text lines.
|
|
48577
|
+
* Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
|
|
48578
|
+
*/
|
|
48579
|
+
static prosemirrorToPlainTextLines(content) {
|
|
48580
|
+
if (!content || !Array.isArray(content.content)) {
|
|
48581
|
+
return [];
|
|
48621
48582
|
}
|
|
48622
|
-
|
|
48623
|
-
|
|
48583
|
+
const plainText = content.content
|
|
48584
|
+
.map((node) => this.nodeToPlainText(node))
|
|
48585
|
+
.join('')
|
|
48586
|
+
.replace(/\r\n/g, '\n')
|
|
48587
|
+
.replace(/\n+$/, '');
|
|
48588
|
+
if (!plainText) {
|
|
48589
|
+
return [];
|
|
48624
48590
|
}
|
|
48625
|
-
return
|
|
48591
|
+
return plainText.split('\n');
|
|
48626
48592
|
}
|
|
48627
|
-
|
|
48628
|
-
|
|
48629
|
-
|
|
48630
|
-
|
|
48631
|
-
|
|
48632
|
-
|
|
48633
|
-
return false;
|
|
48593
|
+
/**
|
|
48594
|
+
* Converts a ProseMirror node tree into plain text.
|
|
48595
|
+
*/
|
|
48596
|
+
static nodeToPlainText(node) {
|
|
48597
|
+
if (!node) {
|
|
48598
|
+
return '';
|
|
48634
48599
|
}
|
|
48635
|
-
|
|
48636
|
-
|
|
48637
|
-
|
|
48638
|
-
|
|
48639
|
-
|
|
48600
|
+
switch (node.type) {
|
|
48601
|
+
case 'text':
|
|
48602
|
+
return node.text || '';
|
|
48603
|
+
case 'hard_break':
|
|
48604
|
+
return '\n';
|
|
48605
|
+
case 'paragraph': {
|
|
48606
|
+
const paragraph = Array.isArray(node.content)
|
|
48607
|
+
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
48608
|
+
: '';
|
|
48609
|
+
return `${paragraph}\n`;
|
|
48610
|
+
}
|
|
48611
|
+
case 'list_item': {
|
|
48612
|
+
const item = Array.isArray(node.content)
|
|
48613
|
+
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
48614
|
+
: '';
|
|
48615
|
+
return `- ${item.trim()}\n`;
|
|
48616
|
+
}
|
|
48617
|
+
default:
|
|
48618
|
+
return Array.isArray(node.content)
|
|
48619
|
+
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
48620
|
+
: '';
|
|
48640
48621
|
}
|
|
48641
|
-
|
|
48642
|
-
|
|
48622
|
+
}
|
|
48623
|
+
/**
|
|
48624
|
+
* Applies a ProseMirror mark to text.
|
|
48625
|
+
* @param text - The text to wrap
|
|
48626
|
+
* @param mark - The mark to apply
|
|
48627
|
+
* @param theme - Theme for color resolution
|
|
48628
|
+
* @returns Wrapped HTML string
|
|
48629
|
+
*/
|
|
48630
|
+
static applyMark(text, mark, _theme) {
|
|
48631
|
+
switch (mark.type) {
|
|
48632
|
+
case 'bold':
|
|
48633
|
+
case 'strong':
|
|
48634
|
+
return `<strong>${text}</strong>`;
|
|
48635
|
+
case 'italic':
|
|
48636
|
+
case 'em':
|
|
48637
|
+
return `<em>${text}</em>`;
|
|
48638
|
+
case 'underline':
|
|
48639
|
+
return `<u>${text}</u>`;
|
|
48640
|
+
case 'strike':
|
|
48641
|
+
return `<s>${text}</s>`;
|
|
48642
|
+
case 'code':
|
|
48643
|
+
return `<code>${text}</code>`;
|
|
48644
|
+
case 'link':
|
|
48645
|
+
const href = mark.attrs?.href || '#';
|
|
48646
|
+
return `<a href="${this.escapeHtml(href)}">${text}</a>`;
|
|
48647
|
+
default:
|
|
48648
|
+
return text;
|
|
48643
48649
|
}
|
|
48644
|
-
|
|
48650
|
+
}
|
|
48651
|
+
/**
|
|
48652
|
+
* Escapes HTML special characters.
|
|
48653
|
+
* @param str - String to escape
|
|
48654
|
+
* @returns Escaped string
|
|
48655
|
+
*/
|
|
48656
|
+
static escapeHtml(str) {
|
|
48657
|
+
return str
|
|
48658
|
+
.replace(/&/g, '&')
|
|
48659
|
+
.replace(/</g, '<')
|
|
48660
|
+
.replace(/>/g, '>')
|
|
48661
|
+
.replace(/"/g, '"')
|
|
48662
|
+
.replace(/'/g, ''');
|
|
48645
48663
|
}
|
|
48646
48664
|
}
|
|
48647
48665
|
|
|
@@ -49838,10 +49856,7 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
|
|
|
49838
49856
|
console.error('Failed to export viewport as PNG: screenshot could not be generated');
|
|
49839
49857
|
return;
|
|
49840
49858
|
}
|
|
49841
|
-
|
|
49842
|
-
link.download = filename;
|
|
49843
|
-
link.href = dataUrl;
|
|
49844
|
-
link.click();
|
|
49859
|
+
this.triggerDataUrlDownload(dataUrl, filename);
|
|
49845
49860
|
}
|
|
49846
49861
|
catch (error) {
|
|
49847
49862
|
console.error('Failed to export viewport as PNG:', error);
|
|
@@ -49863,129 +49878,57 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
|
|
|
49863
49878
|
console.error('Failed to export viewport as SVG: screenshot could not be generated');
|
|
49864
49879
|
return;
|
|
49865
49880
|
}
|
|
49866
|
-
|
|
49867
|
-
link.download = filename;
|
|
49868
|
-
link.href = dataUrl;
|
|
49869
|
-
link.click();
|
|
49881
|
+
this.triggerDataUrlDownload(dataUrl, filename);
|
|
49870
49882
|
}
|
|
49871
49883
|
catch (error) {
|
|
49872
49884
|
console.error('Failed to export viewport as SVG:', error);
|
|
49873
49885
|
}
|
|
49874
49886
|
}
|
|
49875
49887
|
/**
|
|
49876
|
-
*
|
|
49877
|
-
*
|
|
49878
|
-
* @param options - Optional export settings (theme, padding, raster-safe mode for canvas export).
|
|
49879
|
-
* @returns SVG markup string, or null if no objects are selected.
|
|
49888
|
+
* Returns export formats supported by the currently selected object.
|
|
49889
|
+
* Only returns formats when exactly one object is selected.
|
|
49880
49890
|
*/
|
|
49881
|
-
async
|
|
49882
|
-
const
|
|
49883
|
-
if (!
|
|
49884
|
-
return
|
|
49891
|
+
async getSelectedObjectSupportedExportFormats() {
|
|
49892
|
+
const selected = this.getSingleSelectedExportableObject();
|
|
49893
|
+
if (!selected) {
|
|
49894
|
+
return [];
|
|
49885
49895
|
}
|
|
49886
|
-
|
|
49887
|
-
// Inline image bytes as data URLs so they survive both standalone SVG
|
|
49888
|
-
// files and rasterization to PNG (a `blob:` href is not loaded when an
|
|
49889
|
-
// SVG is drawn into a canvas, which otherwise yields a broken image).
|
|
49890
|
-
const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls(selectedObjects, this.core.assetResolver);
|
|
49891
|
-
return KritzelSvgExportHelper.generateSvg(selectedObjects, {
|
|
49892
|
-
theme,
|
|
49893
|
-
padding: options?.padding ?? 0,
|
|
49894
|
-
includeXmlDeclaration: true,
|
|
49895
|
-
rasterSafeForCanvas: options?.rasterSafeForCanvas ?? false,
|
|
49896
|
-
imageDataUrls,
|
|
49897
|
-
});
|
|
49896
|
+
return selected.getSupportedExportFormats();
|
|
49898
49897
|
}
|
|
49899
49898
|
/**
|
|
49900
|
-
*
|
|
49901
|
-
*
|
|
49902
|
-
* @param options - Optional export settings (theme, padding).
|
|
49899
|
+
* Checks whether the currently selected object supports a specific export format.
|
|
49900
|
+
* Returns false when none or multiple objects are selected.
|
|
49903
49901
|
*/
|
|
49904
|
-
async
|
|
49905
|
-
|
|
49906
|
-
|
|
49907
|
-
|
|
49908
|
-
console.warn('No objects selected for SVG export');
|
|
49909
|
-
return;
|
|
49910
|
-
}
|
|
49911
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
49912
|
-
const filename = `selection-${timestamp}.svg`;
|
|
49913
|
-
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
49914
|
-
const url = URL.createObjectURL(blob);
|
|
49915
|
-
const link = document.createElement('a');
|
|
49916
|
-
link.download = filename;
|
|
49917
|
-
link.href = url;
|
|
49918
|
-
link.click();
|
|
49919
|
-
URL.revokeObjectURL(url);
|
|
49920
|
-
}
|
|
49921
|
-
catch (error) {
|
|
49922
|
-
console.error('Failed to export selected objects as SVG:', error);
|
|
49902
|
+
async canExportSelectedObjectAs(format) {
|
|
49903
|
+
const selected = this.getSingleSelectedExportableObject();
|
|
49904
|
+
if (!selected) {
|
|
49905
|
+
return false;
|
|
49923
49906
|
}
|
|
49907
|
+
return selected.getSupportedExportFormats().includes(format);
|
|
49924
49908
|
}
|
|
49925
49909
|
/**
|
|
49926
|
-
*
|
|
49927
|
-
*
|
|
49928
|
-
* @param options - Optional export settings (theme, padding, scale for resolution).
|
|
49929
|
-
* @returns PNG data URL string, or null if no objects are selected.
|
|
49910
|
+
* Exports the currently selected object in the requested format and triggers a file download.
|
|
49911
|
+
* Only supports single selection.
|
|
49930
49912
|
*/
|
|
49931
|
-
async
|
|
49932
|
-
const
|
|
49933
|
-
|
|
49934
|
-
|
|
49935
|
-
// Avoid foreignObject-based text serialization, which taints the
|
|
49936
|
-
// canvas in SVG->PNG conversion paths.
|
|
49937
|
-
rasterSafeForCanvas: true,
|
|
49938
|
-
});
|
|
49939
|
-
if (!svgString) {
|
|
49940
|
-
return null;
|
|
49913
|
+
async exportSelectedObject(format) {
|
|
49914
|
+
const selected = this.getSingleSelectedExportableObject();
|
|
49915
|
+
if (!selected) {
|
|
49916
|
+
return;
|
|
49941
49917
|
}
|
|
49942
|
-
const scale = options?.scale ?? 2; // Default 2x for better quality
|
|
49943
|
-
return new Promise((resolve, reject) => {
|
|
49944
|
-
const img = new Image();
|
|
49945
|
-
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
49946
|
-
const url = URL.createObjectURL(svgBlob);
|
|
49947
|
-
img.onload = () => {
|
|
49948
|
-
const canvas = document.createElement('canvas');
|
|
49949
|
-
canvas.width = img.width * scale;
|
|
49950
|
-
canvas.height = img.height * scale;
|
|
49951
|
-
const ctx = canvas.getContext('2d');
|
|
49952
|
-
if (!ctx) {
|
|
49953
|
-
URL.revokeObjectURL(url);
|
|
49954
|
-
reject(new Error('Failed to get canvas context'));
|
|
49955
|
-
return;
|
|
49956
|
-
}
|
|
49957
|
-
ctx.scale(scale, scale);
|
|
49958
|
-
ctx.drawImage(img, 0, 0);
|
|
49959
|
-
URL.revokeObjectURL(url);
|
|
49960
|
-
resolve(canvas.toDataURL('image/png'));
|
|
49961
|
-
};
|
|
49962
|
-
img.onerror = () => {
|
|
49963
|
-
URL.revokeObjectURL(url);
|
|
49964
|
-
reject(new Error('Failed to load SVG for PNG conversion'));
|
|
49965
|
-
};
|
|
49966
|
-
img.src = url;
|
|
49967
|
-
});
|
|
49968
|
-
}
|
|
49969
|
-
/**
|
|
49970
|
-
* Exports the currently selected objects as a PNG file and triggers a browser download.
|
|
49971
|
-
* @param options - Optional export settings (theme, padding, scale for resolution).
|
|
49972
|
-
*/
|
|
49973
|
-
async exportSelectedObjectsAsPng(options) {
|
|
49974
49918
|
try {
|
|
49975
|
-
const dataUrl = await this.
|
|
49919
|
+
const dataUrl = await selected.exportAs(format, this.getObjectExportContext());
|
|
49976
49920
|
if (!dataUrl) {
|
|
49977
|
-
console.
|
|
49921
|
+
console.error(`Failed to export selected object as ${format}: export returned no data URL`);
|
|
49978
49922
|
return;
|
|
49979
49923
|
}
|
|
49924
|
+
const activeWorkspaceName = this.core.store.state?.activeWorkspace?.name || 'workspace';
|
|
49925
|
+
const objectName = selected.__class__.replace(/^Kritzel/, '').toLowerCase() || 'object';
|
|
49980
49926
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
49981
|
-
const filename =
|
|
49982
|
-
|
|
49983
|
-
link.download = filename;
|
|
49984
|
-
link.href = dataUrl;
|
|
49985
|
-
link.click();
|
|
49927
|
+
const filename = `${activeWorkspaceName}-${objectName}-${timestamp}.${format}`;
|
|
49928
|
+
this.triggerDataUrlDownload(dataUrl, filename);
|
|
49986
49929
|
}
|
|
49987
49930
|
catch (error) {
|
|
49988
|
-
console.error(
|
|
49931
|
+
console.error(`Failed to export selected object as ${format}:`, error);
|
|
49989
49932
|
}
|
|
49990
49933
|
}
|
|
49991
49934
|
/**
|
|
@@ -50180,6 +50123,73 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
|
|
|
50180
50123
|
const response = await fetch(dataUrl);
|
|
50181
50124
|
return response.blob();
|
|
50182
50125
|
}
|
|
50126
|
+
triggerDataUrlDownload(dataUrl, filename) {
|
|
50127
|
+
const link = document.createElement('a');
|
|
50128
|
+
link.download = filename;
|
|
50129
|
+
link.href = dataUrl;
|
|
50130
|
+
link.click();
|
|
50131
|
+
}
|
|
50132
|
+
getSingleSelectedExportableObject() {
|
|
50133
|
+
const selectedObjects = this.core.store.selectionGroup?.objects ?? [];
|
|
50134
|
+
if (selectedObjects.length !== 1) {
|
|
50135
|
+
return null;
|
|
50136
|
+
}
|
|
50137
|
+
const [selected] = selectedObjects;
|
|
50138
|
+
if (!ObjectHelper.isKritzelExportable(selected)) {
|
|
50139
|
+
return null;
|
|
50140
|
+
}
|
|
50141
|
+
return selected;
|
|
50142
|
+
}
|
|
50143
|
+
getObjectExportContext() {
|
|
50144
|
+
return {
|
|
50145
|
+
getObjectAsSvgDataUrl: async (object, options) => {
|
|
50146
|
+
const svg = await this.getObjectAsSvgMarkup(object, options?.rasterSafeForCanvas ?? false);
|
|
50147
|
+
return svg ? this.svgMarkupToDataUrl(svg) : null;
|
|
50148
|
+
},
|
|
50149
|
+
getObjectAsPngDataUrl: async (object) => {
|
|
50150
|
+
const svg = await this.getObjectAsSvgMarkup(object, true);
|
|
50151
|
+
return svg ? this.svgMarkupToPngDataUrl(svg) : null;
|
|
50152
|
+
},
|
|
50153
|
+
};
|
|
50154
|
+
}
|
|
50155
|
+
async getObjectAsSvgMarkup(object, rasterSafeForCanvas) {
|
|
50156
|
+
const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls([object], this.core.assetResolver);
|
|
50157
|
+
const svg = KritzelSvgExportHelper.generateSvg([object], {
|
|
50158
|
+
theme: this.theme,
|
|
50159
|
+
rasterSafeForCanvas,
|
|
50160
|
+
imageDataUrls,
|
|
50161
|
+
});
|
|
50162
|
+
return svg || null;
|
|
50163
|
+
}
|
|
50164
|
+
svgMarkupToDataUrl(svgMarkup) {
|
|
50165
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
|
|
50166
|
+
}
|
|
50167
|
+
async svgMarkupToPngDataUrl(svgMarkup) {
|
|
50168
|
+
const svgBlob = new Blob([svgMarkup], { type: 'image/svg+xml;charset=utf-8' });
|
|
50169
|
+
const svgBlobUrl = URL.createObjectURL(svgBlob);
|
|
50170
|
+
try {
|
|
50171
|
+
const img = await new Promise((resolve, reject) => {
|
|
50172
|
+
const element = new Image();
|
|
50173
|
+
element.onload = () => resolve(element);
|
|
50174
|
+
element.onerror = err => reject(err);
|
|
50175
|
+
element.src = svgBlobUrl;
|
|
50176
|
+
});
|
|
50177
|
+
const width = Math.max(1, Math.ceil(img.naturalWidth || img.width));
|
|
50178
|
+
const height = Math.max(1, Math.ceil(img.naturalHeight || img.height));
|
|
50179
|
+
const canvas = document.createElement('canvas');
|
|
50180
|
+
canvas.width = width;
|
|
50181
|
+
canvas.height = height;
|
|
50182
|
+
const context = canvas.getContext('2d');
|
|
50183
|
+
if (!context) {
|
|
50184
|
+
throw new Error('Could not create 2D canvas context for PNG export');
|
|
50185
|
+
}
|
|
50186
|
+
context.drawImage(img, 0, 0, width, height);
|
|
50187
|
+
return canvas.toDataURL('image/png');
|
|
50188
|
+
}
|
|
50189
|
+
finally {
|
|
50190
|
+
URL.revokeObjectURL(svgBlobUrl);
|
|
50191
|
+
}
|
|
50192
|
+
}
|
|
50183
50193
|
getImportedWorkspaceName(rawName) {
|
|
50184
50194
|
if (typeof rawName === 'string' && rawName.trim().length > 0) {
|
|
50185
50195
|
return rawName;
|
|
@@ -51659,10 +51669,9 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
|
|
|
51659
51669
|
"getScreenshot": [64],
|
|
51660
51670
|
"exportViewportAsPng": [64],
|
|
51661
51671
|
"exportViewportAsSvg": [64],
|
|
51662
|
-
"
|
|
51663
|
-
"
|
|
51664
|
-
"
|
|
51665
|
-
"exportSelectedObjectsAsPng": [64],
|
|
51672
|
+
"getSelectedObjectSupportedExportFormats": [64],
|
|
51673
|
+
"canExportSelectedObjectAs": [64],
|
|
51674
|
+
"exportSelectedObject": [64],
|
|
51666
51675
|
"exportAsJson": [64],
|
|
51667
51676
|
"importFromJson": [64],
|
|
51668
51677
|
"loadObjectsFromJson": [64],
|