@jxsuite/studio 10.2.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{2020-bx702wqa.js → 2020-906vx2dq.js} +3 -3
- package/dist/chunks/adapter-hnvad61x.js +4 -0
- package/dist/chunks/add-new-flow-v0pm96mr.js +4 -0
- package/dist/chunks/cf-account-picker-xb8vvt2h.js +4 -0
- package/dist/chunks/cf-settings-mkbcz4z2.js +4 -0
- package/dist/chunks/components-w5zqwva1.js +4 -0
- package/dist/chunks/content-source-g38w0d3f.js +4 -0
- package/dist/chunks/convert-to-repeater-r8sfxjjm.js +4 -0
- package/dist/chunks/editor-wvf5e6j9.js +4 -0
- package/dist/chunks/entry-commands-t13dyw3c.js +4 -0
- package/dist/chunks/extension-sections-vjzw46xb.js +4 -0
- package/dist/chunks/file-ops-1sghzgcr.js +4 -0
- package/dist/chunks/files-zrqtgt41.js +4 -0
- package/dist/chunks/format-host-9gy76erh.js +4 -0
- package/dist/chunks/fs-events-45rhg3me.js +4 -0
- package/dist/chunks/grid-open-t4fq6bbm.js +4 -0
- package/dist/chunks/{grid-view-tghcwrrt.js → grid-view-2f03wcgw.js} +16 -16
- package/dist/chunks/icons-gb967qc8.js +4 -0
- package/dist/chunks/iframe-host-9aq8ze3q.js +4 -0
- package/dist/chunks/image-bake-4afr4y6a.js +4 -0
- package/dist/chunks/image-editor-x7t8jbjn.js +4 -0
- package/dist/chunks/index-f9qr7kzh.js +4 -0
- package/dist/chunks/{index-vr006kbt.js → index-yjrfeks8.js} +3 -3
- package/dist/chunks/{javascript-z1g3s9fd.js → javascript-hy2mws8v.js} +3 -3
- package/dist/chunks/{jsonMode-7szwg8c4.js → jsonMode-vj30te2h.js} +6 -6
- package/dist/chunks/jx-validate-r4ax11e3.js +4 -0
- package/dist/chunks/layers-m49nmwvx.js +4 -0
- package/dist/chunks/layers-panel-zr92pwdp.js +4 -0
- package/dist/chunks/lit-html-k2mb9k0r.js +4 -0
- package/dist/chunks/media-library-j74rt0w4.js +4 -0
- package/dist/chunks/media-open-mypr0k6b.js +4 -0
- package/dist/chunks/media-picker-pemtbsjv.js +4 -0
- package/dist/chunks/{monaco-binding-v42w9c1v.js → monaco-binding-a51py33c.js} +3 -3
- package/dist/chunks/monaco-buffer-pa0g6ax7.js +4 -0
- package/dist/chunks/{monaco-setup-7bffxd6v.js → monaco-setup-0hw9f835.js} +3 -3
- package/dist/chunks/notify-bwehrp7q.js +4 -0
- package/dist/chunks/parse-hee7ne9b.js +4 -0
- package/dist/chunks/platform-kv0k65cy.js +4 -0
- package/dist/chunks/publish-panel-vg1n880b.js +4 -0
- package/dist/chunks/publish-quick-4fwngwrw.js +4 -0
- package/dist/chunks/publish-workspace-jv4w2bx2.js +4 -0
- package/dist/chunks/quick-search-nh29rt8a.js +4 -0
- package/dist/chunks/references-vqyr5s9x.js +4 -0
- package/dist/chunks/right-panel-3ws9f2w0.js +4 -0
- package/dist/chunks/source-control-h3hcs5gy.js +4 -0
- package/dist/chunks/{studio-j3d2fjqz.js → studio-0939t1q1.js} +4 -4
- package/dist/chunks/studio-0gvvqj3h.js +5 -0
- package/dist/chunks/studio-0khd8xtx.js +4 -0
- package/dist/chunks/{studio-9b7177jq.js → studio-0x6f3nk9.js} +4 -4
- package/dist/chunks/studio-1410q5dw.js +5 -0
- package/dist/chunks/studio-1ctnw3xv.js +5 -0
- package/dist/chunks/studio-1ks5d9y4.js +5 -0
- package/dist/chunks/{studio-awxwjawq.js → studio-1y9mab9m.js} +4 -4
- package/dist/chunks/studio-2e4hry72.js +6 -0
- package/dist/chunks/studio-2kpxm0eh.js +7 -0
- package/dist/chunks/studio-2pk7kc7z.js +5 -0
- package/dist/chunks/studio-2x972v58.js +5 -0
- package/dist/chunks/studio-2zz4qbv9.js +5 -0
- package/dist/chunks/{studio-bvs9kpst.js → studio-3na1r48j.js} +8 -8
- package/dist/chunks/studio-3pz4t5a2.js +5 -0
- package/dist/chunks/studio-3qr08sxq.js +4 -0
- package/dist/chunks/{studio-75kcdbsk.js → studio-45mj4jqe.js} +4 -4
- package/dist/chunks/studio-4bgvvzhv.js +5 -0
- package/dist/chunks/studio-4f7thq0p.js +5 -0
- package/dist/chunks/studio-4fnb5648.js +5 -0
- package/dist/chunks/studio-4zd6an2b.js +5 -0
- package/dist/chunks/{studio-9rbe2xpw.js → studio-5tet77de.js} +3 -3
- package/dist/chunks/studio-61rg2cz5.js +5 -0
- package/dist/chunks/{studio-ybnwymxw.js → studio-7rrdbvex.js} +4 -4
- package/dist/chunks/{studio-sxdb9h3q.js → studio-90zkgne5.js} +7 -7
- package/dist/chunks/studio-9c6cbq2c.js +4 -0
- package/dist/chunks/studio-9dq7rwc7.js +5 -0
- package/dist/chunks/studio-9gr7xs9r.js +6 -0
- package/dist/chunks/studio-a88ej93p.js +7 -0
- package/dist/chunks/{studio-er5f383a.js → studio-abh9kfdh.js} +4 -4
- package/dist/chunks/studio-acer0g8w.js +5 -0
- package/dist/chunks/{studio-qh8berdz.js → studio-amaz545p.js} +4 -4
- package/dist/chunks/studio-az3wvyh2.js +5 -0
- package/dist/chunks/studio-bg8pzprg.js +5 -0
- package/dist/chunks/{studio-399a25za.js → studio-bnc50f9h.js} +4 -4
- package/dist/chunks/studio-brh8d2pr.js +5 -0
- package/dist/chunks/{studio-646nn2ys.js → studio-c2fa783p.js} +5 -5
- package/dist/chunks/studio-cfnn70h1.js +5 -0
- package/dist/chunks/{studio-rzqaz21g.js → studio-cqxj4fcy.js} +4 -4
- package/dist/chunks/studio-cw11h078.js +5 -0
- package/dist/chunks/studio-daksw8kh.js +5 -0
- package/dist/chunks/studio-dn56z7sn.js +5 -0
- package/dist/chunks/studio-egnjpyde.js +9 -0
- package/dist/chunks/studio-ejrypvmq.js +5 -0
- package/dist/chunks/studio-eqcddvc2.js +928 -0
- package/dist/chunks/studio-evq47j2z.js +5 -0
- package/dist/chunks/{studio-r7pf0rkv.js → studio-frc16j8v.js} +7 -7
- package/dist/chunks/studio-g0hxtcga.js +5 -0
- package/dist/chunks/{studio-th7z3fg7.js → studio-g9brbewz.js} +4 -4
- package/dist/chunks/studio-gnazwtv3.js +5 -0
- package/dist/chunks/studio-hfqpfmpk.js +5 -0
- package/dist/chunks/studio-hh394phe.js +5 -0
- package/dist/chunks/{studio-pjtntksy.js → studio-j0zf5x2x.js} +4 -4
- package/dist/chunks/studio-j5fcp6gj.js +5 -0
- package/dist/chunks/studio-jd1qb1st.js +7 -0
- package/dist/chunks/studio-jma2q8r9.js +5 -0
- package/dist/chunks/studio-k0xzrse2.js +64 -0
- package/dist/chunks/studio-k1xqrgm5.js +5 -0
- package/dist/chunks/studio-k2evngrj.js +7 -0
- package/dist/chunks/studio-k3we3kqb.js +5 -0
- package/dist/chunks/studio-knsjyx7n.js +5 -0
- package/dist/chunks/studio-mrjtnm62.js +5 -0
- package/dist/chunks/{studio-sevbc3xv.js → studio-n9qba0gx.js} +4 -4
- package/dist/chunks/{studio-ea74s8c1.js → studio-nvcn2ykc.js} +4 -4
- package/dist/chunks/studio-nwf0c32p.js +5 -0
- package/dist/chunks/studio-nzemfbkk.js +14 -0
- package/dist/chunks/{studio-43y04qck.js → studio-pj216fmn.js} +4 -4
- package/dist/chunks/{studio-4g8yb6jn.js → studio-pm17wdc4.js} +4 -4
- package/dist/chunks/studio-pwctzka3.js +16 -0
- package/dist/chunks/{studio-71m92v9s.js → studio-q80fg1d6.js} +7 -7
- package/dist/chunks/studio-q9hx2qp0.js +5 -0
- package/dist/chunks/studio-qrzmfaf6.js +5 -0
- package/dist/chunks/{studio-86r8hp7r.js → studio-r6mx7vm5.js} +5 -5
- package/dist/chunks/studio-rr00ex23.js +7 -0
- package/dist/chunks/{studio-4jhw38b2.js → studio-s24mhq0z.js} +4 -4
- package/dist/chunks/studio-sce30r7b.js +4 -0
- package/dist/chunks/studio-swedpd82.js +5 -0
- package/dist/chunks/studio-tmfcn347.js +6 -0
- package/dist/chunks/studio-tpxcryf2.js +5 -0
- package/dist/chunks/{studio-wc6za3zs.js → studio-v5wktj6f.js} +4 -4
- package/dist/chunks/studio-v7a7yb5w.js +9 -0
- package/dist/chunks/{studio-kg1abnz0.js → studio-vpevkfxw.js} +4 -4
- package/dist/chunks/{studio-881z1xrx.js → studio-wmjmsw0p.js} +4 -4
- package/dist/chunks/studio-wqfaqw4m.js +5 -0
- package/dist/chunks/{studio-hhp4x8xf.js → studio-xpx91fby.js} +5 -5
- package/dist/chunks/studio-xva3hzkv.js +5 -0
- package/dist/chunks/{studio-6hzdjfv0.js → studio-xx3r3sb0.js} +4 -4
- package/dist/chunks/{studio-cg1bfyhf.js → studio-z3fztxrp.js} +8 -8
- package/dist/chunks/studio-z9453ypn.js +6 -0
- package/dist/chunks/{tsMode-f54j66ym.js → tsMode-jg1dtphj.js} +7 -7
- package/dist/chunks/workspace-6ztqcrsh.js +4 -0
- package/dist/chunks/workspace-home-rse9h8a0.js +4 -0
- package/dist/chunks/{ws-client-e8kj684q.js → ws-client-arz2qsy9.js} +3 -3
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +83 -78
- package/dist/studio.js +84 -84
- package/package.json +11 -11
- package/src/browse/library-pane.ts +44 -29
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +13 -4
- package/src/canvas/asset-resolve.ts +54 -11
- package/src/canvas/canvas-render.ts +25 -0
- package/src/canvas/iframe-entry.ts +5 -1
- package/src/canvas/iframe-host.ts +16 -6
- package/src/canvas/iframe-protocol.ts +9 -1
- package/src/commands/context.ts +5 -0
- package/src/commands/live-context.ts +5 -2
- package/src/editor/shortcuts.ts +3 -0
- package/src/files/asset-versions.ts +108 -2
- package/src/files/file-ops.ts +11 -1
- package/src/files/files.ts +111 -48
- package/src/files/media-paths.ts +12 -4
- package/src/files/media-upload.ts +195 -21
- package/src/files/new-file-name.ts +39 -1
- package/src/media/image-bake.ts +164 -0
- package/src/media/image-editor.ts +846 -0
- package/src/media/image-format.ts +262 -0
- package/src/media/image-recipe.ts +651 -0
- package/src/media/media-commands.ts +65 -1
- package/src/media/media-library-model.ts +1 -1
- package/src/media/media-library.ts +175 -15
- package/src/media/media-open.ts +156 -3
- package/src/media/media-pane.ts +8 -4
- package/src/panels/pane-context.ts +6 -1
- package/src/panels/tab-strip.ts +12 -3
- package/src/studio.ts +13 -2
- package/src/surfaces/image-editor.json +1390 -0
- package/src/surfaces/image-editor.ts +272 -0
- package/src/surfaces/inspector-dock.json +3 -3
- package/src/surfaces/media-library.json +129 -1
- package/src/surfaces/media-library.ts +17 -2
- package/src/types.ts +2 -1
- package/src/workspace/tab-workspace.ts +2 -1
- package/src/workspace/workspace-switch.ts +3 -1
- package/src/workspace/workspace.ts +22 -1
- package/dist/chunks/adapter-pp011m5x.js +0 -4
- package/dist/chunks/add-new-flow-ecqmzz76.js +0 -4
- package/dist/chunks/cf-account-picker-c07znkqt.js +0 -4
- package/dist/chunks/cf-settings-0v2bmqy0.js +0 -4
- package/dist/chunks/components-yn0dn2pq.js +0 -4
- package/dist/chunks/content-source-g0j41yy6.js +0 -4
- package/dist/chunks/convert-to-repeater-sbjk7tgn.js +0 -4
- package/dist/chunks/editor-xz02jeh1.js +0 -4
- package/dist/chunks/entry-commands-kmt02crp.js +0 -4
- package/dist/chunks/extension-sections-0rprf12d.js +0 -4
- package/dist/chunks/file-ops-mexj5qed.js +0 -4
- package/dist/chunks/files-b2tcppdd.js +0 -4
- package/dist/chunks/format-host-btsevv0c.js +0 -4
- package/dist/chunks/fs-events-r0apejva.js +0 -4
- package/dist/chunks/grid-open-n2a8xkht.js +0 -4
- package/dist/chunks/icons-853n6qt3.js +0 -4
- package/dist/chunks/iframe-host-gx0ypyh3.js +0 -4
- package/dist/chunks/index-bs4k9tr6.js +0 -4
- package/dist/chunks/jx-validate-ymf10teh.js +0 -4
- package/dist/chunks/layers-p9nfya38.js +0 -4
- package/dist/chunks/layers-panel-tt1hzhhf.js +0 -4
- package/dist/chunks/lit-html-x18n2sfz.js +0 -4
- package/dist/chunks/media-library-0n45gv8r.js +0 -4
- package/dist/chunks/media-picker-s09at95z.js +0 -4
- package/dist/chunks/monaco-buffer-een2ea0v.js +0 -4
- package/dist/chunks/notify-qwb8radt.js +0 -4
- package/dist/chunks/parse-v77xtwv7.js +0 -4
- package/dist/chunks/platform-8jnez0wr.js +0 -4
- package/dist/chunks/publish-panel-aysnj611.js +0 -4
- package/dist/chunks/publish-quick-kpbqs9mz.js +0 -4
- package/dist/chunks/publish-workspace-nd0a8q99.js +0 -4
- package/dist/chunks/quick-search-vg0jkhk2.js +0 -4
- package/dist/chunks/references-a7v6a0e2.js +0 -4
- package/dist/chunks/right-panel-cvk1vxx5.js +0 -4
- package/dist/chunks/source-control-fsqzc6he.js +0 -4
- package/dist/chunks/studio-05jfwbmf.js +0 -5
- package/dist/chunks/studio-0q5jwddh.js +0 -9
- package/dist/chunks/studio-0w2dr0q8.js +0 -5
- package/dist/chunks/studio-1tzpds97.js +0 -9
- package/dist/chunks/studio-216b27sv.js +0 -4
- package/dist/chunks/studio-2ejhqns4.js +0 -5
- package/dist/chunks/studio-2r90cts3.js +0 -16
- package/dist/chunks/studio-399xknkb.js +0 -6
- package/dist/chunks/studio-4ncmwqw5.js +0 -5
- package/dist/chunks/studio-5atwjgkj.js +0 -7
- package/dist/chunks/studio-5frqk7c0.js +0 -5
- package/dist/chunks/studio-6cwj6xrx.js +0 -5
- package/dist/chunks/studio-78pywnp2.js +0 -6
- package/dist/chunks/studio-886t6ztg.js +0 -5
- package/dist/chunks/studio-8wnvtr4z.js +0 -5
- package/dist/chunks/studio-8z66jk4z.js +0 -5
- package/dist/chunks/studio-b2qpt0xy.js +0 -14
- package/dist/chunks/studio-c71806xz.js +0 -7
- package/dist/chunks/studio-cbwks69x.js +0 -928
- package/dist/chunks/studio-cg710sk9.js +0 -5
- package/dist/chunks/studio-cw0wb9qk.js +0 -7
- package/dist/chunks/studio-cyheys7k.js +0 -5
- package/dist/chunks/studio-ea5m9kg5.js +0 -5
- package/dist/chunks/studio-efdxjwyp.js +0 -64
- package/dist/chunks/studio-egmw2837.js +0 -5
- package/dist/chunks/studio-f746039f.js +0 -5
- package/dist/chunks/studio-fmdymd9v.js +0 -5
- package/dist/chunks/studio-gcm12z00.js +0 -6
- package/dist/chunks/studio-gj98m8gy.js +0 -7
- package/dist/chunks/studio-hasbgjah.js +0 -5
- package/dist/chunks/studio-hz5n7x27.js +0 -5
- package/dist/chunks/studio-jeraxdgv.js +0 -7
- package/dist/chunks/studio-jvgek9xt.js +0 -5
- package/dist/chunks/studio-jws1a87n.js +0 -5
- package/dist/chunks/studio-k2sz13zg.js +0 -5
- package/dist/chunks/studio-k3m4jkdk.js +0 -5
- package/dist/chunks/studio-kg5ya5r2.js +0 -5
- package/dist/chunks/studio-khwbgrtg.js +0 -5
- package/dist/chunks/studio-mx6wzh25.js +0 -5
- package/dist/chunks/studio-mxs3ar9t.js +0 -5
- package/dist/chunks/studio-nm4szey4.js +0 -5
- package/dist/chunks/studio-nqxqf8p2.js +0 -4
- package/dist/chunks/studio-qn7z4krm.js +0 -5
- package/dist/chunks/studio-qyed0ah6.js +0 -5
- package/dist/chunks/studio-r4bcdwhq.js +0 -5
- package/dist/chunks/studio-rtfnhrdg.js +0 -5
- package/dist/chunks/studio-s5tzw84t.js +0 -5
- package/dist/chunks/studio-t51y8v1d.js +0 -5
- package/dist/chunks/studio-t98vq4r6.js +0 -5
- package/dist/chunks/studio-tsxpqvh0.js +0 -6
- package/dist/chunks/studio-v1yascv2.js +0 -5
- package/dist/chunks/studio-wxq6v5sv.js +0 -5
- package/dist/chunks/studio-y60sts62.js +0 -4
- package/dist/chunks/studio-zndrqawb.js +0 -5
- package/dist/chunks/studio-zwaxn5s2.js +0 -5
- package/dist/chunks/workspace-home-x1787wpv.js +0 -4
- package/dist/chunks/workspace-zpe1np43.js +0 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which image files the Image editor may write, decided from the bytes (studio.md §9.5).
|
|
3
|
+
*
|
|
4
|
+
* An extension is a claim about the bytes, and the bytes are checked. A still `.png` and an
|
|
5
|
+
* animated one share an extension, so the container is read: the PNG signature with an `acTL` chunk
|
|
6
|
+
* before the first `IDAT`, or `RIFF`/`WEBP` with the animation bit in a `VP8X` header. An animated
|
|
7
|
+
* `.png` or `.webp` is refused for the reason `.gif` is, rather than silently losing every frame
|
|
8
|
+
* but the first.
|
|
9
|
+
*
|
|
10
|
+
* **No `TextDecoder` anywhere.** Every Latin-1 label is windows-1252 in the Encoding Standard,
|
|
11
|
+
* which remaps 0x80 to 0x9F: `new TextDecoder("latin1").decode([0x89])` is U+2030, and 0x89 is the
|
|
12
|
+
* PNG signature's first byte. So every comparison here is byte against byte.
|
|
13
|
+
*
|
|
14
|
+
* Pure: a function over bytes the editor already holds. It decodes nothing.
|
|
15
|
+
*
|
|
16
|
+
* @docs studio/projects/media
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { extensionOf } from "../files/media-types";
|
|
20
|
+
import type { PixelSize } from "./image-recipe";
|
|
21
|
+
|
|
22
|
+
/** The containers this module can name. */
|
|
23
|
+
export type ImageContainer = "png" | "jpeg" | "webp" | "gif" | "avif" | "unknown";
|
|
24
|
+
|
|
25
|
+
/** What a file's own bytes say it is. */
|
|
26
|
+
export interface ImageSniff {
|
|
27
|
+
container: ImageContainer;
|
|
28
|
+
/** More than one frame. Only ever true for a container that can carry an animation. */
|
|
29
|
+
animated: boolean;
|
|
30
|
+
/** The stored pixel size from the header, or null when the header does not say. */
|
|
31
|
+
size: PixelSize | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The three formats a host can both decode and encode, by extension. */
|
|
35
|
+
const EDITABLE: ReadonlyMap<string, { container: ImageContainer; label: string; mime: string }> =
|
|
36
|
+
new Map([
|
|
37
|
+
[".jpeg", { container: "jpeg", label: "JPEG", mime: "image/jpeg" }],
|
|
38
|
+
[".jpg", { container: "jpeg", label: "JPEG", mime: "image/jpeg" }],
|
|
39
|
+
[".png", { container: "png", label: "PNG", mime: "image/png" }],
|
|
40
|
+
[".webp", { container: "webp", label: "WebP", mime: "image/webp" }],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/** Why an image extension outside the editable set is not offered, one reason each. */
|
|
44
|
+
const REFUSED: ReadonlyMap<string, string> = new Map([
|
|
45
|
+
[".avif", "The browser has no AVIF encoder, so an edit could not be written back as AVIF."],
|
|
46
|
+
[".bmp", "The browser has no BMP encoder, so an edit could not be written back as BMP."],
|
|
47
|
+
[
|
|
48
|
+
".gif",
|
|
49
|
+
"The browser has no GIF encoder, and a bake would flatten the animation to its first frame.",
|
|
50
|
+
],
|
|
51
|
+
[".ico", "An icon file holds several resolutions, not one image."],
|
|
52
|
+
[".svg", "An SVG is a vector document, not a bitmap. Edit it in its Source mode instead."],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The most source pixels a bake will take on, counted before anything is decoded.
|
|
57
|
+
*
|
|
58
|
+
* A bake holds the decoded source and the destination at once, four bytes a pixel each, and the
|
|
59
|
+
* destination is never larger than the source (enlarging is not offered). 48 megapixels is 8000 ×
|
|
60
|
+
* 6000, a full-frame camera's output: 48e6 × 4 B × 2 surfaces is about 384 MB at the peak, which a
|
|
61
|
+
* desktop browser tab can hold and a larger figure starts not to. A ceiling in BYTES ON DISK would
|
|
62
|
+
* be the wrong unit, since a compressed file says nothing about its decoded size, and
|
|
63
|
+
* `assetCapabilities.maxUploadBytes` is declared by no adapter, so there is no host figure to
|
|
64
|
+
* borrow either.
|
|
65
|
+
*/
|
|
66
|
+
export const SOURCE_PIXEL_CEILING = 48_000_000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The host's largest drawing surface. Measured in Chromium: 16384 × 16384 draws and reads back, and
|
|
70
|
+
* 20000 × 20000 hands back a context whose pixels read as zeros. Past it a bake would "succeed" and
|
|
71
|
+
* write an empty image, so the bake refuses rather than finding out.
|
|
72
|
+
*/
|
|
73
|
+
export const MAX_SURFACE_SIDE = 16_384;
|
|
74
|
+
export const MAX_SURFACE_AREA = MAX_SURFACE_SIDE * MAX_SURFACE_SIDE;
|
|
75
|
+
|
|
76
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] as const;
|
|
77
|
+
|
|
78
|
+
/** Whether `bytes` holds `ascii` at `at`, compared byte by byte. */
|
|
79
|
+
function hasAscii(bytes: Uint8Array, at: number, ascii: string): boolean {
|
|
80
|
+
if (at + ascii.length > bytes.length) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
for (let i = 0; i < ascii.length; i++) {
|
|
84
|
+
if (bytes[at + i] !== ascii.codePointAt(i)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function startsWithBytes(bytes: Uint8Array, prefix: readonly number[]): boolean {
|
|
92
|
+
return prefix.length <= bytes.length && prefix.every((byte, i) => bytes[i] === byte);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The bytes as a view that reads integers in either byte order. Arithmetic rather than bit
|
|
97
|
+
* operators throughout, so no read here can wrap a 32-bit value negative.
|
|
98
|
+
*/
|
|
99
|
+
function view(bytes: Uint8Array): DataView {
|
|
100
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A 24-bit little-endian integer, the width WebP's `VP8X` header stores each side minus one in. */
|
|
104
|
+
function u24le(bytes: Uint8Array, at: number): number {
|
|
105
|
+
return view(bytes).getUint16(at, true) + bytes[at + 2]! * 0x1_00_00;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The low fourteen bits of an integer: a WebP side, which is never wider than 16383. */
|
|
109
|
+
function low14(value: number): number {
|
|
110
|
+
return value % 0x40_00;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A PNG: its `IHDR` size, and whether an `acTL` chunk comes before the first `IDAT`. */
|
|
114
|
+
function sniffPng(bytes: Uint8Array): ImageSniff {
|
|
115
|
+
let size: PixelSize | null = null;
|
|
116
|
+
let at = PNG_SIGNATURE.length;
|
|
117
|
+
while (at + 8 <= bytes.length) {
|
|
118
|
+
const length = view(bytes).getUint32(at);
|
|
119
|
+
if (hasAscii(bytes, at + 4, "IHDR") && at + 16 <= bytes.length) {
|
|
120
|
+
size = { height: view(bytes).getUint32(at + 12), width: view(bytes).getUint32(at + 8) };
|
|
121
|
+
} else if (hasAscii(bytes, at + 4, "acTL")) {
|
|
122
|
+
return { animated: true, container: "png", size };
|
|
123
|
+
} else if (hasAscii(bytes, at + 4, "IDAT") || hasAscii(bytes, at + 4, "IEND")) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
at += 12 + length;
|
|
127
|
+
}
|
|
128
|
+
return { animated: false, container: "png", size };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A WebP: the first chunk's size, and the `VP8X` animation flag (0x02). */
|
|
132
|
+
function sniffWebp(bytes: Uint8Array): ImageSniff {
|
|
133
|
+
if (hasAscii(bytes, 12, "VP8X") && bytes.length >= 30) {
|
|
134
|
+
return {
|
|
135
|
+
// Bit 1 of the flags byte.
|
|
136
|
+
animated: Math.floor(bytes[20]! / 2) % 2 === 1,
|
|
137
|
+
container: "webp",
|
|
138
|
+
size: { height: u24le(bytes, 27) + 1, width: u24le(bytes, 24) + 1 },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (hasAscii(bytes, 12, "VP8L") && bytes.length >= 25) {
|
|
142
|
+
// Fourteen bits of width minus one, then fourteen of height minus one, after a signature byte.
|
|
143
|
+
const bits = view(bytes).getUint32(21, true);
|
|
144
|
+
return {
|
|
145
|
+
animated: false,
|
|
146
|
+
container: "webp",
|
|
147
|
+
size: { height: low14(Math.floor(bits / 0x40_00)) + 1, width: low14(bits) + 1 },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (hasAscii(bytes, 12, "VP8 ") && bytes.length >= 30) {
|
|
151
|
+
return {
|
|
152
|
+
animated: false,
|
|
153
|
+
container: "webp",
|
|
154
|
+
size: {
|
|
155
|
+
height: low14(view(bytes).getUint16(28, true)),
|
|
156
|
+
width: low14(view(bytes).getUint16(26, true)),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { animated: false, container: "webp", size: null };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A JPEG: the size from the first start-of-frame marker. A JPEG is never animated. */
|
|
164
|
+
function sniffJpeg(bytes: Uint8Array): ImageSniff {
|
|
165
|
+
let at = 2;
|
|
166
|
+
while (at + 9 <= bytes.length) {
|
|
167
|
+
if (bytes[at] !== 0xff) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
const marker = bytes[at + 1]!;
|
|
171
|
+
// Any number of fill bytes may precede a marker (T.81 B.1.1.2), and a standalone marker has no length.
|
|
172
|
+
if (marker === 0xff) {
|
|
173
|
+
at += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
177
|
+
at += 2;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// SOF0 to SOF15, except DHT (C4), JPG (C8) and DAC (CC), which share the range.
|
|
181
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
182
|
+
return {
|
|
183
|
+
animated: false,
|
|
184
|
+
container: "jpeg",
|
|
185
|
+
size: { height: view(bytes).getUint16(at + 5), width: view(bytes).getUint16(at + 7) },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
at += 2 + view(bytes).getUint16(at + 2);
|
|
189
|
+
}
|
|
190
|
+
return { animated: false, container: "jpeg", size: null };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** What a file's own bytes say it is. Only the header is read; nothing is decoded. */
|
|
194
|
+
export function sniffImage(bytes: Uint8Array): ImageSniff {
|
|
195
|
+
if (startsWithBytes(bytes, PNG_SIGNATURE)) {
|
|
196
|
+
return sniffPng(bytes);
|
|
197
|
+
}
|
|
198
|
+
if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) {
|
|
199
|
+
return sniffJpeg(bytes);
|
|
200
|
+
}
|
|
201
|
+
if (hasAscii(bytes, 0, "RIFF") && hasAscii(bytes, 8, "WEBP")) {
|
|
202
|
+
return sniffWebp(bytes);
|
|
203
|
+
}
|
|
204
|
+
if (hasAscii(bytes, 0, "GIF8")) {
|
|
205
|
+
return { animated: false, container: "gif", size: null };
|
|
206
|
+
}
|
|
207
|
+
if (hasAscii(bytes, 4, "ftyp") && (hasAscii(bytes, 8, "avif") || hasAscii(bytes, 8, "avis"))) {
|
|
208
|
+
return { animated: hasAscii(bytes, 8, "avis"), container: "avif", size: null };
|
|
209
|
+
}
|
|
210
|
+
return { animated: false, container: "unknown", size: null };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The MIME type a path's extension promises, for the three editable formats; null otherwise. */
|
|
214
|
+
export function mimeForPath(path: string): string | null {
|
|
215
|
+
return EDITABLE.get(extensionOf(path))?.mime ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Whether an encode quality means anything for this type. It is an encode parameter for `.jpg` and
|
|
220
|
+
* `.webp` and has no meaning for `.png`, which is lossless, so it is not drawn for one.
|
|
221
|
+
*/
|
|
222
|
+
export function takesQuality(mime: string): boolean {
|
|
223
|
+
return mime === "image/jpeg" || mime === "image/webp";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether the path's extension is one the editor can write. The cheap half of the question, asked
|
|
228
|
+
* before any byte is read: it decides whether the Image mode is OFFERED at all.
|
|
229
|
+
*/
|
|
230
|
+
export function isEditableExtension(path: string): boolean {
|
|
231
|
+
return EDITABLE.has(extensionOf(path));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Why the file at `path` with these bytes cannot be edited, or null when it can.
|
|
236
|
+
*
|
|
237
|
+
* Four answers, each a sentence a person can act on: an extension outside the set, bytes of a
|
|
238
|
+
* different format than the extension promises, an animation, and a source past
|
|
239
|
+
* {@link SOURCE_PIXEL_CEILING} (stated with the limit, before anything is decoded).
|
|
240
|
+
*/
|
|
241
|
+
export function editRefusal(path: string, bytes: Uint8Array): string | null {
|
|
242
|
+
const ext = extensionOf(path);
|
|
243
|
+
const editable = EDITABLE.get(ext);
|
|
244
|
+
if (editable === undefined) {
|
|
245
|
+
return (
|
|
246
|
+
REFUSED.get(ext) ??
|
|
247
|
+
`${ext || "A file with no extension"} is not an image format Studio edits.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const sniff = sniffImage(bytes);
|
|
251
|
+
if (sniff.container !== editable.container) {
|
|
252
|
+
return `This file's bytes are not ${editable.label}, whatever its ${ext} extension says, so an edit could not be written back as one.`;
|
|
253
|
+
}
|
|
254
|
+
if (sniff.animated) {
|
|
255
|
+
return `This ${editable.label} is animated, and a bake would keep only its first frame.`;
|
|
256
|
+
}
|
|
257
|
+
if (sniff.size !== null && sniff.size.width * sniff.size.height > SOURCE_PIXEL_CEILING) {
|
|
258
|
+
const megapixels = (sniff.size.width * sniff.size.height) / 1e6;
|
|
259
|
+
return `This image is ${sniff.size.width} × ${sniff.size.height} (${megapixels.toFixed(1)} megapixels), past the ${SOURCE_PIXEL_CEILING / 1e6} megapixels an edit can hold in memory.`;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|