@editframe/elements 0.33.0-beta → 0.34.6-beta
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/EF_FRAMEGEN.js +5 -3
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.94.0 → _@oxc-project_runtime@0.95.0}/helpers/decorate.js +1 -1
- package/dist/canvas/EFCanvas.d.ts +7 -4
- package/dist/canvas/EFCanvas.js +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +4 -4
- package/dist/canvas/EFCanvasItem.js +1 -1
- package/dist/canvas/overlays/SelectionOverlay.d.ts +95 -0
- package/dist/canvas/overlays/SelectionOverlay.js +1 -1
- package/dist/canvas/selection/SelectionController.js +7 -11
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +25 -7
- package/dist/elements/EFAudio.js +31 -61
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +65 -52
- package/dist/elements/EFCaptions.js +186 -400
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +34 -6
- package/dist/elements/EFImage.js +114 -79
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +17 -17
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +41 -25
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +4 -4
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +31 -17
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +17 -9
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +66 -20
- package/dist/elements/EFMedia.js +412 -30
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +4 -4
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFSourceMixin.js +43 -15
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +23 -10
- package/dist/elements/EFSurface.js +64 -22
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +8 -2
- package/dist/elements/EFTemporal.js +42 -31
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +11 -2
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTextSegment.js +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +4 -4
- package/dist/elements/EFThumbnailStrip.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +22 -8
- package/dist/elements/EFTimegroup.js +203 -115
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +57 -20
- package/dist/elements/EFVideo.js +324 -72
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +33 -7
- package/dist/elements/EFWaveform.js +103 -59
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js +14 -3
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +1 -1
- package/dist/gui/Controllable.d.ts +2 -0
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFConfiguration.js +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFDial.js +1 -1
- package/dist/gui/EFFilmstrip.d.ts +3 -2
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.js +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFFocusOverlay.js +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayItem.js +1 -1
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.js +1 -1
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPause.js +1 -1
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPlay.js +1 -1
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFResizableBox.js +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFToggleLoop.js +1 -1
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js +1 -1
- package/dist/gui/EFTransformHandles.d.ts +4 -4
- package/dist/gui/EFTransformHandles.js +1 -1
- package/dist/gui/EFWorkbench.d.ts +5 -4
- package/dist/gui/EFWorkbench.js +1 -1
- package/dist/gui/PlaybackController.d.ts +10 -2
- package/dist/gui/PlaybackController.js +52 -30
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.js +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +6 -2
- package/dist/gui/timeline/EFTimeline.js +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +57 -0
- package/dist/gui/timeline/EFTimelineRow.js +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/TrimHandles.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.d.ts +2 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +58 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +1 -1
- package/dist/gui/timeline/tracks/HTMLTrack.d.ts +13 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.d.ts +14 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +26 -0
- package/dist/gui/timeline/tracks/TextTrack.js +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +47 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +4 -12
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +81 -0
- package/dist/gui/timeline/tracks/TrackItem.js +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +25 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.d.ts +14 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +1 -1
- package/dist/gui/timeline/tracks/ensureTrackItemInit.d.ts +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.d.ts +9 -0
- package/dist/gui/tree/EFTree.d.ts +5 -4
- package/dist/gui/tree/EFTree.js +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/preview/AdaptiveResolutionTracker.js +6 -14
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +123 -0
- package/dist/preview/FrameController.js +216 -0
- package/dist/preview/FrameController.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +1 -0
- package/dist/preview/RenderContext.js +193 -0
- package/dist/preview/RenderContext.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +166 -0
- package/dist/preview/encoding/canvasEncoder.js.map +1 -0
- package/dist/preview/encoding/mainThreadEncoder.js +39 -0
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -0
- package/dist/preview/encoding/types.d.ts +1 -0
- package/dist/preview/encoding/workerEncoder.js +58 -0
- package/dist/preview/encoding/workerEncoder.js.map +1 -0
- package/dist/preview/logger.js +41 -0
- package/dist/preview/logger.js.map +1 -0
- package/dist/preview/previewTypes.js +11 -10
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderTimegroupPreview.js +259 -236
- package/dist/preview/renderTimegroupPreview.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.d.ts +5 -0
- package/dist/preview/renderTimegroupToCanvas.js +100 -489
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.d.ts +1 -0
- package/dist/preview/renderTimegroupToVideo.js +80 -22
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/inlineImages.js +56 -0
- package/dist/preview/rendering/inlineImages.js.map +1 -0
- package/dist/preview/rendering/renderToImage.d.ts +1 -0
- package/dist/preview/rendering/renderToImage.js +120 -0
- package/dist/preview/rendering/renderToImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageForeignObject.js +135 -0
- package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.d.ts +1 -0
- package/dist/preview/rendering/renderToImageNative.js +129 -0
- package/dist/preview/rendering/renderToImageNative.js.map +1 -0
- package/dist/preview/rendering/svgSerializer.js +43 -0
- package/dist/preview/rendering/svgSerializer.js.map +1 -0
- package/dist/preview/rendering/types.d.ts +2 -0
- package/dist/preview/statsTrackingStrategy.js +3 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +8 -57
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +35 -0
- package/dist/render/EFRenderAPI.js +1 -0
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/sandbox/PlaybackControls.d.ts +1 -0
- package/dist/sandbox/ScenarioRunner.d.ts +1 -0
- package/dist/sandbox/defineSandbox.d.ts +1 -0
- package/dist/sandbox/index.d.ts +3 -0
- package/dist/style.css +3 -0
- package/dist/transcoding/types/index.d.ts +6 -3
- package/package.json +2 -3
- package/test/EFVideo.framegen.browsertest.ts +8 -1
- package/test/profilingPlugin.ts +1 -3
- package/test/setup.ts +23 -1
- package/dist/EF_INTERACTIVE.js +0 -7
- package/dist/EF_INTERACTIVE.js.map +0 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +0 -50
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +0 -12
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +0 -104
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -168
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +0 -46
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +0 -49
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +0 -30
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +0 -49
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +0 -47
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -140
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +0 -1
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +0 -13
- package/dist/elements/EFMedia/shared/BufferUtils.js +0 -86
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +0 -1
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +0 -90
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +0 -80
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -49
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +0 -58
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +0 -71
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -52
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +0 -50
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +0 -109
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +0 -12
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +0 -97
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +0 -1
- package/dist/elements/SampleBuffer.d.ts +0 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderers.js","names":[],"sources":["../../src/preview/renderers.ts"],"sourcesContent":["/**\n * Renderer strategy pattern for HTML-to-image rendering.\n * Provides a unified interface for native (drawElementImage) and foreignObject paths.\n */\n\nimport { isNativeCanvasApiAvailable, getRenderMode, type RenderMode } from \"./previewSettings.js\";\n\n/**\n * Options for rendering HTML to an image or canvas.\n */\nexport interface RenderOptions {\n /** Skip device pixel ratio scaling (render at logical pixels) */\n skipDprScaling?: boolean;\n /** Scale factor for encoding internal canvases (foreignObject only) */\n canvasScale?: number;\n /** Whether to reuse an existing canvas (native only) */\n reuseCanvas?: HTMLCanvasElement;\n
|
|
1
|
+
{"version":3,"file":"renderers.js","names":[],"sources":["../../src/preview/renderers.ts"],"sourcesContent":["/**\n * Renderer strategy pattern for HTML-to-image rendering.\n * Provides a unified interface for native (drawElementImage) and foreignObject paths.\n */\n\nimport { isNativeCanvasApiAvailable, getRenderMode, type RenderMode } from \"./previewSettings.js\";\n\n/**\n * Options for rendering HTML to an image or canvas.\n */\nexport interface RenderOptions {\n /** Skip device pixel ratio scaling (render at logical pixels) */\n skipDprScaling?: boolean;\n /** Scale factor for encoding internal canvases (foreignObject only) */\n canvasScale?: number;\n /** Whether to reuse an existing canvas (native only) */\n reuseCanvas?: HTMLCanvasElement;\n}\n\n/**\n * Result of a render operation.\n * Native path returns a canvas, foreignObject path returns an image.\n */\nexport type RenderResult = HTMLCanvasElement | HTMLImageElement;\n\n/**\n * Renderer interface for HTML-to-image conversion.\n */\nexport interface Renderer {\n /** The render mode this renderer implements */\n readonly mode: RenderMode;\n \n /**\n * Render an HTML container to an image or canvas.\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns Promise resolving to a canvas or image element\n */\n render(\n container: HTMLElement,\n width: number,\n height: number,\n options?: RenderOptions,\n ): Promise<RenderResult>;\n \n /**\n * Check if this renderer is available in the current environment.\n */\n isAvailable(): boolean;\n}\n\n/**\n * Get the effective render mode, validating that native is available when selected.\n * Falls back to foreignObject if native is selected but not available.\n */\nexport function getEffectiveRenderMode(): RenderMode {\n const mode = getRenderMode();\n \n if (mode === \"native\" && !isNativeCanvasApiAvailable()) {\n return \"foreignObject\";\n }\n \n return mode;\n}\n\n/**\n * Check if a render result is a canvas element.\n */\nexport function isCanvas(result: RenderResult): result is HTMLCanvasElement {\n return result instanceof HTMLCanvasElement;\n}\n\n/**\n * Check if a render result is an image element.\n */\nexport function isImage(result: RenderResult): result is HTMLImageElement {\n return result instanceof HTMLImageElement;\n}\n"],"mappings":";;;;;;;AAyDA,SAAgB,yBAAqC;CACnD,MAAM,OAAO,eAAe;AAE5B,KAAI,SAAS,YAAY,CAAC,4BAA4B,CACpD,QAAO;AAGT,QAAO"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { logger } from "../logger.js";
|
|
2
|
+
|
|
3
|
+
//#region src/preview/rendering/inlineImages.ts
|
|
4
|
+
/** Maximum number of cached inline images before eviction */
|
|
5
|
+
const MAX_INLINE_IMAGE_CACHE_SIZE = 100;
|
|
6
|
+
/** Image cache for inlining external images as data URIs (foreignObject path) */
|
|
7
|
+
const _inlineImageCache = /* @__PURE__ */ new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Convert a Blob to a data URL.
|
|
10
|
+
*/
|
|
11
|
+
function blobToDataURL(blob) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const reader = new FileReader();
|
|
14
|
+
reader.onload = () => resolve(reader.result);
|
|
15
|
+
reader.onerror = reject;
|
|
16
|
+
reader.readAsDataURL(blob);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Inline all images in a container as base64 data URIs.
|
|
21
|
+
* SVG foreignObject can't load external images due to security restrictions.
|
|
22
|
+
* Uses an LRU-style cache with size limits to prevent memory leaks.
|
|
23
|
+
*/
|
|
24
|
+
async function inlineImages(container) {
|
|
25
|
+
const images = container.querySelectorAll("img");
|
|
26
|
+
for (const image of images) {
|
|
27
|
+
const src = image.getAttribute("src");
|
|
28
|
+
if (!src || src.startsWith("data:")) continue;
|
|
29
|
+
const cached = _inlineImageCache.get(src);
|
|
30
|
+
if (cached) {
|
|
31
|
+
image.setAttribute("src", cached);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const dataUrl = await blobToDataURL(await (await fetch(src)).blob());
|
|
36
|
+
image.setAttribute("src", dataUrl);
|
|
37
|
+
if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {
|
|
38
|
+
const firstKey = _inlineImageCache.keys().next().value;
|
|
39
|
+
if (firstKey) _inlineImageCache.delete(firstKey);
|
|
40
|
+
}
|
|
41
|
+
_inlineImageCache.set(src, dataUrl);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
logger.warn("Failed to inline image:", src, e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clear the inline image cache. Useful for memory management in long-running sessions.
|
|
49
|
+
*/
|
|
50
|
+
function clearInlineImageCache() {
|
|
51
|
+
_inlineImageCache.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { clearInlineImageCache, inlineImages };
|
|
56
|
+
//# sourceMappingURL=inlineImages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inlineImages.js","names":[],"sources":["../../../src/preview/rendering/inlineImages.ts"],"sourcesContent":["/**\n * Image inlining utilities for SVG foreignObject rendering.\n * SVG foreignObject can't load external images due to security restrictions,\n * so we convert them to base64 data URIs.\n */\n\nimport { logger } from \"../logger.js\";\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nexport async function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n \n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n logger.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n"],"mappings":";;;;AASA,MAAM,8BAA8B;;AAGpC,MAAM,oCAAoB,IAAI,KAAqB;;;;AAKnD,SAAS,cAAc,MAA6B;AAClD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,YAAY;AAC/B,SAAO,eAAe,QAAQ,OAAO,OAAiB;AACtD,SAAO,UAAU;AACjB,SAAO,cAAc,KAAK;GAC1B;;;;;;;AAQJ,eAAsB,aAAa,WAAuC;CACxE,MAAM,SAAS,UAAU,iBAAiB,MAAM;AAChD,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM,aAAa,MAAM;AACrC,MAAI,CAAC,OAAO,IAAI,WAAW,QAAQ,CAAE;EAErC,MAAM,SAAS,kBAAkB,IAAI,IAAI;AACzC,MAAI,QAAQ;AACV,SAAM,aAAa,OAAO,OAAO;AACjC;;AAGF,MAAI;GAGF,MAAM,UAAU,MAAM,cADT,OADI,MAAM,MAAM,IAAI,EACL,MAAM,CACO;AACzC,SAAM,aAAa,OAAO,QAAQ;AAGlC,OAAI,kBAAkB,QAAQ,6BAA6B;IACzD,MAAM,WAAW,kBAAkB,MAAM,CAAC,MAAM,CAAC;AACjD,QAAI,SAAU,mBAAkB,OAAO,SAAS;;AAElD,qBAAkB,IAAI,KAAK,QAAQ;WAC5B,GAAG;AACV,UAAO,KAAK,2BAA2B,KAAK,EAAE;;;;;;;AAQpD,SAAgB,wBAA8B;AAC5C,mBAAkB,OAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getEffectiveRenderMode } from "../renderers.js";
|
|
2
|
+
import { defaultProfiler } from "../RenderProfiler.js";
|
|
3
|
+
import { renderToImageNative } from "./renderToImageNative.js";
|
|
4
|
+
import { inlineImages } from "./inlineImages.js";
|
|
5
|
+
import { encodeCanvasesInParallel } from "../encoding/canvasEncoder.js";
|
|
6
|
+
import { serializeToSvgDataUri } from "./renderToImageForeignObject.js";
|
|
7
|
+
|
|
8
|
+
//#region src/preview/rendering/renderToImage.ts
|
|
9
|
+
/**
|
|
10
|
+
* Check if an element or any of its ancestors has display:none.
|
|
11
|
+
* Used to skip encoding hidden canvases.
|
|
12
|
+
*/
|
|
13
|
+
function isElementHidden(element) {
|
|
14
|
+
let current = element;
|
|
15
|
+
while (current) {
|
|
16
|
+
if (current instanceof HTMLElement && current.style.display === "none") return true;
|
|
17
|
+
current = current.parentElement;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load an image from a data URI. Returns a Promise that resolves when loaded.
|
|
23
|
+
*/
|
|
24
|
+
function loadImageFromDataUri(dataUri) {
|
|
25
|
+
const img = new Image();
|
|
26
|
+
const imageLoadStart = performance.now();
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
img.onload = () => {
|
|
29
|
+
defaultProfiler.addTime("imageLoad", performance.now() - imageLoadStart);
|
|
30
|
+
resolve(img);
|
|
31
|
+
};
|
|
32
|
+
img.onerror = reject;
|
|
33
|
+
img.src = dataUri;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render HTML content to an image (or canvas) for drawing.
|
|
38
|
+
*
|
|
39
|
+
* Supports two rendering modes (configurable via previewSettings):
|
|
40
|
+
* - "native": Chrome's experimental drawElementImage API (fastest when available)
|
|
41
|
+
* - "foreignObject": SVG foreignObject serialization (fallback, works everywhere)
|
|
42
|
+
*
|
|
43
|
+
* @param container - The HTML element to render
|
|
44
|
+
* @param width - Target width in logical pixels
|
|
45
|
+
* @param height - Target height in logical pixels
|
|
46
|
+
* @param options - Rendering options
|
|
47
|
+
* @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject
|
|
48
|
+
*/
|
|
49
|
+
async function renderToImage(container, width, height, options) {
|
|
50
|
+
if (getEffectiveRenderMode() === "native") return renderToImageNative(container, width, height, options);
|
|
51
|
+
const allOriginalCanvases = Array.from(container.querySelectorAll("canvas"));
|
|
52
|
+
const clone = container.cloneNode(true);
|
|
53
|
+
const allClonedCanvases = Array.from(clone.querySelectorAll("canvas"));
|
|
54
|
+
const visibleIndices = [];
|
|
55
|
+
const visibleCanvases = [];
|
|
56
|
+
for (let i = 0; i < allOriginalCanvases.length; i++) {
|
|
57
|
+
const canvas = allOriginalCanvases[i];
|
|
58
|
+
if (!isElementHidden(canvas)) {
|
|
59
|
+
visibleIndices.push(i);
|
|
60
|
+
visibleCanvases.push(canvas);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const canvasScale = options?.canvasScale ?? 1;
|
|
64
|
+
const canvasStart = performance.now();
|
|
65
|
+
const encodedResults = await encodeCanvasesInParallel(visibleCanvases, {
|
|
66
|
+
scale: canvasScale,
|
|
67
|
+
renderContext: options?.renderContext,
|
|
68
|
+
sourceMap: options?.sourceMap
|
|
69
|
+
});
|
|
70
|
+
for (let j = 0; j < visibleCanvases.length; j++) {
|
|
71
|
+
const srcCanvas = visibleCanvases[j];
|
|
72
|
+
const dstCanvas = allClonedCanvases[visibleIndices[j]];
|
|
73
|
+
const encoded = encodedResults.find((r) => r.canvas === srcCanvas);
|
|
74
|
+
if (!dstCanvas || !encoded) continue;
|
|
75
|
+
try {
|
|
76
|
+
const img = document.createElement("img");
|
|
77
|
+
img.src = encoded.dataUrl;
|
|
78
|
+
img.width = srcCanvas.width;
|
|
79
|
+
img.height = srcCanvas.height;
|
|
80
|
+
const style = dstCanvas.getAttribute("style");
|
|
81
|
+
if (style) img.setAttribute("style", style);
|
|
82
|
+
dstCanvas.parentNode?.replaceChild(img, dstCanvas);
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
defaultProfiler.addTime("canvasEncode", performance.now() - canvasStart);
|
|
86
|
+
const inlineStart = performance.now();
|
|
87
|
+
await inlineImages(clone);
|
|
88
|
+
defaultProfiler.addTime("inline", performance.now() - inlineStart);
|
|
89
|
+
const { dataUri } = await serializeToSvgDataUri(clone, width, height);
|
|
90
|
+
return loadImageFromDataUri(dataUri);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Render a pre-built clone container to an image WITHOUT cloning it again.
|
|
94
|
+
* This is the fast path for reusing clone structures across frames.
|
|
95
|
+
*
|
|
96
|
+
* Key difference from renderToImage:
|
|
97
|
+
* - Does NOT call cloneNode (avoids expensive DOM duplication)
|
|
98
|
+
* - Converts canvases to images in-place, then restores them after serialization
|
|
99
|
+
* - Assumes the container already has refreshed canvas content
|
|
100
|
+
*
|
|
101
|
+
* @param container - Pre-built clone container with refreshed canvas content
|
|
102
|
+
* @param width - Output width
|
|
103
|
+
* @param height - Output height
|
|
104
|
+
* @returns Promise resolving to an HTMLImageElement
|
|
105
|
+
*/
|
|
106
|
+
async function renderToImageDirect(container, width, height) {
|
|
107
|
+
defaultProfiler.incrementRenderCount();
|
|
108
|
+
const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {
|
|
109
|
+
inlineImages: true,
|
|
110
|
+
logEarlyRenders: true
|
|
111
|
+
});
|
|
112
|
+
restore();
|
|
113
|
+
const image = await loadImageFromDataUri(dataUri);
|
|
114
|
+
defaultProfiler.shouldLogByFrameCount(100);
|
|
115
|
+
return image;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { loadImageFromDataUri, renderToImage, renderToImageDirect };
|
|
120
|
+
//# sourceMappingURL=renderToImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderToImage.js","names":["current: Element | null","visibleIndices: number[]","visibleCanvases: HTMLCanvasElement[]"],"sources":["../../../src/preview/rendering/renderToImage.ts"],"sourcesContent":["/**\n * Public rendering API facade.\n * Dispatches to native or foreignObject rendering paths based on settings.\n */\n\nimport type { ForeignObjectRenderOptions } from \"./types.js\";\nimport { renderToImageNative } from \"./renderToImageNative.js\";\nimport { serializeToSvgDataUri } from \"./renderToImageForeignObject.js\";\nimport { inlineImages } from \"./inlineImages.js\";\nimport { getEffectiveRenderMode } from \"../renderers.js\";\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Check if an element or any of its ancestors has display:none.\n * Used to skip encoding hidden canvases.\n */\nfunction isElementHidden(element: Element): boolean {\n let current: Element | null = element;\n while (current) {\n if (current instanceof HTMLElement && current.style.display === \"none\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n \n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n\n/**\n * Render HTML content to an image (or canvas) for drawing.\n * \n * Supports two rendering modes (configurable via previewSettings):\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject\n */\nexport async function renderToImage(\n container: HTMLElement,\n width: number,\n height: number,\n options?: ForeignObjectRenderOptions,\n): Promise<HTMLImageElement | HTMLCanvasElement> {\n const renderMode = getEffectiveRenderMode();\n \n // Native HTML-in-Canvas API path (fastest, requires Chrome flag)\n if (renderMode === \"native\") {\n return renderToImageNative(container, width, height, options);\n }\n \n // Fallback: SVG foreignObject serialization\n // Clone the container first (don't modify original)\n // Note: cloneNode doesn't copy canvas pixels, so we encode from original canvases\n const allOriginalCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const clone = container.cloneNode(true) as HTMLElement;\n const allClonedCanvases = Array.from(clone.querySelectorAll(\"canvas\"));\n \n // Filter out hidden canvases - they have display:none and won't render anyway\n // Keep track of indices to match with cloned canvases\n const visibleIndices: number[] = [];\n const visibleCanvases: HTMLCanvasElement[] = [];\n for (let i = 0; i < allOriginalCanvases.length; i++) {\n const canvas = allOriginalCanvases[i]!;\n if (!isElementHidden(canvas)) {\n visibleIndices.push(i);\n visibleCanvases.push(canvas);\n }\n }\n \n // Encode visible original canvases\n // Pass through renderContext and sourceMap for caching\n const canvasScale = options?.canvasScale ?? 1;\n const canvasStart = performance.now();\n const encodedResults = await encodeCanvasesInParallel(visibleCanvases, { \n scale: canvasScale,\n renderContext: options?.renderContext,\n sourceMap: options?.sourceMap,\n });\n \n // Map encoded results to corresponding cloned canvases using tracked indices\n for (let j = 0; j < visibleCanvases.length; j++) {\n const srcCanvas = visibleCanvases[j]!;\n const originalIndex = visibleIndices[j]!;\n const dstCanvas = allClonedCanvases[originalIndex];\n const encoded = encodedResults.find((r) => r.canvas === srcCanvas);\n \n if (!dstCanvas || !encoded) continue;\n \n try {\n const img = document.createElement(\"img\");\n img.src = encoded.dataUrl;\n img.width = srcCanvas.width;\n img.height = srcCanvas.height;\n const style = dstCanvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n dstCanvas.parentNode?.replaceChild(img, dstCanvas);\n } catch {\n // Cross-origin or other error - skip\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n\n // Inline external images in the clone\n const inlineStart = performance.now();\n await inlineImages(clone);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n\n // Use common serialization pipeline (no restore needed since we're working on a clone)\n const { dataUri } = await serializeToSvgDataUri(clone, width, height);\n \n // Load as image\n return loadImageFromDataUri(dataUri);\n}\n\n/**\n * Render a pre-built clone container to an image WITHOUT cloning it again.\n * This is the fast path for reusing clone structures across frames.\n * \n * Key difference from renderToImage:\n * - Does NOT call cloneNode (avoids expensive DOM duplication)\n * - Converts canvases to images in-place, then restores them after serialization\n * - Assumes the container already has refreshed canvas content\n * \n * @param container - Pre-built clone container with refreshed canvas content\n * @param width - Output width\n * @param height - Output height\n * @returns Promise resolving to an HTMLImageElement\n */\nexport async function renderToImageDirect(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<HTMLImageElement> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {\n inlineImages: true,\n logEarlyRenders: true,\n });\n restore();\n \n // Load as image\n const image = await loadImageFromDataUri(dataUri);\n \n // Log timing breakdown periodically\n defaultProfiler.shouldLogByFrameCount(100);\n \n return image;\n}\n\n/**\n * Prepare a frame's data URI without waiting for image load.\n * Returns the data URI asynchronously (after parallel canvas encoding and serialization) for pipelined loading.\n * The DOM is restored before this function returns.\n */\nexport async function prepareFrameDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<string> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height);\n restore();\n \n return dataUri;\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAS,gBAAgB,SAA2B;CAClD,IAAIA,UAA0B;AAC9B,QAAO,SAAS;AACd,MAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D,QAAO;AAET,YAAU,QAAQ;;AAEpB,QAAO;;;;;AAMT,SAAgB,qBAAqB,SAA4C;CAC/E,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,iBAAiB,YAAY,KAAK;AAExC,QAAO,IAAI,SAA2B,SAAS,WAAW;AACxD,MAAI,eAAe;AACjB,mBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;AACxE,WAAQ,IAAI;;AAEd,MAAI,UAAU;AACd,MAAI,MAAM;GACV;;;;;;;;;;;;;;;AAgBJ,eAAsB,cACpB,WACA,OACA,QACA,SAC+C;AAI/C,KAHmB,wBAAwB,KAGxB,SACjB,QAAO,oBAAoB,WAAW,OAAO,QAAQ,QAAQ;CAM/D,MAAM,sBAAsB,MAAM,KAAK,UAAU,iBAAiB,SAAS,CAAC;CAC5E,MAAM,QAAQ,UAAU,UAAU,KAAK;CACvC,MAAM,oBAAoB,MAAM,KAAK,MAAM,iBAAiB,SAAS,CAAC;CAItE,MAAMC,iBAA2B,EAAE;CACnC,MAAMC,kBAAuC,EAAE;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,oBAAoB,QAAQ,KAAK;EACnD,MAAM,SAAS,oBAAoB;AACnC,MAAI,CAAC,gBAAgB,OAAO,EAAE;AAC5B,kBAAe,KAAK,EAAE;AACtB,mBAAgB,KAAK,OAAO;;;CAMhC,MAAM,cAAc,SAAS,eAAe;CAC5C,MAAM,cAAc,YAAY,KAAK;CACrC,MAAM,iBAAiB,MAAM,yBAAyB,iBAAiB;EACrE,OAAO;EACP,eAAe,SAAS;EACxB,WAAW,SAAS;EACrB,CAAC;AAGF,MAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;EAC/C,MAAM,YAAY,gBAAgB;EAElC,MAAM,YAAY,kBADI,eAAe;EAErC,MAAM,UAAU,eAAe,MAAM,MAAM,EAAE,WAAW,UAAU;AAElE,MAAI,CAAC,aAAa,CAAC,QAAS;AAE5B,MAAI;GACF,MAAM,MAAM,SAAS,cAAc,MAAM;AACzC,OAAI,MAAM,QAAQ;AAClB,OAAI,QAAQ,UAAU;AACtB,OAAI,SAAS,UAAU;GACvB,MAAM,QAAQ,UAAU,aAAa,QAAQ;AAC7C,OAAI,MAAO,KAAI,aAAa,SAAS,MAAM;AAC3C,aAAU,YAAY,aAAa,KAAK,UAAU;UAC5C;;AAIV,iBAAgB,QAAQ,gBAAgB,YAAY,KAAK,GAAG,YAAY;CAGxE,MAAM,cAAc,YAAY,KAAK;AACrC,OAAM,aAAa,MAAM;AACzB,iBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;CAGlE,MAAM,EAAE,YAAY,MAAM,sBAAsB,OAAO,OAAO,OAAO;AAGrE,QAAO,qBAAqB,QAAQ;;;;;;;;;;;;;;;;AAiBtC,eAAsB,oBACpB,WACA,OACA,QAC2B;AAC3B,iBAAgB,sBAAsB;CAGtC,MAAM,EAAE,SAAS,YAAY,MAAM,sBAAsB,WAAW,OAAO,QAAQ;EACjF,cAAc;EACd,iBAAiB;EAClB,CAAC;AACF,UAAS;CAGT,MAAM,QAAQ,MAAM,qBAAqB,QAAQ;AAGjD,iBAAgB,sBAAsB,IAAI;AAE1C,QAAO"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { logger } from "../logger.js";
|
|
2
|
+
import { defaultProfiler } from "../RenderProfiler.js";
|
|
3
|
+
import { encodeBase64Fast } from "./svgSerializer.js";
|
|
4
|
+
import { inlineImages } from "./inlineImages.js";
|
|
5
|
+
import { encodeCanvasesInParallel } from "../encoding/canvasEncoder.js";
|
|
6
|
+
|
|
7
|
+
//#region src/preview/rendering/renderToImageForeignObject.ts
|
|
8
|
+
let _xmlSerializer = null;
|
|
9
|
+
let _textEncoder = null;
|
|
10
|
+
/**
|
|
11
|
+
* Check if an element or any of its ancestors has display:none.
|
|
12
|
+
* Used to skip encoding hidden canvases.
|
|
13
|
+
*/
|
|
14
|
+
function isElementHidden(element) {
|
|
15
|
+
let current = element;
|
|
16
|
+
while (current) {
|
|
17
|
+
if (current instanceof HTMLElement && current.style.display === "none") return true;
|
|
18
|
+
current = current.parentElement;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const SVG_PREFIX = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"";
|
|
23
|
+
const SVG_HEIGHT_PREFIX = "\" height=\"";
|
|
24
|
+
const SVG_MIDDLE = "\"><foreignObject width=\"100%\" height=\"100%\">";
|
|
25
|
+
const SVG_SUFFIX = "</foreignObject></svg>";
|
|
26
|
+
const DATA_URI_PREFIX = "data:image/svg+xml;base64,";
|
|
27
|
+
const WRAPPER_STYLE_BASE = "overflow:hidden;position:relative;";
|
|
28
|
+
/**
|
|
29
|
+
* Common SVG foreignObject serialization pipeline.
|
|
30
|
+
* Handles canvas encoding, serialization, and base64 encoding.
|
|
31
|
+
*
|
|
32
|
+
* @param container - The HTML element to serialize
|
|
33
|
+
* @param width - Output width
|
|
34
|
+
* @param height - Output height
|
|
35
|
+
* @param options - Serialization options
|
|
36
|
+
* @returns Serialization result with data URI and restore function
|
|
37
|
+
*/
|
|
38
|
+
async function serializeToSvgDataUri(container, width, height, options = {}) {
|
|
39
|
+
const { canvasScale = 1, inlineImages: shouldInlineImages = false, logEarlyRenders = false, renderContext, sourceMap } = options;
|
|
40
|
+
const canvasRestoreInfo = [];
|
|
41
|
+
const canvasStart = performance.now();
|
|
42
|
+
const visibleCanvases = Array.from(container.querySelectorAll("canvas")).filter((canvas) => !isElementHidden(canvas));
|
|
43
|
+
const canvasSnapshots = [];
|
|
44
|
+
for (let i = 0; i < visibleCanvases.length; i++) {
|
|
45
|
+
const canvas = visibleCanvases[i];
|
|
46
|
+
if (canvas.width > 0 && canvas.height > 0) {
|
|
47
|
+
const copy = document.createElement("canvas");
|
|
48
|
+
copy.width = canvas.width;
|
|
49
|
+
copy.height = canvas.height;
|
|
50
|
+
if (canvas.dataset.preserveAlpha) copy.dataset.preserveAlpha = canvas.dataset.preserveAlpha;
|
|
51
|
+
const ctx = copy.getContext("2d");
|
|
52
|
+
if (ctx) ctx.drawImage(canvas, 0, 0);
|
|
53
|
+
canvasSnapshots.push({
|
|
54
|
+
original: canvas,
|
|
55
|
+
copy
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const encodedWithOriginals = (await encodeCanvasesInParallel(canvasSnapshots.map((s) => s.copy), {
|
|
60
|
+
scale: canvasScale,
|
|
61
|
+
renderContext,
|
|
62
|
+
sourceMap
|
|
63
|
+
})).map((result) => {
|
|
64
|
+
const snapshot = canvasSnapshots.find((s) => s.copy === result.canvas);
|
|
65
|
+
return {
|
|
66
|
+
...result,
|
|
67
|
+
canvas: snapshot?.original ?? result.canvas
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
for (const { canvas, dataUrl } of encodedWithOriginals) try {
|
|
71
|
+
const img = document.createElement("img");
|
|
72
|
+
img.src = dataUrl;
|
|
73
|
+
img.width = canvas.width;
|
|
74
|
+
img.height = canvas.height;
|
|
75
|
+
const style = canvas.getAttribute("style");
|
|
76
|
+
if (style) img.setAttribute("style", style);
|
|
77
|
+
const parent = canvas.parentNode;
|
|
78
|
+
if (parent) {
|
|
79
|
+
const nextSibling = canvas.nextSibling;
|
|
80
|
+
parent.replaceChild(img, canvas);
|
|
81
|
+
canvasRestoreInfo.push({
|
|
82
|
+
canvas,
|
|
83
|
+
parent,
|
|
84
|
+
nextSibling,
|
|
85
|
+
img
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
defaultProfiler.addTime("canvasEncode", performance.now() - canvasStart);
|
|
90
|
+
if (shouldInlineImages) {
|
|
91
|
+
const inlineStart = performance.now();
|
|
92
|
+
await inlineImages(container);
|
|
93
|
+
defaultProfiler.addTime("inline", performance.now() - inlineStart);
|
|
94
|
+
}
|
|
95
|
+
const serializeStart = performance.now();
|
|
96
|
+
const wrapperElement = document.createElement("div");
|
|
97
|
+
wrapperElement.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
98
|
+
wrapperElement.setAttribute("style", `width:${width}px;height:${height}px;${WRAPPER_STYLE_BASE}`);
|
|
99
|
+
wrapperElement.appendChild(container);
|
|
100
|
+
if (!_xmlSerializer) _xmlSerializer = new XMLSerializer();
|
|
101
|
+
const perfStart = performance.now();
|
|
102
|
+
const serialized = _xmlSerializer.serializeToString(wrapperElement);
|
|
103
|
+
const serializeTime = performance.now() - perfStart;
|
|
104
|
+
if (Math.random() < .01) {
|
|
105
|
+
const elementCount = wrapperElement.querySelectorAll("*").length;
|
|
106
|
+
console.log(`[serialize] elements=${elementCount}, time=${serializeTime.toFixed(1)}ms, size=${(serialized.length / 1024).toFixed(1)}KB`);
|
|
107
|
+
}
|
|
108
|
+
defaultProfiler.addTime("serialize", performance.now() - serializeStart);
|
|
109
|
+
const restore = () => {
|
|
110
|
+
const restoreStart = performance.now();
|
|
111
|
+
if (container.parentNode === wrapperElement) wrapperElement.removeChild(container);
|
|
112
|
+
for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) if (img.parentNode === parent) parent.replaceChild(canvas, img);
|
|
113
|
+
else if (canvas.parentNode !== parent) if (nextSibling && nextSibling.parentNode === parent) parent.insertBefore(canvas, nextSibling);
|
|
114
|
+
else parent.appendChild(canvas);
|
|
115
|
+
defaultProfiler.addTime("restore", performance.now() - restoreStart);
|
|
116
|
+
};
|
|
117
|
+
if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) logger.debug(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);
|
|
118
|
+
const base64Start = performance.now();
|
|
119
|
+
const svg = SVG_PREFIX + width + SVG_HEIGHT_PREFIX + height + SVG_MIDDLE + serialized + SVG_SUFFIX;
|
|
120
|
+
if (!_textEncoder) _textEncoder = new TextEncoder();
|
|
121
|
+
const utf8Bytes = _textEncoder.encode(svg);
|
|
122
|
+
let base64;
|
|
123
|
+
if (typeof Uint8Array.prototype.toBase64 === "function") base64 = utf8Bytes.toBase64();
|
|
124
|
+
else base64 = encodeBase64Fast(utf8Bytes);
|
|
125
|
+
const dataUri = DATA_URI_PREFIX + base64;
|
|
126
|
+
defaultProfiler.addTime("base64", performance.now() - base64Start);
|
|
127
|
+
return {
|
|
128
|
+
dataUri,
|
|
129
|
+
restore
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { serializeToSvgDataUri };
|
|
135
|
+
//# sourceMappingURL=renderToImageForeignObject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderToImageForeignObject.js","names":["_xmlSerializer: XMLSerializer | null","_textEncoder: TextEncoder | null","current: Element | null","canvasRestoreInfo: CanvasRestoreInfo[]","canvasSnapshots: { original: HTMLCanvasElement; copy: HTMLCanvasElement }[]","base64: string"],"sources":["../../../src/preview/rendering/renderToImageForeignObject.ts"],"sourcesContent":["/**\n * SVG foreignObject rendering path with serialization.\n */\n\nimport type { SerializeToSvgOptions, SerializationResult, CanvasRestoreInfo } from \"./types.js\";\nimport { encodeBase64Fast } from \"./svgSerializer.js\";\nimport { inlineImages } from \"./inlineImages.js\";\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\nimport { logger } from \"../logger.js\";\n\n// Reusable instances for better performance (avoid creating new instances every frame)\n// Note: wrapper element is NOT reused - each concurrent frame needs its own wrapper\nlet _xmlSerializer: XMLSerializer | null = null;\nlet _textEncoder: TextEncoder | null = null;\n\n/**\n * Check if an element or any of its ancestors has display:none.\n * Used to skip encoding hidden canvases.\n */\nfunction isElementHidden(element: Element): boolean {\n let current: Element | null = element;\n while (current) {\n if (current instanceof HTMLElement && current.style.display === \"none\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\n// Pre-computed SVG constants\nconst SVG_PREFIX = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"';\nconst SVG_HEIGHT_PREFIX = '\" height=\"';\nconst SVG_MIDDLE = '\"><foreignObject width=\"100%\" height=\"100%\">';\nconst SVG_SUFFIX = '</foreignObject></svg>';\nconst DATA_URI_PREFIX = 'data:image/svg+xml;base64,';\n\n// Shared style string to reduce allocations\nconst WRAPPER_STYLE_BASE = \"overflow:hidden;position:relative;\";\n\n/**\n * Common SVG foreignObject serialization pipeline.\n * Handles canvas encoding, serialization, and base64 encoding.\n * \n * @param container - The HTML element to serialize\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options\n * @returns Serialization result with data URI and restore function\n */\nexport async function serializeToSvgDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n options: SerializeToSvgOptions = {},\n): Promise<SerializationResult> {\n const { \n canvasScale = 1, \n inlineImages: shouldInlineImages = false, \n logEarlyRenders = false,\n renderContext,\n sourceMap,\n } = options;\n \n // Store info for restoration (only used if modifying in-place)\n const canvasRestoreInfo: CanvasRestoreInfo[] = [];\n \n // Phase 1: Encode canvases to data URLs (parallel)\n // Filter out hidden canvases - they have display:none and won't render anyway\n const canvasStart = performance.now();\n const allCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const visibleCanvases = allCanvases.filter(canvas => !isElementHidden(canvas));\n \n // CRITICAL FIX: Synchronously copy canvas pixels BEFORE any async work.\n // This prevents race conditions where concurrent render tasks overwrite\n // the shared clone canvases while encoding is in progress.\n // See: Hypothesis 1 - Clone Canvas Overwritten During Serialization\n const canvasSnapshots: { original: HTMLCanvasElement; copy: HTMLCanvasElement }[] = [];\n for (let i = 0; i < visibleCanvases.length; i++) {\n const canvas = visibleCanvases[i]!;\n if (canvas.width > 0 && canvas.height > 0) {\n const copy = document.createElement(\"canvas\");\n copy.width = canvas.width;\n copy.height = canvas.height;\n // Copy dataset attributes (e.g., preserveAlpha)\n if (canvas.dataset.preserveAlpha) {\n copy.dataset.preserveAlpha = canvas.dataset.preserveAlpha;\n }\n const ctx = copy.getContext(\"2d\");\n if (ctx) {\n // drawImage is SYNCHRONOUS - pixels are copied immediately\n ctx.drawImage(canvas, 0, 0);\n }\n canvasSnapshots.push({ original: canvas, copy });\n }\n }\n \n // Encode from the snapshot copies (safe from concurrent overwrites)\n const snapshotCanvases = canvasSnapshots.map(s => s.copy);\n const encodedResults = await encodeCanvasesInParallel(snapshotCanvases, { \n scale: canvasScale,\n renderContext,\n sourceMap,\n });\n \n // Map encoded results back to original canvases for DOM replacement\n const encodedWithOriginals = encodedResults.map(result => {\n const snapshot = canvasSnapshots.find(s => s.copy === result.canvas);\n return {\n ...result,\n canvas: snapshot?.original ?? result.canvas,\n };\n });\n \n // Replace canvases with images\n for (const { canvas, dataUrl } of encodedWithOriginals) {\n try {\n const img = document.createElement(\"img\");\n img.src = dataUrl;\n img.width = canvas.width;\n img.height = canvas.height;\n const style = canvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n \n const parent = canvas.parentNode;\n if (parent) {\n const nextSibling = canvas.nextSibling;\n parent.replaceChild(img, canvas);\n canvasRestoreInfo.push({ canvas, parent, nextSibling, img });\n }\n } catch {\n // Cross-origin canvas - leave as-is\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n \n // Phase 2: Inline external images (if requested)\n if (shouldInlineImages) {\n const inlineStart = performance.now();\n await inlineImages(container);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n }\n \n // Phase 3: Serialize to XHTML\n const serializeStart = performance.now();\n \n // Create fresh wrapper element for THIS frame (local variable for closure safety)\n // Multiple concurrent frames in video export each get their own wrapper\n const wrapperElement = document.createElement(\"div\");\n wrapperElement.setAttribute(\"xmlns\", \"http://www.w3.org/1999/xhtml\");\n wrapperElement.setAttribute(\"style\", `width:${width}px;height:${height}px;${WRAPPER_STYLE_BASE}`);\n wrapperElement.appendChild(container);\n \n if (!_xmlSerializer) {\n _xmlSerializer = new XMLSerializer();\n }\n \n // NOTE: Hidden element handling is now done by the caller via removeHiddenNodesForSerialization().\n // The caller physically removes hidden nodes from the clone tree BEFORE calling this function,\n // so hidden elements are never serialized at all - not just hidden with display:none.\n //\n // Benefits of removing before serialization:\n // - Hidden canvases are not encoded (saves encoding time and memory)\n // - Hidden elements are not serialized (smaller SVG, faster serialization)\n // - Hidden images are not inlined (saves fetch and encoding)\n // - The serialized output is smaller and faster to base64 encode\n \n // Serialize to XHTML string\n const perfStart = performance.now();\n const serialized = _xmlSerializer.serializeToString(wrapperElement);\n const serializeTime = performance.now() - perfStart;\n \n // Sample 1% of frames to avoid spam\n if (Math.random() < 0.01) {\n const elementCount = wrapperElement.querySelectorAll('*').length;\n console.log(`[serialize] elements=${elementCount}, time=${serializeTime.toFixed(1)}ms, size=${(serialized.length / 1024).toFixed(1)}KB`);\n }\n\n defaultProfiler.addTime(\"serialize\", performance.now() - serializeStart);\n \n // Prepare restore function (removes container from wrapper, restores canvases)\n // Must be robust against concurrent frame rendering where DOM state may change\n const restore = (): void => {\n const restoreStart = performance.now();\n \n // Guard: only remove if container is still a child of wrapper\n if (container.parentNode === wrapperElement) {\n wrapperElement.removeChild(container);\n }\n \n for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) {\n // Guard: only restore if img is still in expected position\n if (img.parentNode === parent) {\n // Use replaceChild which is atomic and safer than insertBefore + removeChild\n parent.replaceChild(canvas, img);\n } else if (canvas.parentNode !== parent) {\n // Canvas was never restored and img was moved/removed - try to restore canvas\n if (nextSibling && nextSibling.parentNode === parent) {\n parent.insertBefore(canvas, nextSibling);\n } else {\n parent.appendChild(canvas);\n }\n }\n }\n defaultProfiler.addTime(\"restore\", performance.now() - restoreStart);\n };\n \n // DEBUG: Log serialized HTML size for early renders\n if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) {\n logger.debug(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);\n }\n \n // Phase 4: Create SVG and encode to base64\n const base64Start = performance.now();\n \n // Build SVG string with minimal allocations (concatenation is faster for small strings)\n const svg = SVG_PREFIX + width + SVG_HEIGHT_PREFIX + height + SVG_MIDDLE + serialized + SVG_SUFFIX;\n \n if (!_textEncoder) {\n _textEncoder = new TextEncoder();\n }\n const utf8Bytes = _textEncoder.encode(svg);\n \n let base64: string;\n if (typeof (Uint8Array.prototype as any).toBase64 === \"function\") {\n base64 = (utf8Bytes as any).toBase64();\n } else {\n base64 = encodeBase64Fast(utf8Bytes);\n }\n const dataUri = DATA_URI_PREFIX + base64;\n defaultProfiler.addTime(\"base64\", performance.now() - base64Start);\n \n return { dataUri, restore };\n}\n"],"mappings":";;;;;;;AAaA,IAAIA,iBAAuC;AAC3C,IAAIC,eAAmC;;;;;AAMvC,SAAS,gBAAgB,SAA2B;CAClD,IAAIC,UAA0B;AAC9B,QAAO,SAAS;AACd,MAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D,QAAO;AAET,YAAU,QAAQ;;AAEpB,QAAO;;AAIT,MAAM,aAAa;AACnB,MAAM,oBAAoB;AAC1B,MAAM,aAAa;AACnB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAGxB,MAAM,qBAAqB;;;;;;;;;;;AAY3B,eAAsB,sBACpB,WACA,OACA,QACA,UAAiC,EAAE,EACL;CAC9B,MAAM,EACJ,cAAc,GACd,cAAc,qBAAqB,OACnC,kBAAkB,OAClB,eACA,cACE;CAGJ,MAAMC,oBAAyC,EAAE;CAIjD,MAAM,cAAc,YAAY,KAAK;CAErC,MAAM,kBADc,MAAM,KAAK,UAAU,iBAAiB,SAAS,CAAC,CAChC,QAAO,WAAU,CAAC,gBAAgB,OAAO,CAAC;CAM9E,MAAMC,kBAA8E,EAAE;AACtF,MAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;EAC/C,MAAM,SAAS,gBAAgB;AAC/B,MAAI,OAAO,QAAQ,KAAK,OAAO,SAAS,GAAG;GACzC,MAAM,OAAO,SAAS,cAAc,SAAS;AAC7C,QAAK,QAAQ,OAAO;AACpB,QAAK,SAAS,OAAO;AAErB,OAAI,OAAO,QAAQ,cACjB,MAAK,QAAQ,gBAAgB,OAAO,QAAQ;GAE9C,MAAM,MAAM,KAAK,WAAW,KAAK;AACjC,OAAI,IAEF,KAAI,UAAU,QAAQ,GAAG,EAAE;AAE7B,mBAAgB,KAAK;IAAE,UAAU;IAAQ;IAAM,CAAC;;;CAapD,MAAM,wBAPiB,MAAM,yBADJ,gBAAgB,KAAI,MAAK,EAAE,KAAK,EACe;EACtE,OAAO;EACP;EACA;EACD,CAAC,EAG0C,KAAI,WAAU;EACxD,MAAM,WAAW,gBAAgB,MAAK,MAAK,EAAE,SAAS,OAAO,OAAO;AACpE,SAAO;GACL,GAAG;GACH,QAAQ,UAAU,YAAY,OAAO;GACtC;GACD;AAGF,MAAK,MAAM,EAAE,QAAQ,aAAa,qBAChC,KAAI;EACF,MAAM,MAAM,SAAS,cAAc,MAAM;AACzC,MAAI,MAAM;AACV,MAAI,QAAQ,OAAO;AACnB,MAAI,SAAS,OAAO;EACpB,MAAM,QAAQ,OAAO,aAAa,QAAQ;AAC1C,MAAI,MAAO,KAAI,aAAa,SAAS,MAAM;EAE3C,MAAM,SAAS,OAAO;AACtB,MAAI,QAAQ;GACV,MAAM,cAAc,OAAO;AAC3B,UAAO,aAAa,KAAK,OAAO;AAChC,qBAAkB,KAAK;IAAE;IAAQ;IAAQ;IAAa;IAAK,CAAC;;SAExD;AAIV,iBAAgB,QAAQ,gBAAgB,YAAY,KAAK,GAAG,YAAY;AAGxE,KAAI,oBAAoB;EACtB,MAAM,cAAc,YAAY,KAAK;AACrC,QAAM,aAAa,UAAU;AAC7B,kBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;;CAIpE,MAAM,iBAAiB,YAAY,KAAK;CAIxC,MAAM,iBAAiB,SAAS,cAAc,MAAM;AACpD,gBAAe,aAAa,SAAS,+BAA+B;AACpE,gBAAe,aAAa,SAAS,SAAS,MAAM,YAAY,OAAO,KAAK,qBAAqB;AACjG,gBAAe,YAAY,UAAU;AAErC,KAAI,CAAC,eACH,kBAAiB,IAAI,eAAe;CActC,MAAM,YAAY,YAAY,KAAK;CACnC,MAAM,aAAa,eAAe,kBAAkB,eAAe;CACnE,MAAM,gBAAgB,YAAY,KAAK,GAAG;AAG1C,KAAI,KAAK,QAAQ,GAAG,KAAM;EACxB,MAAM,eAAe,eAAe,iBAAiB,IAAI,CAAC;AAC1D,UAAQ,IAAI,wBAAwB,aAAa,SAAS,cAAc,QAAQ,EAAE,CAAC,YAAY,WAAW,SAAS,MAAM,QAAQ,EAAE,CAAC,IAAI;;AAG1I,iBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;CAIxE,MAAM,gBAAsB;EAC1B,MAAM,eAAe,YAAY,KAAK;AAGtC,MAAI,UAAU,eAAe,eAC3B,gBAAe,YAAY,UAAU;AAGvC,OAAK,MAAM,EAAE,QAAQ,QAAQ,aAAa,SAAS,kBAEjD,KAAI,IAAI,eAAe,OAErB,QAAO,aAAa,QAAQ,IAAI;WACvB,OAAO,eAAe,OAE/B,KAAI,eAAe,YAAY,eAAe,OAC5C,QAAO,aAAa,QAAQ,YAAY;MAExC,QAAO,YAAY,OAAO;AAIhC,kBAAgB,QAAQ,WAAW,YAAY,KAAK,GAAG,aAAa;;AAItE,KAAI,mBAAmB,gBAAgB,cAAc,EAAE,CACrD,QAAO,MAAM,0CAA0C,WAAW,OAAO,QAAQ;CAInF,MAAM,cAAc,YAAY,KAAK;CAGrC,MAAM,MAAM,aAAa,QAAQ,oBAAoB,SAAS,aAAa,aAAa;AAExF,KAAI,CAAC,aACH,gBAAe,IAAI,aAAa;CAElC,MAAM,YAAY,aAAa,OAAO,IAAI;CAE1C,IAAIC;AACJ,KAAI,OAAQ,WAAW,UAAkB,aAAa,WACpD,UAAU,UAAkB,UAAU;KAEtC,UAAS,iBAAiB,UAAU;CAEtC,MAAM,UAAU,kBAAkB;AAClC,iBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;AAElE,QAAO;EAAE;EAAS;EAAS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { defaultProfiler } from "../RenderProfiler.js";
|
|
2
|
+
|
|
3
|
+
//#region src/preview/rendering/renderToImageNative.ts
|
|
4
|
+
/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */
|
|
5
|
+
const _layoutInitializedCanvases = /* @__PURE__ */ new WeakSet();
|
|
6
|
+
/**
|
|
7
|
+
* Wait for next animation frame (allows browser to complete layout)
|
|
8
|
+
*/
|
|
9
|
+
function waitForFrame() {
|
|
10
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a canvas element with proper DPR handling.
|
|
14
|
+
* Buffer size is based on renderWidth/renderHeight (internal resolution).
|
|
15
|
+
* CSS size is based on fullWidth/fullHeight (logical display size).
|
|
16
|
+
*/
|
|
17
|
+
function createDprCanvas(options) {
|
|
18
|
+
const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;
|
|
19
|
+
const dpr = options.dpr ?? window.devicePixelRatio ?? 1;
|
|
20
|
+
const canvas = document.createElement("canvas");
|
|
21
|
+
canvas.width = Math.floor(renderWidth * scale * dpr);
|
|
22
|
+
canvas.height = Math.floor(renderHeight * scale * dpr);
|
|
23
|
+
canvas.style.width = `${Math.floor(fullWidth * scale)}px`;
|
|
24
|
+
canvas.style.height = `${Math.floor(fullHeight * scale)}px`;
|
|
25
|
+
return canvas;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).
|
|
29
|
+
* This is much faster than the foreignObject approach and avoids canvas tainting.
|
|
30
|
+
*
|
|
31
|
+
* Note: The native API renders at device pixel ratio, so we capture at DPR scale
|
|
32
|
+
* and then downsample to logical pixels to match the foreignObject path's output.
|
|
33
|
+
*
|
|
34
|
+
* @param container - The HTML element to render
|
|
35
|
+
* @param width - Target width in logical pixels
|
|
36
|
+
* @param height - Target height in logical pixels
|
|
37
|
+
* @param options - Rendering options (skipWait for batch mode)
|
|
38
|
+
*
|
|
39
|
+
* @see https://github.com/WICG/html-in-canvas
|
|
40
|
+
*/
|
|
41
|
+
async function renderToImageNative(container, width, height, options = {}) {
|
|
42
|
+
const t0 = performance.now();
|
|
43
|
+
const { reuseCanvas, skipDprScaling = false } = options;
|
|
44
|
+
const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;
|
|
45
|
+
let captureCanvas;
|
|
46
|
+
let shouldCleanup = false;
|
|
47
|
+
if (reuseCanvas) {
|
|
48
|
+
captureCanvas = reuseCanvas;
|
|
49
|
+
const dpr$1 = skipDprScaling ? 1 : window.devicePixelRatio || 1;
|
|
50
|
+
const targetWidth = Math.floor(width * dpr$1);
|
|
51
|
+
const targetHeight = Math.floor(height * dpr$1);
|
|
52
|
+
if (captureCanvas.width !== targetWidth) captureCanvas.width = targetWidth;
|
|
53
|
+
if (captureCanvas.height !== targetHeight) captureCanvas.height = targetHeight;
|
|
54
|
+
captureCanvas.style.cssText = `
|
|
55
|
+
position: fixed;
|
|
56
|
+
left: 0;
|
|
57
|
+
top: 0;
|
|
58
|
+
width: ${width}px;
|
|
59
|
+
height: ${height}px;
|
|
60
|
+
opacity: 0;
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
z-index: -9999;
|
|
63
|
+
`;
|
|
64
|
+
if (!captureCanvas.hasAttribute("layoutsubtree")) {
|
|
65
|
+
captureCanvas.setAttribute("layoutsubtree", "");
|
|
66
|
+
captureCanvas.layoutSubtree = true;
|
|
67
|
+
}
|
|
68
|
+
if (!captureCanvas.parentNode) document.body.appendChild(captureCanvas);
|
|
69
|
+
if (container.parentElement !== captureCanvas) captureCanvas.appendChild(container);
|
|
70
|
+
if (getComputedStyle(container).display === "none") container.style.display = "block";
|
|
71
|
+
if (!_layoutInitializedCanvases.has(captureCanvas)) {
|
|
72
|
+
captureCanvas.offsetHeight;
|
|
73
|
+
container.offsetHeight;
|
|
74
|
+
getComputedStyle(captureCanvas).opacity;
|
|
75
|
+
getComputedStyle(container).opacity;
|
|
76
|
+
_layoutInitializedCanvases.add(captureCanvas);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
captureCanvas = document.createElement("canvas");
|
|
80
|
+
captureCanvas.width = Math.floor(width * dpr);
|
|
81
|
+
captureCanvas.height = Math.floor(height * dpr);
|
|
82
|
+
captureCanvas.setAttribute("layoutsubtree", "");
|
|
83
|
+
captureCanvas.layoutSubtree = true;
|
|
84
|
+
captureCanvas.appendChild(container);
|
|
85
|
+
captureCanvas.style.cssText = `
|
|
86
|
+
position: fixed;
|
|
87
|
+
left: 0;
|
|
88
|
+
top: 0;
|
|
89
|
+
width: ${width}px;
|
|
90
|
+
height: ${height}px;
|
|
91
|
+
opacity: 0;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
z-index: -9999;
|
|
94
|
+
`;
|
|
95
|
+
document.body.appendChild(captureCanvas);
|
|
96
|
+
shouldCleanup = true;
|
|
97
|
+
}
|
|
98
|
+
const t1 = performance.now();
|
|
99
|
+
defaultProfiler.addTime("setup", t1 - t0);
|
|
100
|
+
try {
|
|
101
|
+
getComputedStyle(container).opacity;
|
|
102
|
+
if (reuseCanvas && captureCanvas.layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {
|
|
103
|
+
await waitForFrame();
|
|
104
|
+
_layoutInitializedCanvases.add(captureCanvas);
|
|
105
|
+
if (!captureCanvas.parentNode) return captureCanvas;
|
|
106
|
+
}
|
|
107
|
+
captureCanvas.getContext("2d").drawElementImage(container, 0, 0);
|
|
108
|
+
} finally {
|
|
109
|
+
if (shouldCleanup && captureCanvas.parentNode) captureCanvas.parentNode.removeChild(captureCanvas);
|
|
110
|
+
}
|
|
111
|
+
const t2 = performance.now();
|
|
112
|
+
defaultProfiler.addTime("draw", t2 - t1);
|
|
113
|
+
if (dpr === 1) {
|
|
114
|
+
defaultProfiler.incrementRenderCount();
|
|
115
|
+
return captureCanvas;
|
|
116
|
+
}
|
|
117
|
+
const outputCanvas = document.createElement("canvas");
|
|
118
|
+
outputCanvas.width = width;
|
|
119
|
+
outputCanvas.height = height;
|
|
120
|
+
outputCanvas.getContext("2d").drawImage(captureCanvas, 0, 0, captureCanvas.width, captureCanvas.height, 0, 0, width, height);
|
|
121
|
+
const t3 = performance.now();
|
|
122
|
+
defaultProfiler.addTime("downsample", t3 - t2);
|
|
123
|
+
defaultProfiler.incrementRenderCount();
|
|
124
|
+
return outputCanvas;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
export { createDprCanvas, renderToImageNative };
|
|
129
|
+
//# sourceMappingURL=renderToImageNative.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type { HtmlInCanvasContext, HtmlInCanvasElement, NativeRenderOptions } from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n \n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n \n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n * \n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n * \n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n \n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n \n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n \n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n \n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n \n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n \n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n \n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n \n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n \n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === 'none') {\n container.style.display = 'block';\n }\n \n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n \n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n \n captureCanvas.appendChild(container);\n \n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n \n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n \n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n \n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (reuseCanvas && (captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n \n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n \n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n \n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n \n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n \n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0, 0, captureCanvas.width, captureCanvas.height, // source (full DPR capture)\n 0, 0, width, height // destination (logical pixels)\n );\n \n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n \n return outputCanvas;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/preview/rendering/svgSerializer.ts
|
|
2
|
+
/**
|
|
3
|
+
* SVG serialization and base64 encoding utilities.
|
|
4
|
+
*/
|
|
5
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
6
|
+
/**
|
|
7
|
+
* Fast base64 encoding directly from Uint8Array.
|
|
8
|
+
* Optimized with array buffer pre-allocation and batch string building.
|
|
9
|
+
* ~2-3x faster than string concatenation approach.
|
|
10
|
+
*/
|
|
11
|
+
function encodeBase64Fast(bytes) {
|
|
12
|
+
const len = bytes.length;
|
|
13
|
+
const outputLen = (len + 2) / 3 << 2;
|
|
14
|
+
const result = new Array(outputLen);
|
|
15
|
+
let i = 0;
|
|
16
|
+
let outIndex = 0;
|
|
17
|
+
const len3 = len - 2;
|
|
18
|
+
while (i < len3) {
|
|
19
|
+
const byte1 = bytes[i++];
|
|
20
|
+
const byte2 = bytes[i++];
|
|
21
|
+
const byte3 = bytes[i++];
|
|
22
|
+
const bitmap = byte1 << 16 | byte2 << 8 | byte3;
|
|
23
|
+
result[outIndex++] = BASE64_CHARS[bitmap >> 18 & 63];
|
|
24
|
+
result[outIndex++] = BASE64_CHARS[bitmap >> 12 & 63];
|
|
25
|
+
result[outIndex++] = BASE64_CHARS[bitmap >> 6 & 63];
|
|
26
|
+
result[outIndex++] = BASE64_CHARS[bitmap & 63];
|
|
27
|
+
}
|
|
28
|
+
const remaining = len - i;
|
|
29
|
+
if (remaining > 0) {
|
|
30
|
+
const byte1 = bytes[i++];
|
|
31
|
+
const byte2 = remaining > 1 ? bytes[i++] : 0;
|
|
32
|
+
const bitmap = byte1 << 16 | byte2 << 8;
|
|
33
|
+
result[outIndex++] = BASE64_CHARS[bitmap >> 18 & 63];
|
|
34
|
+
result[outIndex++] = BASE64_CHARS[bitmap >> 12 & 63];
|
|
35
|
+
result[outIndex++] = remaining > 1 ? BASE64_CHARS[bitmap >> 6 & 63] : "=";
|
|
36
|
+
result[outIndex++] = "=";
|
|
37
|
+
}
|
|
38
|
+
return result.join("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { encodeBase64Fast };
|
|
43
|
+
//# sourceMappingURL=svgSerializer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svgSerializer.js","names":[],"sources":["../../../src/preview/rendering/svgSerializer.ts"],"sourcesContent":["/**\n * SVG serialization and base64 encoding utilities.\n */\n\n// Pre-computed base64 lookup table as Uint8Array for faster indexing\nconst BASE64_CHARS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/**\n * Fast base64 encoding directly from Uint8Array.\n * Optimized with array buffer pre-allocation and batch string building.\n * ~2-3x faster than string concatenation approach.\n */\nexport function encodeBase64Fast(bytes: Uint8Array): string {\n const len = bytes.length;\n \n // Pre-calculate output size: 4 chars per 3 bytes, rounded up\n const outputLen = ((len + 2) / 3) << 2;\n const result = new Array(outputLen);\n \n let i = 0;\n let outIndex = 0;\n \n // Process 3 bytes at a time (produces 4 base64 chars)\n // Unrolled for better performance\n const len3 = len - 2;\n while (i < len3) {\n const byte1 = bytes[i++]!;\n const byte2 = bytes[i++]!;\n const byte3 = bytes[i++]!;\n \n const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;\n \n result[outIndex++] = BASE64_CHARS[(bitmap >> 18) & 63];\n result[outIndex++] = BASE64_CHARS[(bitmap >> 12) & 63];\n result[outIndex++] = BASE64_CHARS[(bitmap >> 6) & 63];\n result[outIndex++] = BASE64_CHARS[bitmap & 63];\n }\n \n // Handle remaining bytes (1 or 2)\n const remaining = len - i;\n if (remaining > 0) {\n const byte1 = bytes[i++]!;\n const byte2 = remaining > 1 ? bytes[i++]! : 0;\n const bitmap = (byte1 << 16) | (byte2 << 8);\n \n result[outIndex++] = BASE64_CHARS[(bitmap >> 18) & 63];\n result[outIndex++] = BASE64_CHARS[(bitmap >> 12) & 63];\n result[outIndex++] = remaining > 1 ? BASE64_CHARS[(bitmap >> 6) & 63] : \"=\";\n result[outIndex++] = \"=\";\n }\n \n return result.join(\"\");\n}\n"],"mappings":";;;;AAKA,MAAM,eAAe;;;;;;AAOrB,SAAgB,iBAAiB,OAA2B;CAC1D,MAAM,MAAM,MAAM;CAGlB,MAAM,aAAc,MAAM,KAAK,KAAM;CACrC,MAAM,SAAS,IAAI,MAAM,UAAU;CAEnC,IAAI,IAAI;CACR,IAAI,WAAW;CAIf,MAAM,OAAO,MAAM;AACnB,QAAO,IAAI,MAAM;EACf,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,MAAM;EAEpB,MAAM,SAAU,SAAS,KAAO,SAAS,IAAK;AAE9C,SAAO,cAAc,aAAc,UAAU,KAAM;AACnD,SAAO,cAAc,aAAc,UAAU,KAAM;AACnD,SAAO,cAAc,aAAc,UAAU,IAAK;AAClD,SAAO,cAAc,aAAa,SAAS;;CAI7C,MAAM,YAAY,MAAM;AACxB,KAAI,YAAY,GAAG;EACjB,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,YAAY,IAAI,MAAM,OAAQ;EAC5C,MAAM,SAAU,SAAS,KAAO,SAAS;AAEzC,SAAO,cAAc,aAAc,UAAU,KAAM;AACnD,SAAO,cAAc,aAAc,UAAU,KAAM;AACnD,SAAO,cAAc,YAAY,IAAI,aAAc,UAAU,IAAK,MAAM;AACxE,SAAO,cAAc;;AAGvB,QAAO,OAAO,KAAK,GAAG"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
|
|
1
3
|
//#region src/preview/statsTrackingStrategy.ts
|
|
2
4
|
/**
|
|
3
5
|
* Canvas mode stats tracking strategy.
|
|
@@ -34,7 +36,7 @@ var CanvasStatsStrategy = class {
|
|
|
34
36
|
this.updateStats(renderWidth, renderHeight, currentScale);
|
|
35
37
|
}
|
|
36
38
|
} catch (e) {
|
|
37
|
-
|
|
39
|
+
logger.error("Canvas stats tracking failed:", e);
|
|
38
40
|
}
|
|
39
41
|
this.animationFrame = requestAnimationFrame(loop);
|
|
40
42
|
};
|