@zseven-w/dsh-openpencil-linux-arm64 0.1.0-rc.4
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/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +72 -0
- package/bin/op-host-web-server +0 -0
- package/openpencil-runtime.json +211 -0
- package/package.json +34 -0
- package/web/canvaskit/LICENSE +29 -0
- package/web/canvaskit/README.md +21 -0
- package/web/canvaskit/canvaskit.js +288 -0
- package/web/canvaskit/canvaskit.wasm +0 -0
- package/web/pkg/assets/iconify-catalog-core.json +1 -0
- package/web/pkg/assets/prompt_center_previews/component-data-grid.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/component-form-lab.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/dashboard-pulse.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/dashboard-sentinel.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-calendar.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-calm.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-daily-app.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-now-playing.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-weather.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-aloft.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-bloom.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-companion.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-daybreak.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-ember.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-forage.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-gallery.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-hearth.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-lingua.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-marginalia.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-marquee.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-meteo.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-nightcap.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-nocturne.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-relic.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-ritual.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-still.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-verdant.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-volt.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-wander.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-aloft.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-bloom.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-companion.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-daybreak.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-ember.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-forage.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-gallery.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-hearth.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-lingua.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-marginalia.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-marquee.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-meteo.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-nightcap.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-nocturne.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-relic.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-ritual.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-still.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-verdant.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-volt.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-wander.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/modify-complete-states.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/modify-polish-current.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-barbershop.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-coffee-shop.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-dashboard.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-travel-app.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-atelier.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-kilnform.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-orbit.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-reefwright.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/app-onboarding-triptych.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/banxin-rule-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/before-after.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/bookreview-silk-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/brand-concept-sheet.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/challenge-tracker-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/cityguide-film-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/concept-contrast-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/daily-sign-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/data-report-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/data-story-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/datareport-grid-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/diy-blueprint-guide.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/do-dont-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/dossier-linen-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/ecosystem-map-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/event-poster-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/faq-thread-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/gradient-tech.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/gridpaper-graphite-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/journal-checklist-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/knowledge-card-square.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/knowledge-card-vertical.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/knowledge-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/lecture-deck-light.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/ledger-tick-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/logo-qa-board.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/metric-single-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/milestone-timeline-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/minimal-keynote.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/myth-truth-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/notice-board-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/opinion-longform-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/photo-composition-tutorial.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/pitch-deck-dark.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/pitfall-list-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/price-tier-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/pricing-tiers-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/product-landing-light.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/punch-quote-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/qa-chalkboard-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/quote-frame-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/ranking-board-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/recipe-four-step.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/saas-landing-orange.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/scenario-guide-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/screenshot-tutorial.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/skincare-routine-cards.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/slide-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/software-step-tutorial.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/sounding-navy-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/spec-table-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/spine-culture-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/steps-flow-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/storage-makeover-steps.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/story-night-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/three-way-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/tidemark-slate-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/time-shift-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/toolkit-notebook-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/tradeoff-scale-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/tutorial-journal-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/version-diff-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/weekly-report-lesson.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/workout-breakdown-guide.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/yearreview-mineral-carousel.jpg +0 -0
- package/web/pkg/assets/scene_templates/app-onboarding-triptych.op +1087 -0
- package/web/pkg/assets/scene_templates/banxin-rule-deck.op +2702 -0
- package/web/pkg/assets/scene_templates/before-after.op +753 -0
- package/web/pkg/assets/scene_templates/bookreview-silk-carousel.op +3272 -0
- package/web/pkg/assets/scene_templates/brand-concept-sheet.op +1 -0
- package/web/pkg/assets/scene_templates/challenge-tracker-infographic.op +2448 -0
- package/web/pkg/assets/scene_templates/cityguide-film-carousel.op +13691 -0
- package/web/pkg/assets/scene_templates/concept-contrast-infographic.op +1433 -0
- package/web/pkg/assets/scene_templates/daily-sign-card.op +370 -0
- package/web/pkg/assets/scene_templates/data-report-infographic.op +1946 -0
- package/web/pkg/assets/scene_templates/data-story-infographic.op +1916 -0
- package/web/pkg/assets/scene_templates/datareport-grid-carousel.op +2799 -0
- package/web/pkg/assets/scene_templates/diy-blueprint-guide.op +2744 -0
- package/web/pkg/assets/scene_templates/do-dont-comparison.op +1170 -0
- package/web/pkg/assets/scene_templates/dossier-linen-deck.op +7045 -0
- package/web/pkg/assets/scene_templates/ecosystem-map-infographic.op +2015 -0
- package/web/pkg/assets/scene_templates/event-poster-deck.op +2756 -0
- package/web/pkg/assets/scene_templates/faq-thread-infographic.op +1698 -0
- package/web/pkg/assets/scene_templates/gradient-tech.op +3322 -0
- package/web/pkg/assets/scene_templates/gridpaper-graphite-deck.op +11996 -0
- package/web/pkg/assets/scene_templates/journal-checklist-card.op +764 -0
- package/web/pkg/assets/scene_templates/knowledge-card-square.op +560 -0
- package/web/pkg/assets/scene_templates/knowledge-card-vertical.op +719 -0
- package/web/pkg/assets/scene_templates/knowledge-carousel.op +1640 -0
- package/web/pkg/assets/scene_templates/lecture-deck-light.op +5211 -0
- package/web/pkg/assets/scene_templates/ledger-tick-deck.op +6756 -0
- package/web/pkg/assets/scene_templates/logo-qa-board.op +1 -0
- package/web/pkg/assets/scene_templates/metric-single-card.op +259 -0
- package/web/pkg/assets/scene_templates/milestone-timeline-infographic.op +2116 -0
- package/web/pkg/assets/scene_templates/minimal-keynote.op +1085 -0
- package/web/pkg/assets/scene_templates/myth-truth-comparison.op +1315 -0
- package/web/pkg/assets/scene_templates/notice-board-card.op +671 -0
- package/web/pkg/assets/scene_templates/opinion-longform-carousel.op +2492 -0
- package/web/pkg/assets/scene_templates/photo-composition-tutorial.op +2558 -0
- package/web/pkg/assets/scene_templates/pitch-deck-dark.op +3020 -0
- package/web/pkg/assets/scene_templates/pitfall-list-infographic.op +1646 -0
- package/web/pkg/assets/scene_templates/price-tier-card.op +647 -0
- package/web/pkg/assets/scene_templates/pricing-tiers-comparison.op +1762 -0
- package/web/pkg/assets/scene_templates/product-landing-light.op +10717 -0
- package/web/pkg/assets/scene_templates/punch-quote-card.op +438 -0
- package/web/pkg/assets/scene_templates/qa-chalkboard-carousel.op +2568 -0
- package/web/pkg/assets/scene_templates/quote-frame-card.op +1233 -0
- package/web/pkg/assets/scene_templates/ranking-board-infographic.op +1945 -0
- package/web/pkg/assets/scene_templates/recipe-four-step.op +1177 -0
- package/web/pkg/assets/scene_templates/saas-landing-orange.op +6616 -0
- package/web/pkg/assets/scene_templates/scenario-guide-comparison.op +1721 -0
- package/web/pkg/assets/scene_templates/screenshot-tutorial.op +1 -0
- package/web/pkg/assets/scene_templates/skincare-routine-cards.op +3240 -0
- package/web/pkg/assets/scene_templates/slide-deck.op +2475 -0
- package/web/pkg/assets/scene_templates/software-step-tutorial.op +797 -0
- package/web/pkg/assets/scene_templates/sounding-navy-deck.op +4292 -0
- package/web/pkg/assets/scene_templates/spec-table-comparison.op +2033 -0
- package/web/pkg/assets/scene_templates/spine-culture-card.op +647 -0
- package/web/pkg/assets/scene_templates/steps-flow-infographic.op +1664 -0
- package/web/pkg/assets/scene_templates/storage-makeover-steps.op +2912 -0
- package/web/pkg/assets/scene_templates/story-night-carousel.op +2814 -0
- package/web/pkg/assets/scene_templates/three-way-comparison.op +1917 -0
- package/web/pkg/assets/scene_templates/tidemark-slate-deck.op +5716 -0
- package/web/pkg/assets/scene_templates/time-shift-comparison.op +1127 -0
- package/web/pkg/assets/scene_templates/toolkit-notebook-carousel.op +5335 -0
- package/web/pkg/assets/scene_templates/tradeoff-scale-comparison.op +995 -0
- package/web/pkg/assets/scene_templates/tutorial-journal-carousel.op +4425 -0
- package/web/pkg/assets/scene_templates/version-diff-comparison.op +1001 -0
- package/web/pkg/assets/scene_templates/weekly-report-lesson.op +2188 -0
- package/web/pkg/assets/scene_templates/workout-breakdown-guide.op +2579 -0
- package/web/pkg/assets/scene_templates/yearreview-mineral-carousel.op +4367 -0
- package/web/pkg/op_host_web.js +1971 -0
- package/web/pkg/op_host_web_bg.wasm +0 -0
- package/web/pkg/snippets/op-host-web-f712ff5933a5d456/src/figma_temp_worker.js +494 -0
- package/web/pkg/snippets/op-host-web-f712ff5933a5d456/src/op_ck_bridge.js +1712 -0
- package/web/pkg/snippets/op-host-web-f712ff5933a5d456/src/op_ck_image_cache.js +404 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// CanvasKit image residency and thumbnail drawing.
|
|
2
|
+
//
|
|
3
|
+
// Full document images and blur-up thumbnails intentionally use independent
|
|
4
|
+
// LRU budgets. A large document therefore cannot evict every thumbnail, and
|
|
5
|
+
// a thumbnail-heavy document cannot consume the full-resolution raster budget.
|
|
6
|
+
|
|
7
|
+
const FULL_IMAGE_CACHE_CAP = 4096;
|
|
8
|
+
const FULL_IMAGE_CACHE_BYTE_BUDGET = 384 * 1024 * 1024;
|
|
9
|
+
const THUMBNAIL_CACHE_CAP = 1024;
|
|
10
|
+
const THUMBNAIL_CACHE_BYTE_BUDGET = 4 * 1024 * 1024;
|
|
11
|
+
const THUMBNAIL_ENCODED_BYTE_LIMIT = 4 * 1024;
|
|
12
|
+
const THUMBNAIL_MAX_EDGE = 32;
|
|
13
|
+
const THUMBNAIL_FAILURE_CAP = 1024;
|
|
14
|
+
const JPEG_SOF_MARKERS = new Set([
|
|
15
|
+
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7,
|
|
16
|
+
0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// Read JPEG SOF metadata before CanvasKit expands pixels. The persisted table
|
|
20
|
+
// is untrusted input: a very compressible, multi-megapixel JPEG can fit under
|
|
21
|
+
// the 4 KiB encoded limit but is not safe to decode synchronously in paint.
|
|
22
|
+
const jpegFitsThumbnailBounds = (jpeg) => {
|
|
23
|
+
if (!jpeg || jpeg.byteLength < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
let offset = 2;
|
|
27
|
+
while (offset < jpeg.byteLength) {
|
|
28
|
+
while (offset < jpeg.byteLength && jpeg[offset] === 0xff) offset += 1;
|
|
29
|
+
if (offset >= jpeg.byteLength) return false;
|
|
30
|
+
const marker = jpeg[offset++];
|
|
31
|
+
if (marker === 0xd9 || marker === 0xda) return false;
|
|
32
|
+
if (marker === 0x01 || marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (offset + 1 >= jpeg.byteLength) return false;
|
|
36
|
+
const length = (jpeg[offset] << 8) | jpeg[offset + 1];
|
|
37
|
+
if (length < 2 || offset + length > jpeg.byteLength) return false;
|
|
38
|
+
if (JPEG_SOF_MARKERS.has(marker)) {
|
|
39
|
+
if (length < 7) return false;
|
|
40
|
+
const height = (jpeg[offset + 3] << 8) | jpeg[offset + 4];
|
|
41
|
+
const width = (jpeg[offset + 5] << 8) | jpeg[offset + 6];
|
|
42
|
+
return (
|
|
43
|
+
width > 0 &&
|
|
44
|
+
height > 0 &&
|
|
45
|
+
width <= THUMBNAIL_MAX_EDGE &&
|
|
46
|
+
height <= THUMBNAIL_MAX_EDGE
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
offset += length;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const imageKey = (lo, hi) => String(hi >>> 0) + ':' + String(lo >>> 0);
|
|
55
|
+
|
|
56
|
+
const copyBytes = (bytes) =>
|
|
57
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
58
|
+
|
|
59
|
+
const decodedRasterBytes = (image) => {
|
|
60
|
+
const width = image.width();
|
|
61
|
+
const height = image.height();
|
|
62
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
return width * height * 4;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const deleteImage = (image) => {
|
|
69
|
+
if (image && image.delete) image.delete();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// SVG has no magic bytes — sniff the markup (first 4 KiB decode as text and
|
|
73
|
+
// contain an `<svg` tag). Mirrors the native seam's sniff in
|
|
74
|
+
// `op-editor-ui/.../svg_raster.rs`.
|
|
75
|
+
const sniffsAsSvg = (bytes) => {
|
|
76
|
+
try {
|
|
77
|
+
const head = new TextDecoder().decode(
|
|
78
|
+
bytes.subarray(0, Math.min(bytes.byteLength, 4096)),
|
|
79
|
+
);
|
|
80
|
+
const trimmed = head.replace(/^/, '').trimStart();
|
|
81
|
+
return trimmed.startsWith('<') && trimmed.includes('<svg');
|
|
82
|
+
} catch (_error) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const SVG_RASTER_MAX_EDGE = 2048;
|
|
88
|
+
const SVG_RASTER_SCALE = 2;
|
|
89
|
+
const SVG_FAILURE_CAP = 1024;
|
|
90
|
+
|
|
91
|
+
export function createWebImageCaches(CK) {
|
|
92
|
+
const fullImageCache = new Map();
|
|
93
|
+
let fullImageCacheBytes = 0;
|
|
94
|
+
const thumbnailCache = new Map();
|
|
95
|
+
let thumbnailCacheBytes = 0;
|
|
96
|
+
const thumbnailFailures = new Set();
|
|
97
|
+
// SVG rasterizations in flight / permanently failed. Pending keys report
|
|
98
|
+
// decode success to the Rust side so the id is not negative-cached; paint
|
|
99
|
+
// keeps re-requesting until the browser's async decode lands the raster
|
|
100
|
+
// in `fullImageCache`.
|
|
101
|
+
const svgPending = new Set();
|
|
102
|
+
const svgFailures = new Set();
|
|
103
|
+
|
|
104
|
+
const rememberSvgFailure = (key) => {
|
|
105
|
+
if (svgFailures.size >= SVG_FAILURE_CAP) {
|
|
106
|
+
svgFailures.delete(svgFailures.values().next().value);
|
|
107
|
+
}
|
|
108
|
+
svgFailures.add(key);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const installRasterizedSvg = (key, img) => {
|
|
112
|
+
const sourceW = img.naturalWidth || img.width;
|
|
113
|
+
const sourceH = img.naturalHeight || img.height;
|
|
114
|
+
if (!(sourceW > 0) || !(sourceH > 0)) return false;
|
|
115
|
+
const scale = Math.min(
|
|
116
|
+
SVG_RASTER_SCALE,
|
|
117
|
+
SVG_RASTER_MAX_EDGE / Math.max(sourceW, sourceH),
|
|
118
|
+
);
|
|
119
|
+
if (!(scale > 0)) return false;
|
|
120
|
+
const width = Math.max(1, Math.round(sourceW * scale));
|
|
121
|
+
const height = Math.max(1, Math.round(sourceH * scale));
|
|
122
|
+
const surface = document.createElement('canvas');
|
|
123
|
+
surface.width = width;
|
|
124
|
+
surface.height = height;
|
|
125
|
+
const context = surface.getContext('2d');
|
|
126
|
+
if (!context) return false;
|
|
127
|
+
context.drawImage(img, 0, 0, width, height);
|
|
128
|
+
const pixels = context.getImageData(0, 0, width, height);
|
|
129
|
+
if (!CK.MakeImage) return false;
|
|
130
|
+
const image = CK.MakeImage(
|
|
131
|
+
{
|
|
132
|
+
width,
|
|
133
|
+
height,
|
|
134
|
+
colorType: CK.ColorType.RGBA_8888,
|
|
135
|
+
alphaType: CK.AlphaType.Unpremul,
|
|
136
|
+
colorSpace: CK.ColorSpace.SRGB,
|
|
137
|
+
},
|
|
138
|
+
pixels.data,
|
|
139
|
+
4 * width,
|
|
140
|
+
);
|
|
141
|
+
if (!image) return false;
|
|
142
|
+
const bytes = decodedRasterBytes(image);
|
|
143
|
+
if (!(bytes > 0)) {
|
|
144
|
+
deleteImage(image);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
fullImageCache.set(key, {
|
|
148
|
+
image,
|
|
149
|
+
bytes,
|
|
150
|
+
coversEdgePx: Number.MAX_SAFE_INTEGER,
|
|
151
|
+
});
|
|
152
|
+
fullImageCacheBytes += bytes;
|
|
153
|
+
evictFullImages();
|
|
154
|
+
return fullImageCache.has(key);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// The browser is the SVG decoder CanvasKit lacks: load the markup into an
|
|
158
|
+
// `<img>` (async), draw it onto a 2d canvas, and install the pixels as an
|
|
159
|
+
// ordinary CanvasKit image. resvg does this on native; carrying it into
|
|
160
|
+
// the wasm bundle would bust the 6 MiB ceiling for a codec the platform
|
|
161
|
+
// already ships.
|
|
162
|
+
const startSvgRaster = (key, encoded) => {
|
|
163
|
+
if (svgPending.has(key)) return;
|
|
164
|
+
svgPending.add(key);
|
|
165
|
+
let url = null;
|
|
166
|
+
const settle = (installed) => {
|
|
167
|
+
if (url) URL.revokeObjectURL(url);
|
|
168
|
+
svgPending.delete(key);
|
|
169
|
+
if (!installed) rememberSvgFailure(key);
|
|
170
|
+
};
|
|
171
|
+
try {
|
|
172
|
+
const blob = new Blob([copyBytes(encoded)], { type: 'image/svg+xml' });
|
|
173
|
+
url = URL.createObjectURL(blob);
|
|
174
|
+
const img = new Image();
|
|
175
|
+
img.onload = () => {
|
|
176
|
+
try {
|
|
177
|
+
settle(installRasterizedSvg(key, img));
|
|
178
|
+
} catch (_error) {
|
|
179
|
+
settle(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
img.onerror = () => settle(false);
|
|
183
|
+
img.src = url;
|
|
184
|
+
} catch (_error) {
|
|
185
|
+
settle(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const evictFullImages = () => {
|
|
190
|
+
while (
|
|
191
|
+
fullImageCache.size > FULL_IMAGE_CACHE_CAP ||
|
|
192
|
+
fullImageCacheBytes > FULL_IMAGE_CACHE_BYTE_BUDGET
|
|
193
|
+
) {
|
|
194
|
+
const oldestKey = fullImageCache.keys().next().value;
|
|
195
|
+
const oldest = fullImageCache.get(oldestKey);
|
|
196
|
+
if (!oldest) break;
|
|
197
|
+
fullImageCache.delete(oldestKey);
|
|
198
|
+
fullImageCacheBytes -= oldest.bytes;
|
|
199
|
+
deleteImage(oldest.image);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const evictThumbnails = () => {
|
|
204
|
+
while (
|
|
205
|
+
thumbnailCache.size > THUMBNAIL_CACHE_CAP ||
|
|
206
|
+
thumbnailCacheBytes > THUMBNAIL_CACHE_BYTE_BUDGET
|
|
207
|
+
) {
|
|
208
|
+
const oldestKey = thumbnailCache.keys().next().value;
|
|
209
|
+
const oldest = thumbnailCache.get(oldestKey);
|
|
210
|
+
if (!oldest) break;
|
|
211
|
+
thumbnailCache.delete(oldestKey);
|
|
212
|
+
thumbnailCacheBytes -= oldest.bytes;
|
|
213
|
+
deleteImage(oldest.image);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const rememberThumbnailFailure = (key) => {
|
|
218
|
+
if (thumbnailFailures.has(key)) return;
|
|
219
|
+
if (thumbnailFailures.size >= THUMBNAIL_FAILURE_CAP) {
|
|
220
|
+
thumbnailFailures.delete(thumbnailFailures.values().next().value);
|
|
221
|
+
}
|
|
222
|
+
thumbnailFailures.add(key);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// A cached raster satisfies a draw only when it is at least as sharp
|
|
226
|
+
// as the view needs. Rastering every visible image at its authored
|
|
227
|
+
// size is what made a zoomed-out image-dense page evict and re-decode
|
|
228
|
+
// continuously; sizing to the view keeps hundreds of thumbnails cheap.
|
|
229
|
+
const hasFullImage = (lo, hi, maxEdgePx) => {
|
|
230
|
+
const hit = fullImageCache.get(imageKey(lo, hi));
|
|
231
|
+
if (!hit) return false;
|
|
232
|
+
if (!(maxEdgePx > 0)) return true;
|
|
233
|
+
return hit.coversEdgePx >= maxEdgePx;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const fullImage = (lo, hi) => {
|
|
237
|
+
const key = imageKey(lo, hi);
|
|
238
|
+
const hit = fullImageCache.get(key);
|
|
239
|
+
if (!hit) return null;
|
|
240
|
+
fullImageCache.delete(key);
|
|
241
|
+
fullImageCache.set(key, hit);
|
|
242
|
+
return hit.image;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// NOTE: the browser decodes at the source's own size. Sizing the
|
|
246
|
+
// raster to the view (as the native host does) needs a surface
|
|
247
|
+
// round-trip whose snapshot aliases the surface's pixels, so freeing
|
|
248
|
+
// the surface would dangle the cached image — deferred rather than
|
|
249
|
+
// shipped unsafely. A full raster serves every requested size, so it
|
|
250
|
+
// records itself as covering any edge.
|
|
251
|
+
const installFullImage = (lo, hi, encoded, _maxEdgePx) => {
|
|
252
|
+
const key = imageKey(lo, hi);
|
|
253
|
+
if (fullImageCache.has(key)) return true;
|
|
254
|
+
|
|
255
|
+
let image = null;
|
|
256
|
+
try {
|
|
257
|
+
image = CK.MakeImageFromEncoded(copyBytes(encoded));
|
|
258
|
+
if (!image) {
|
|
259
|
+
// CanvasKit has no SVG codec — hand the markup to the browser's own
|
|
260
|
+
// decoder. Reporting `true` while the async raster is in flight
|
|
261
|
+
// keeps the id off the Rust side's permanent-failure cache; paint
|
|
262
|
+
// re-requests each frame until the raster lands (or the failure is
|
|
263
|
+
// remembered here and this returns false for good).
|
|
264
|
+
if (svgFailures.has(key)) return false;
|
|
265
|
+
if (sniffsAsSvg(encoded)) {
|
|
266
|
+
startSvgRaster(key, encoded);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const bytes = decodedRasterBytes(image);
|
|
272
|
+
if (!(bytes > 0)) {
|
|
273
|
+
deleteImage(image);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
fullImageCache.set(key, {
|
|
277
|
+
image,
|
|
278
|
+
bytes,
|
|
279
|
+
coversEdgePx: Number.MAX_SAFE_INTEGER,
|
|
280
|
+
});
|
|
281
|
+
fullImageCacheBytes += bytes;
|
|
282
|
+
evictFullImages();
|
|
283
|
+
return fullImageCache.has(key);
|
|
284
|
+
} catch (_error) {
|
|
285
|
+
deleteImage(image);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const thumbnailImage = (lo, hi, jpeg) => {
|
|
291
|
+
const key = imageKey(lo, hi);
|
|
292
|
+
const hit = thumbnailCache.get(key);
|
|
293
|
+
if (hit) {
|
|
294
|
+
thumbnailCache.delete(key);
|
|
295
|
+
thumbnailCache.set(key, hit);
|
|
296
|
+
return hit.image;
|
|
297
|
+
}
|
|
298
|
+
if (thumbnailFailures.has(key)) return null;
|
|
299
|
+
if (!jpeg || jpeg.byteLength > THUMBNAIL_ENCODED_BYTE_LIMIT) {
|
|
300
|
+
rememberThumbnailFailure(key);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (!jpegFitsThumbnailBounds(jpeg)) {
|
|
304
|
+
rememberThumbnailFailure(key);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let image = null;
|
|
309
|
+
try {
|
|
310
|
+
image = CK.MakeImageFromEncoded(copyBytes(jpeg));
|
|
311
|
+
if (!image) {
|
|
312
|
+
rememberThumbnailFailure(key);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const bytes = decodedRasterBytes(image);
|
|
316
|
+
if (!(bytes > 0) || bytes > THUMBNAIL_CACHE_BYTE_BUDGET) {
|
|
317
|
+
deleteImage(image);
|
|
318
|
+
rememberThumbnailFailure(key);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
thumbnailCache.set(key, { image, bytes });
|
|
322
|
+
thumbnailCacheBytes += bytes;
|
|
323
|
+
evictThumbnails();
|
|
324
|
+
return image;
|
|
325
|
+
} catch (_error) {
|
|
326
|
+
deleteImage(image);
|
|
327
|
+
rememberThumbnailFailure(key);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const discardThumbnail = (key) => {
|
|
333
|
+
const entry = thumbnailCache.get(key);
|
|
334
|
+
if (!entry) return;
|
|
335
|
+
thumbnailCache.delete(key);
|
|
336
|
+
thumbnailCacheBytes -= entry.bytes;
|
|
337
|
+
deleteImage(entry.image);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const drawThumbnailCover = (canvas, lo, hi, jpeg, x, y, w, h) => {
|
|
341
|
+
if (!(w > 0) || !(h > 0)) return false;
|
|
342
|
+
const image = thumbnailImage(lo, hi, jpeg);
|
|
343
|
+
if (!image) return false;
|
|
344
|
+
|
|
345
|
+
const imageW = image.width();
|
|
346
|
+
const imageH = image.height();
|
|
347
|
+
if (!(imageW > 0) || !(imageH > 0)) return false;
|
|
348
|
+
const scale = Math.max(w / imageW, h / imageH);
|
|
349
|
+
const drawW = imageW * scale;
|
|
350
|
+
const drawH = imageH * scale;
|
|
351
|
+
const src = CK.LTRBRect(0, 0, imageW, imageH);
|
|
352
|
+
const dst = CK.LTRBRect(
|
|
353
|
+
x + (w - drawW) / 2,
|
|
354
|
+
y + (h - drawH) / 2,
|
|
355
|
+
x + (w + drawW) / 2,
|
|
356
|
+
y + (h + drawH) / 2,
|
|
357
|
+
);
|
|
358
|
+
const clip = CK.LTRBRect(x, y, x + w, y + h);
|
|
359
|
+
const paint = new CK.Paint();
|
|
360
|
+
paint.setAntiAlias(true);
|
|
361
|
+
|
|
362
|
+
let saved = false;
|
|
363
|
+
try {
|
|
364
|
+
canvas.save();
|
|
365
|
+
saved = true;
|
|
366
|
+
canvas.clipRect(clip, CK.ClipOp.Intersect, true);
|
|
367
|
+
if (canvas.drawImageRectOptions) {
|
|
368
|
+
canvas.drawImageRectOptions(
|
|
369
|
+
image,
|
|
370
|
+
src,
|
|
371
|
+
dst,
|
|
372
|
+
CK.FilterMode.Linear,
|
|
373
|
+
CK.MipmapMode.None,
|
|
374
|
+
paint,
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
canvas.drawImageRect(image, src, dst, paint, false);
|
|
378
|
+
}
|
|
379
|
+
} catch (_error) {
|
|
380
|
+
const key = imageKey(lo, hi);
|
|
381
|
+
discardThumbnail(key);
|
|
382
|
+
rememberThumbnailFailure(key);
|
|
383
|
+
return false;
|
|
384
|
+
} finally {
|
|
385
|
+
if (saved) canvas.restore();
|
|
386
|
+
paint.delete();
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
hasFullImage,
|
|
393
|
+
fullImage,
|
|
394
|
+
installFullImage,
|
|
395
|
+
drawThumbnailCover,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// wasm-bindgen only copies local modules named by Rust attributes; it does not
|
|
400
|
+
// recursively package relative imports from another local module. Rust obtains
|
|
401
|
+
// this function object and passes it into the bridge during initialization.
|
|
402
|
+
export function webImageCacheFactory() {
|
|
403
|
+
return createWebImageCaches;
|
|
404
|
+
}
|