omni-viewer-core 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/dist/assets/audio-engine/audio_engine.mjs +2 -0
- package/dist/assets/audio-engine/audio_engine.wasm +0 -0
- package/dist/assets/pdfjs/pdf.worker.min.mjs +21 -0
- package/dist/host/index.d.ts +133 -0
- package/dist/host/index.js +10 -0
- package/dist/i18n/catalog.en.d.ts +1 -0
- package/dist/i18n/catalog.en.js +411 -0
- package/dist/i18n/catalog.ko.d.ts +1 -0
- package/dist/i18n/catalog.ko.js +68 -0
- package/dist/i18n/index.d.ts +18 -0
- package/dist/i18n/index.js +41 -0
- package/dist/licenses/emscripten.txt +101 -0
- package/dist/licenses/kissfft.txt +28 -0
- package/dist/licenses/pdfjs-dist.txt +177 -0
- package/dist/parsers/a2l/index.d.ts +1 -0
- package/dist/parsers/a2l/index.js +1 -0
- package/dist/parsers/archive/index.d.ts +22 -0
- package/dist/parsers/archive/index.js +46 -0
- package/dist/parsers/archive/model.d.ts +29 -0
- package/dist/parsers/archive/model.js +1 -0
- package/dist/parsers/archive/worker.d.ts +21 -0
- package/dist/parsers/archive/worker.js +1 -0
- package/dist/parsers/arxml/index.d.ts +1 -0
- package/dist/parsers/arxml/index.js +1 -0
- package/dist/parsers/asc/index.d.ts +1 -0
- package/dist/parsers/asc/index.js +1 -0
- package/dist/parsers/audio/index.d.ts +9 -0
- package/dist/parsers/audio/index.js +46 -0
- package/dist/parsers/automotive/index.d.ts +33 -0
- package/dist/parsers/automotive/index.js +973 -0
- package/dist/parsers/blf/index.d.ts +1 -0
- package/dist/parsers/blf/index.js +1 -0
- package/dist/parsers/csv/delimiter.d.ts +31 -0
- package/dist/parsers/csv/delimiter.js +151 -0
- package/dist/parsers/csv/index.d.ts +30 -0
- package/dist/parsers/csv/index.js +108 -0
- package/dist/parsers/csv/parseText.d.ts +20 -0
- package/dist/parsers/csv/parseText.js +108 -0
- package/dist/parsers/csv/sort.d.ts +26 -0
- package/dist/parsers/csv/sort.js +224 -0
- package/dist/parsers/csv/statistics.d.ts +49 -0
- package/dist/parsers/csv/statistics.js +150 -0
- package/dist/parsers/dbc/index.d.ts +56 -0
- package/dist/parsers/dbc/index.js +94 -0
- package/dist/parsers/doc-binary/index.d.ts +35 -0
- package/dist/parsers/doc-binary/index.js +267 -0
- package/dist/parsers/doc-binary/vscode-parser.d.ts +142 -0
- package/dist/parsers/doc-binary/vscode-parser.js +3747 -0
- package/dist/parsers/excel/index.d.ts +17 -0
- package/dist/parsers/excel/index.js +406 -0
- package/dist/parsers/excel/model.d.ts +80 -0
- package/dist/parsers/excel/model.js +48 -0
- package/dist/parsers/excel/serialize.d.ts +3 -0
- package/dist/parsers/excel/serialize.js +78 -0
- package/dist/parsers/hdf5/index.d.ts +75 -0
- package/dist/parsers/hdf5/index.js +658 -0
- package/dist/parsers/hdf5/node.d.ts +21 -0
- package/dist/parsers/hdf5/node.js +95 -0
- package/dist/parsers/json/index.d.ts +15 -0
- package/dist/parsers/json/index.js +102 -0
- package/dist/parsers/json/model.d.ts +42 -0
- package/dist/parsers/json/model.js +140 -0
- package/dist/parsers/json/parse.d.ts +22 -0
- package/dist/parsers/json/parse.js +279 -0
- package/dist/parsers/json/tokenizer.d.ts +17 -0
- package/dist/parsers/json/tokenizer.js +175 -0
- package/dist/parsers/jsonl/index.d.ts +14 -0
- package/dist/parsers/jsonl/index.js +18 -0
- package/dist/parsers/jsonl/model.d.ts +23 -0
- package/dist/parsers/jsonl/model.js +1 -0
- package/dist/parsers/jsonl/parse.d.ts +7 -0
- package/dist/parsers/jsonl/parse.js +34 -0
- package/dist/parsers/markdown/index.d.ts +37 -0
- package/dist/parsers/markdown/index.js +59 -0
- package/dist/parsers/mat/index.d.ts +31 -0
- package/dist/parsers/mat/index.js +426 -0
- package/dist/parsers/mermaid/index.d.ts +5 -0
- package/dist/parsers/mermaid/index.js +3 -0
- package/dist/parsers/mf4/index.d.ts +1 -0
- package/dist/parsers/mf4/index.js +1 -0
- package/dist/parsers/parquet/index.d.ts +21 -0
- package/dist/parsers/parquet/index.js +68 -0
- package/dist/parsers/pcap/index.d.ts +1 -0
- package/dist/parsers/pcap/index.js +1 -0
- package/dist/parsers/pcapng/index.d.ts +1 -0
- package/dist/parsers/pcapng/index.js +1 -0
- package/dist/parsers/plantuml/index.d.ts +5 -0
- package/dist/parsers/plantuml/index.js +4 -0
- package/dist/parsers/ppt-binary/index.d.ts +5 -0
- package/dist/parsers/ppt-binary/index.js +62 -0
- package/dist/parsers/ppt-binary/pptBinaryBuildContext.d.ts +12 -0
- package/dist/parsers/ppt-binary/pptBinaryBuildContext.js +40 -0
- package/dist/parsers/ppt-binary/pptBinaryContainer.d.ts +4 -0
- package/dist/parsers/ppt-binary/pptBinaryContainer.js +174 -0
- package/dist/parsers/ppt-binary/pptBinaryTypes.d.ts +125 -0
- package/dist/parsers/ppt-binary/pptBinaryTypes.js +1 -0
- package/dist/parsers/ppt-binary/pptSlideLayouts.d.ts +9 -0
- package/dist/parsers/ppt-binary/pptSlideLayouts.js +510 -0
- package/dist/parsers/ppt-binary/vscode-parser.d.ts +116 -0
- package/dist/parsers/ppt-binary/vscode-parser.js +2853 -0
- package/dist/parsers/ppt-binary/vscode.d.ts +4 -0
- package/dist/parsers/ppt-binary/vscode.js +12 -0
- package/dist/parsers/pptx/index.d.ts +15 -0
- package/dist/parsers/pptx/index.js +88 -0
- package/dist/parsers/pptx/vscode-parser.d.ts +167 -0
- package/dist/parsers/pptx/vscode-parser.js +1503 -0
- package/dist/parsers/pptx/vscode.d.ts +4 -0
- package/dist/parsers/pptx/vscode.js +18 -0
- package/dist/parsers/pptx/zip-reader.d.ts +3 -0
- package/dist/parsers/pptx/zip-reader.js +20 -0
- package/dist/parsers/proto/index.d.ts +90 -0
- package/dist/parsers/proto/index.js +197 -0
- package/dist/parsers/psd/index.d.ts +16 -0
- package/dist/parsers/psd/index.js +10 -0
- package/dist/parsers/shapefile/dbf.d.ts +23 -0
- package/dist/parsers/shapefile/dbf.js +112 -0
- package/dist/parsers/shapefile/index.d.ts +44 -0
- package/dist/parsers/shapefile/index.js +119 -0
- package/dist/parsers/shapefile/reproject.d.ts +10 -0
- package/dist/parsers/shapefile/reproject.js +41 -0
- package/dist/parsers/slide-model.d.ts +106 -0
- package/dist/parsers/slide-model.js +1 -0
- package/dist/parsers/toml/index.d.ts +15 -0
- package/dist/parsers/toml/index.js +18 -0
- package/dist/parsers/toml/model.d.ts +15 -0
- package/dist/parsers/toml/model.js +1 -0
- package/dist/parsers/toml/parse.d.ts +16 -0
- package/dist/parsers/toml/parse.js +266 -0
- package/dist/parsers/types.d.ts +103 -0
- package/dist/parsers/types.js +106 -0
- package/dist/parsers/video/index.d.ts +6 -0
- package/dist/parsers/video/index.js +2 -0
- package/dist/parsers/yaml/index.d.ts +16 -0
- package/dist/parsers/yaml/index.js +58 -0
- package/dist/parsers/yaml/model.d.ts +22 -0
- package/dist/parsers/yaml/model.js +1 -0
- package/dist/parsers/yaml/self-loading.d.ts +3 -0
- package/dist/parsers/yaml/self-loading.js +19 -0
- package/dist/parsers/zip-scan.d.ts +8 -0
- package/dist/parsers/zip-scan.js +80 -0
- package/dist/registry/container.d.ts +17 -0
- package/dist/registry/container.js +49 -0
- package/dist/registry/index.d.ts +133 -0
- package/dist/registry/index.js +348 -0
- package/dist/registry/probe.d.ts +3 -0
- package/dist/registry/probe.js +177 -0
- package/dist/registry/sniff.d.ts +17 -0
- package/dist/registry/sniff.js +57 -0
- package/dist/styles/archive.css +10 -0
- package/dist/styles/audio.css +66 -0
- package/dist/styles/automotive.css +1 -0
- package/dist/styles/csv.css +262 -0
- package/dist/styles/excel.css +212 -0
- package/dist/styles/fallback.css +48 -0
- package/dist/styles/hdf5.css +3 -0
- package/dist/styles/hwp.css +12 -0
- package/dist/styles/image.css +115 -0
- package/dist/styles/json.css +342 -0
- package/dist/styles/jsonl.css +1 -0
- package/dist/styles/markdown.css +10 -0
- package/dist/styles/mat.css +2 -0
- package/dist/styles/mermaid.css +1 -0
- package/dist/styles/parquet.css +2 -0
- package/dist/styles/pdf.css +551 -0
- package/dist/styles/plantuml.css +1 -0
- package/dist/styles/ppt.css +1 -0
- package/dist/styles/proto.css +2 -0
- package/dist/styles/psd.css +1 -0
- package/dist/styles/shapefile.css +25 -0
- package/dist/styles/toml.css +1 -0
- package/dist/styles/video.css +74 -0
- package/dist/styles/word.css +685 -0
- package/dist/styles/yaml.css +1 -0
- package/dist/viewers/a2l/index.d.ts +15 -0
- package/dist/viewers/a2l/index.js +5 -0
- package/dist/viewers/archive/archive-preview-decoder.d.ts +4 -0
- package/dist/viewers/archive/archive-preview-decoder.js +223 -0
- package/dist/viewers/archive/controller.d.ts +24 -0
- package/dist/viewers/archive/controller.js +18 -0
- package/dist/viewers/archive/index.d.ts +25 -0
- package/dist/viewers/archive/index.js +230 -0
- package/dist/viewers/archive/preview.d.ts +2 -0
- package/dist/viewers/archive/preview.js +2 -0
- package/dist/viewers/archive/styles.d.ts +1 -0
- package/dist/viewers/archive/styles.js +11 -0
- package/dist/viewers/arxml/index.d.ts +15 -0
- package/dist/viewers/arxml/index.js +5 -0
- package/dist/viewers/asc/index.d.ts +15 -0
- package/dist/viewers/asc/index.js +5 -0
- package/dist/viewers/audio/controller.d.ts +33 -0
- package/dist/viewers/audio/controller.js +49 -0
- package/dist/viewers/audio/engine.d.ts +44 -0
- package/dist/viewers/audio/engine.js +108 -0
- package/dist/viewers/audio/index.d.ts +92 -0
- package/dist/viewers/audio/index.js +448 -0
- package/dist/viewers/audio/self-loading.d.ts +7 -0
- package/dist/viewers/audio/self-loading.js +42 -0
- package/dist/viewers/audio/styles.d.ts +1 -0
- package/dist/viewers/audio/styles.js +73 -0
- package/dist/viewers/automotive/index.d.ts +8 -0
- package/dist/viewers/automotive/index.js +152 -0
- package/dist/viewers/automotive/styles.d.ts +1 -0
- package/dist/viewers/automotive/styles.js +1 -0
- package/dist/viewers/avro/index.d.ts +15 -0
- package/dist/viewers/avro/index.js +5 -0
- package/dist/viewers/bag/index.d.ts +15 -0
- package/dist/viewers/bag/index.js +5 -0
- package/dist/viewers/blf/index.d.ts +15 -0
- package/dist/viewers/blf/index.js +5 -0
- package/dist/viewers/csv/controller.d.ts +126 -0
- package/dist/viewers/csv/controller.js +598 -0
- package/dist/viewers/csv/index.d.ts +48 -0
- package/dist/viewers/csv/index.js +846 -0
- package/dist/viewers/csv/styles.d.ts +1 -0
- package/dist/viewers/csv/styles.js +267 -0
- package/dist/viewers/db3/index.d.ts +15 -0
- package/dist/viewers/db3/index.js +5 -0
- package/dist/viewers/dbc/index.d.ts +15 -0
- package/dist/viewers/dbc/index.js +5 -0
- package/dist/viewers/diagram.d.ts +14 -0
- package/dist/viewers/diagram.js +95 -0
- package/dist/viewers/excel/controller.d.ts +108 -0
- package/dist/viewers/excel/controller.js +548 -0
- package/dist/viewers/excel/grid.d.ts +28 -0
- package/dist/viewers/excel/grid.js +99 -0
- package/dist/viewers/excel/index.d.ts +37 -0
- package/dist/viewers/excel/index.js +620 -0
- package/dist/viewers/excel/self-loading.d.ts +6 -0
- package/dist/viewers/excel/self-loading.js +13 -0
- package/dist/viewers/excel/styles.d.ts +1 -0
- package/dist/viewers/excel/styles.js +216 -0
- package/dist/viewers/fallback/index.d.ts +17 -0
- package/dist/viewers/fallback/index.js +130 -0
- package/dist/viewers/fallback/styles.d.ts +1 -0
- package/dist/viewers/fallback/styles.js +49 -0
- package/dist/viewers/hdf5/index.d.ts +19 -0
- package/dist/viewers/hdf5/index.js +170 -0
- package/dist/viewers/hdf5/styles.d.ts +1 -0
- package/dist/viewers/hdf5/styles.js +4 -0
- package/dist/viewers/hwp/controller.d.ts +19 -0
- package/dist/viewers/hwp/controller.js +22 -0
- package/dist/viewers/hwp/index.d.ts +39 -0
- package/dist/viewers/hwp/index.js +271 -0
- package/dist/viewers/hwp/self-loading.d.ts +2 -0
- package/dist/viewers/hwp/self-loading.js +6 -0
- package/dist/viewers/hwp/styles.d.ts +1 -0
- package/dist/viewers/hwp/styles.js +13 -0
- package/dist/viewers/image/controller.d.ts +210 -0
- package/dist/viewers/image/controller.js +336 -0
- package/dist/viewers/image/decode.d.ts +54 -0
- package/dist/viewers/image/decode.js +150 -0
- package/dist/viewers/image/index.d.ts +38 -0
- package/dist/viewers/image/index.js +749 -0
- package/dist/viewers/image/limits.d.ts +18 -0
- package/dist/viewers/image/limits.js +20 -0
- package/dist/viewers/image/styles.d.ts +1 -0
- package/dist/viewers/image/styles.js +119 -0
- package/dist/viewers/json/controller.d.ts +89 -0
- package/dist/viewers/json/controller.js +411 -0
- package/dist/viewers/json/index.d.ts +25 -0
- package/dist/viewers/json/index.js +503 -0
- package/dist/viewers/json/styles.d.ts +1 -0
- package/dist/viewers/json/styles.js +346 -0
- package/dist/viewers/json/transforms.d.ts +27 -0
- package/dist/viewers/json/transforms.js +253 -0
- package/dist/viewers/jsonl/controller.d.ts +48 -0
- package/dist/viewers/jsonl/controller.js +136 -0
- package/dist/viewers/jsonl/index.d.ts +22 -0
- package/dist/viewers/jsonl/index.js +184 -0
- package/dist/viewers/jsonl/styles.d.ts +1 -0
- package/dist/viewers/jsonl/styles.js +1 -0
- package/dist/viewers/markdown/controller.d.ts +50 -0
- package/dist/viewers/markdown/controller.js +62 -0
- package/dist/viewers/markdown/index.d.ts +64 -0
- package/dist/viewers/markdown/index.js +425 -0
- package/dist/viewers/markdown/math.d.ts +21 -0
- package/dist/viewers/markdown/math.js +100 -0
- package/dist/viewers/markdown/self-loading.d.ts +5 -0
- package/dist/viewers/markdown/self-loading.js +48 -0
- package/dist/viewers/markdown/styles.d.ts +1 -0
- package/dist/viewers/markdown/styles.js +11 -0
- package/dist/viewers/mat/index.d.ts +19 -0
- package/dist/viewers/mat/index.js +113 -0
- package/dist/viewers/mat/styles.d.ts +1 -0
- package/dist/viewers/mat/styles.js +3 -0
- package/dist/viewers/media.d.ts +9 -0
- package/dist/viewers/media.js +77 -0
- package/dist/viewers/mermaid/index.d.ts +17 -0
- package/dist/viewers/mermaid/index.js +6 -0
- package/dist/viewers/mermaid/self-loading.d.ts +2 -0
- package/dist/viewers/mermaid/self-loading.js +2 -0
- package/dist/viewers/mf4/index.d.ts +15 -0
- package/dist/viewers/mf4/index.js +5 -0
- package/dist/viewers/parquet/controller.d.ts +37 -0
- package/dist/viewers/parquet/controller.js +26 -0
- package/dist/viewers/parquet/index.d.ts +23 -0
- package/dist/viewers/parquet/index.js +134 -0
- package/dist/viewers/parquet/styles.d.ts +1 -0
- package/dist/viewers/parquet/styles.js +3 -0
- package/dist/viewers/pcap/index.d.ts +15 -0
- package/dist/viewers/pcap/index.js +5 -0
- package/dist/viewers/pcapng/index.d.ts +15 -0
- package/dist/viewers/pcapng/index.js +5 -0
- package/dist/viewers/pdf/controller.d.ts +120 -0
- package/dist/viewers/pdf/controller.js +191 -0
- package/dist/viewers/pdf/editing.d.ts +26 -0
- package/dist/viewers/pdf/editing.js +265 -0
- package/dist/viewers/pdf/index.d.ts +88 -0
- package/dist/viewers/pdf/index.js +1400 -0
- package/dist/viewers/pdf/self-loading.d.ts +6 -0
- package/dist/viewers/pdf/self-loading.js +14 -0
- package/dist/viewers/pdf/styles.d.ts +1 -0
- package/dist/viewers/pdf/styles.js +555 -0
- package/dist/viewers/plantuml/index.d.ts +17 -0
- package/dist/viewers/plantuml/index.js +6 -0
- package/dist/viewers/plantuml/self-loading.d.ts +2 -0
- package/dist/viewers/plantuml/self-loading.js +2 -0
- package/dist/viewers/ppt/chart.d.ts +8 -0
- package/dist/viewers/ppt/chart.js +83 -0
- package/dist/viewers/ppt/controller.d.ts +34 -0
- package/dist/viewers/ppt/controller.js +27 -0
- package/dist/viewers/ppt/index.d.ts +33 -0
- package/dist/viewers/ppt/index.js +101 -0
- package/dist/viewers/ppt/render.d.ts +5 -0
- package/dist/viewers/ppt/render.js +144 -0
- package/dist/viewers/ppt/self-loading.d.ts +9 -0
- package/dist/viewers/ppt/self-loading.js +4 -0
- package/dist/viewers/ppt/styles.d.ts +1 -0
- package/dist/viewers/ppt/styles.js +1 -0
- package/dist/viewers/proto/index.d.ts +17 -0
- package/dist/viewers/proto/index.js +170 -0
- package/dist/viewers/proto/styles.d.ts +1 -0
- package/dist/viewers/proto/styles.js +2 -0
- package/dist/viewers/psd/index.d.ts +27 -0
- package/dist/viewers/psd/index.js +39 -0
- package/dist/viewers/psd/self-loading.d.ts +10 -0
- package/dist/viewers/psd/self-loading.js +20 -0
- package/dist/viewers/psd/styles.d.ts +1 -0
- package/dist/viewers/psd/styles.js +1 -0
- package/dist/viewers/reqif/index.d.ts +15 -0
- package/dist/viewers/reqif/index.js +5 -0
- package/dist/viewers/shapefile/index.d.ts +35 -0
- package/dist/viewers/shapefile/index.js +336 -0
- package/dist/viewers/shapefile/self-loading.d.ts +7 -0
- package/dist/viewers/shapefile/self-loading.js +32 -0
- package/dist/viewers/shapefile/styles.d.ts +1 -0
- package/dist/viewers/shapefile/styles.js +28 -0
- package/dist/viewers/stp/index.d.ts +15 -0
- package/dist/viewers/stp/index.js +5 -0
- package/dist/viewers/structured-styles.d.ts +1 -0
- package/dist/viewers/structured-styles.js +1 -0
- package/dist/viewers/structured.d.ts +36 -0
- package/dist/viewers/structured.js +138 -0
- package/dist/viewers/toml/controller.d.ts +38 -0
- package/dist/viewers/toml/controller.js +61 -0
- package/dist/viewers/toml/index.d.ts +18 -0
- package/dist/viewers/toml/index.js +34 -0
- package/dist/viewers/types.d.ts +36 -0
- package/dist/viewers/types.js +9 -0
- package/dist/viewers/video/controller.d.ts +29 -0
- package/dist/viewers/video/controller.js +54 -0
- package/dist/viewers/video/index.d.ts +18 -0
- package/dist/viewers/video/index.js +305 -0
- package/dist/viewers/video/styles.d.ts +1 -0
- package/dist/viewers/video/styles.js +79 -0
- package/dist/viewers/word/controller.d.ts +19 -0
- package/dist/viewers/word/controller.js +22 -0
- package/dist/viewers/word/docx-preprocess.d.ts +63 -0
- package/dist/viewers/word/docx-preprocess.js +136 -0
- package/dist/viewers/word/index.d.ts +32 -0
- package/dist/viewers/word/index.js +187 -0
- package/dist/viewers/word/normalize-docx.d.ts +2 -0
- package/dist/viewers/word/normalize-docx.js +50 -0
- package/dist/viewers/word/paginate.d.ts +1 -0
- package/dist/viewers/word/paginate.js +1035 -0
- package/dist/viewers/word/self-loading.d.ts +2 -0
- package/dist/viewers/word/self-loading.js +7 -0
- package/dist/viewers/word/styles.d.ts +1 -0
- package/dist/viewers/word/styles.js +686 -0
- package/dist/viewers/yaml/controller.d.ts +42 -0
- package/dist/viewers/yaml/controller.js +41 -0
- package/dist/viewers/yaml/index.d.ts +21 -0
- package/dist/viewers/yaml/index.js +20 -0
- package/package.json +375 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// ImageController — the behavior-controller layer for the image viewer
|
|
2
|
+
// (DESIGN.md §3-②, docs/viewers/image.md §2 편집 모델). Pure state machine:
|
|
3
|
+
// no DOM, no host services, no ImageBitmap. The DOM renderer (mount.ts) and a
|
|
4
|
+
// future web (Vue) host consume the same controller so interaction semantics
|
|
5
|
+
// and the conformance kit's "action sequence → normalized state" stay shared.
|
|
6
|
+
//
|
|
7
|
+
// Coordinates: every annotation is stored in *source pixel* coordinates. View
|
|
8
|
+
// transform (zoom/fit) is display-only and never mutates annotation geometry;
|
|
9
|
+
// rotation/flip/filter are edits that the exporter composes into the output
|
|
10
|
+
// (image.md §2, §5 export 기준선).
|
|
11
|
+
/** Zoom range and step (image.md §3): 10 %–500 %, 25 % increments. */
|
|
12
|
+
export const MIN_ZOOM = 0.1;
|
|
13
|
+
export const MAX_ZOOM = 5;
|
|
14
|
+
export const ZOOM_STEP = 0.25;
|
|
15
|
+
/** Undo history depth cap (image.md §4 maxUndoEntries). */
|
|
16
|
+
export const MAX_UNDO_ENTRIES = 100;
|
|
17
|
+
export const DEFAULT_STYLE = {
|
|
18
|
+
color: '#e23b3b',
|
|
19
|
+
fillOpacity: 0,
|
|
20
|
+
borderOpacity: 1,
|
|
21
|
+
fontSize: 16,
|
|
22
|
+
brushSize: 4
|
|
23
|
+
};
|
|
24
|
+
export const NEUTRAL_FILTER = {
|
|
25
|
+
brightness: 100,
|
|
26
|
+
contrast: 100,
|
|
27
|
+
saturation: 100,
|
|
28
|
+
grayscale: 0
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Preset filter values. NOTE (image.md §3, I-preset): the exact numbers must be
|
|
32
|
+
* pinned to the existing VS Code / Obsidian fixtures during migration — these
|
|
33
|
+
* are the working defaults until those goldens land.
|
|
34
|
+
*/
|
|
35
|
+
export const FILTER_PRESETS = {
|
|
36
|
+
original: { brightness: 100, contrast: 100, saturation: 100, grayscale: 0 },
|
|
37
|
+
bright: { brightness: 120, contrast: 105, saturation: 105, grayscale: 0 },
|
|
38
|
+
dark: { brightness: 80, contrast: 110, saturation: 100, grayscale: 0 },
|
|
39
|
+
vintage: { brightness: 110, contrast: 90, saturation: 80, grayscale: 20 },
|
|
40
|
+
bw: { brightness: 100, contrast: 100, saturation: 0, grayscale: 100 }
|
|
41
|
+
};
|
|
42
|
+
export const DEFAULT_GRID = {
|
|
43
|
+
visible: false,
|
|
44
|
+
mode: 'cell-size',
|
|
45
|
+
cellWidth: 32,
|
|
46
|
+
cellHeight: 32,
|
|
47
|
+
rows: 4,
|
|
48
|
+
cols: 4
|
|
49
|
+
};
|
|
50
|
+
let idCounter = 0;
|
|
51
|
+
/** Deterministic monotonic ids so conformance snapshots stay comparable. */
|
|
52
|
+
function nextId() {
|
|
53
|
+
return `a${idCounter++}`;
|
|
54
|
+
}
|
|
55
|
+
function clamp(value, lo, hi) {
|
|
56
|
+
return Math.min(hi, Math.max(lo, value));
|
|
57
|
+
}
|
|
58
|
+
/** Snap zoom to the 25 % grid within range (image.md §3). */
|
|
59
|
+
export function normalizeZoom(zoom) {
|
|
60
|
+
const snapped = Math.round(zoom / ZOOM_STEP) * ZOOM_STEP;
|
|
61
|
+
return clamp(Number(snapped.toFixed(4)), MIN_ZOOM, MAX_ZOOM);
|
|
62
|
+
}
|
|
63
|
+
export function filterToCss(filter) {
|
|
64
|
+
return (`brightness(${filter.brightness}%) contrast(${filter.contrast}%) ` +
|
|
65
|
+
`saturate(${filter.saturation}%) grayscale(${filter.grayscale}%)`);
|
|
66
|
+
}
|
|
67
|
+
function initialEdit() {
|
|
68
|
+
return {
|
|
69
|
+
rotation: 0,
|
|
70
|
+
flipH: false,
|
|
71
|
+
flipV: false,
|
|
72
|
+
filter: { ...NEUTRAL_FILTER },
|
|
73
|
+
preset: 'original',
|
|
74
|
+
grid: { ...DEFAULT_GRID },
|
|
75
|
+
annotations: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function cloneEdit(edit) {
|
|
79
|
+
return {
|
|
80
|
+
rotation: edit.rotation,
|
|
81
|
+
flipH: edit.flipH,
|
|
82
|
+
flipV: edit.flipV,
|
|
83
|
+
filter: { ...edit.filter },
|
|
84
|
+
preset: edit.preset,
|
|
85
|
+
grid: { ...edit.grid },
|
|
86
|
+
annotations: edit.annotations.map(cloneAnnotation)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function cloneAnnotation(a) {
|
|
90
|
+
if (a.type === 'stroke') {
|
|
91
|
+
return { ...a, points: a.points.map((p) => ({ ...p })), style: { ...a.style } };
|
|
92
|
+
}
|
|
93
|
+
return { ...a, style: { ...a.style } };
|
|
94
|
+
}
|
|
95
|
+
export function createImageController() {
|
|
96
|
+
const listeners = new Set();
|
|
97
|
+
let status = 'ok';
|
|
98
|
+
let failure = null;
|
|
99
|
+
let diagnostics = [];
|
|
100
|
+
let meta = null;
|
|
101
|
+
let editingEnabled = true;
|
|
102
|
+
let zoom = 1;
|
|
103
|
+
let fitMode = true;
|
|
104
|
+
let edit = initialEdit();
|
|
105
|
+
let tool = 'select';
|
|
106
|
+
let selectedId = null;
|
|
107
|
+
let style = { ...DEFAULT_STYLE };
|
|
108
|
+
// Cursor-based linear history (image.md I4): undo only in v1, but the model
|
|
109
|
+
// supports a future redo without a refactor. history[0] is the initial edit.
|
|
110
|
+
const history = [cloneEdit(edit)];
|
|
111
|
+
let historyIndex = 0;
|
|
112
|
+
function emit() {
|
|
113
|
+
const snapshot = buildState();
|
|
114
|
+
for (const listener of listeners)
|
|
115
|
+
listener(snapshot);
|
|
116
|
+
}
|
|
117
|
+
function buildState() {
|
|
118
|
+
return {
|
|
119
|
+
status,
|
|
120
|
+
failure,
|
|
121
|
+
diagnostics,
|
|
122
|
+
meta,
|
|
123
|
+
editingEnabled,
|
|
124
|
+
zoom,
|
|
125
|
+
fitMode,
|
|
126
|
+
rotation: edit.rotation,
|
|
127
|
+
flipH: edit.flipH,
|
|
128
|
+
flipV: edit.flipV,
|
|
129
|
+
filter: { ...edit.filter },
|
|
130
|
+
preset: edit.preset,
|
|
131
|
+
grid: { ...edit.grid },
|
|
132
|
+
annotations: edit.annotations.map(cloneAnnotation),
|
|
133
|
+
tool,
|
|
134
|
+
selectedId,
|
|
135
|
+
style: { ...style },
|
|
136
|
+
dirty: historyIndex !== 0,
|
|
137
|
+
canUndo: historyIndex > 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Commit the current `edit` as a new history entry (image.md §4: drop the
|
|
141
|
+
* oldest past the cap; a new edit truncates any undone tail). */
|
|
142
|
+
function pushHistory() {
|
|
143
|
+
history.splice(historyIndex + 1); // drop redo branch (none in v1 UI)
|
|
144
|
+
history.push(cloneEdit(edit));
|
|
145
|
+
historyIndex = history.length - 1;
|
|
146
|
+
while (history.length > MAX_UNDO_ENTRIES + 1) {
|
|
147
|
+
history.shift();
|
|
148
|
+
historyIndex--;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function restore(index) {
|
|
152
|
+
historyIndex = index;
|
|
153
|
+
edit = cloneEdit(history[index]);
|
|
154
|
+
// A restored document may no longer contain the selected annotation.
|
|
155
|
+
if (selectedId && !edit.annotations.some((a) => a.id === selectedId)) {
|
|
156
|
+
selectedId = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Grid inputs keep the previous valid value on 0/negative/NaN (image.md §3). */
|
|
160
|
+
function normalizeGridValue(value) {
|
|
161
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
162
|
+
return null;
|
|
163
|
+
return Math.floor(value);
|
|
164
|
+
}
|
|
165
|
+
function dispatch(action) {
|
|
166
|
+
switch (action.type) {
|
|
167
|
+
// ---- view (display-only) ----------------------------------------
|
|
168
|
+
case 'set-zoom':
|
|
169
|
+
zoom = normalizeZoom(action.zoom);
|
|
170
|
+
fitMode = false;
|
|
171
|
+
break;
|
|
172
|
+
case 'zoom-in':
|
|
173
|
+
zoom = normalizeZoom(zoom + ZOOM_STEP);
|
|
174
|
+
fitMode = false;
|
|
175
|
+
break;
|
|
176
|
+
case 'zoom-out':
|
|
177
|
+
zoom = normalizeZoom(zoom - ZOOM_STEP);
|
|
178
|
+
fitMode = false;
|
|
179
|
+
break;
|
|
180
|
+
case 'fit':
|
|
181
|
+
fitMode = true;
|
|
182
|
+
break;
|
|
183
|
+
case 'actual-size':
|
|
184
|
+
zoom = 1;
|
|
185
|
+
fitMode = false;
|
|
186
|
+
break;
|
|
187
|
+
// ---- edits (undoable) -------------------------------------------
|
|
188
|
+
case 'rotate-cw':
|
|
189
|
+
edit.rotation = ((edit.rotation + 90) % 360);
|
|
190
|
+
pushHistory();
|
|
191
|
+
break;
|
|
192
|
+
case 'flip-h':
|
|
193
|
+
edit.flipH = !edit.flipH;
|
|
194
|
+
pushHistory();
|
|
195
|
+
break;
|
|
196
|
+
case 'flip-v':
|
|
197
|
+
edit.flipV = !edit.flipV;
|
|
198
|
+
pushHistory();
|
|
199
|
+
break;
|
|
200
|
+
case 'reset':
|
|
201
|
+
// Transform, filter, grid, annotation and selection all return to
|
|
202
|
+
// initial (image.md §3). Zoom/fit reset too (display-only).
|
|
203
|
+
edit = initialEdit();
|
|
204
|
+
zoom = 1;
|
|
205
|
+
fitMode = true;
|
|
206
|
+
selectedId = null;
|
|
207
|
+
pushHistory();
|
|
208
|
+
break;
|
|
209
|
+
case 'set-filter':
|
|
210
|
+
edit.filter = {
|
|
211
|
+
brightness: clamp(action.filter.brightness ?? edit.filter.brightness, 0, 200),
|
|
212
|
+
contrast: clamp(action.filter.contrast ?? edit.filter.contrast, 0, 200),
|
|
213
|
+
saturation: clamp(action.filter.saturation ?? edit.filter.saturation, 0, 200),
|
|
214
|
+
grayscale: clamp(action.filter.grayscale ?? edit.filter.grayscale, 0, 100)
|
|
215
|
+
};
|
|
216
|
+
edit.preset = null;
|
|
217
|
+
pushHistory();
|
|
218
|
+
break;
|
|
219
|
+
case 'set-preset':
|
|
220
|
+
edit.filter = { ...FILTER_PRESETS[action.preset] };
|
|
221
|
+
edit.preset = action.preset;
|
|
222
|
+
pushHistory();
|
|
223
|
+
break;
|
|
224
|
+
case 'toggle-grid':
|
|
225
|
+
edit.grid = { ...edit.grid, visible: !edit.grid.visible };
|
|
226
|
+
pushHistory();
|
|
227
|
+
break;
|
|
228
|
+
case 'set-grid-mode':
|
|
229
|
+
edit.grid = { ...edit.grid, mode: action.mode };
|
|
230
|
+
pushHistory();
|
|
231
|
+
break;
|
|
232
|
+
case 'set-grid-value': {
|
|
233
|
+
const normalized = normalizeGridValue(action.value);
|
|
234
|
+
if (normalized === null)
|
|
235
|
+
return; // keep previous valid value; no history
|
|
236
|
+
edit.grid = { ...edit.grid, [action.key]: normalized };
|
|
237
|
+
pushHistory();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
// ---- annotations -------------------------------------------------
|
|
241
|
+
case 'set-tool':
|
|
242
|
+
tool = action.tool;
|
|
243
|
+
break;
|
|
244
|
+
case 'set-style':
|
|
245
|
+
style = { ...style, ...action.style };
|
|
246
|
+
if (selectedId) {
|
|
247
|
+
const target = edit.annotations.find((a) => a.id === selectedId);
|
|
248
|
+
if (target) {
|
|
249
|
+
target.style = { ...target.style, ...action.style };
|
|
250
|
+
pushHistory();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case 'add-annotation':
|
|
255
|
+
edit.annotations = [...edit.annotations, cloneAnnotation(action.annotation)];
|
|
256
|
+
selectedId = action.annotation.id;
|
|
257
|
+
pushHistory();
|
|
258
|
+
break;
|
|
259
|
+
case 'select-annotation':
|
|
260
|
+
selectedId = action.id;
|
|
261
|
+
break; // selection is not an edit
|
|
262
|
+
case 'move-annotation': {
|
|
263
|
+
if (!selectedId)
|
|
264
|
+
return;
|
|
265
|
+
const target = edit.annotations.find((a) => a.id === selectedId);
|
|
266
|
+
if (!target)
|
|
267
|
+
return;
|
|
268
|
+
edit.annotations = edit.annotations.map((a) => a.id === selectedId ? translate(a, action.dx, action.dy) : a);
|
|
269
|
+
pushHistory();
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'update-annotation': {
|
|
273
|
+
const target = edit.annotations.find((a) => a.id === action.id);
|
|
274
|
+
if (!target || target.type !== 'text')
|
|
275
|
+
return;
|
|
276
|
+
edit.annotations = edit.annotations.map((a) => a.id === action.id && a.type === 'text' ? { ...a, ...action.patch } : a);
|
|
277
|
+
pushHistory();
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 'delete-annotation': {
|
|
281
|
+
const id = action.id ?? selectedId;
|
|
282
|
+
if (!id || !edit.annotations.some((a) => a.id === id))
|
|
283
|
+
return;
|
|
284
|
+
edit.annotations = edit.annotations.filter((a) => a.id !== id);
|
|
285
|
+
if (selectedId === id)
|
|
286
|
+
selectedId = null;
|
|
287
|
+
pushHistory();
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
// ---- history -----------------------------------------------------
|
|
291
|
+
case 'undo':
|
|
292
|
+
if (historyIndex === 0)
|
|
293
|
+
return;
|
|
294
|
+
restore(historyIndex - 1);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
emit();
|
|
298
|
+
}
|
|
299
|
+
function setDocument(input) {
|
|
300
|
+
status = input.status;
|
|
301
|
+
failure = input.failure ?? null;
|
|
302
|
+
diagnostics = input.diagnostics ?? [];
|
|
303
|
+
meta = input.meta ?? null;
|
|
304
|
+
editingEnabled = input.editingEnabled ?? input.status !== 'failed';
|
|
305
|
+
emit();
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
get state() {
|
|
309
|
+
return buildState();
|
|
310
|
+
},
|
|
311
|
+
dispatch,
|
|
312
|
+
subscribe(listener) {
|
|
313
|
+
listeners.add(listener);
|
|
314
|
+
return () => listeners.delete(listener);
|
|
315
|
+
},
|
|
316
|
+
filterCss() {
|
|
317
|
+
return filterToCss(edit.filter);
|
|
318
|
+
},
|
|
319
|
+
setDocument
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/** Shift every coordinate of an annotation by (dx, dy) in source pixels. */
|
|
323
|
+
function translate(a, dx, dy) {
|
|
324
|
+
switch (a.type) {
|
|
325
|
+
case 'line':
|
|
326
|
+
return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
|
|
327
|
+
case 'stroke':
|
|
328
|
+
return { ...a, points: a.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) };
|
|
329
|
+
default:
|
|
330
|
+
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Factory helper for the renderer: build an annotation with a fresh id. */
|
|
334
|
+
export function makeAnnotation(spec) {
|
|
335
|
+
return { id: nextId(), ...spec };
|
|
336
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ParseResult } from '../../parsers/types.js';
|
|
2
|
+
import type { ImageMeta } from './controller.js';
|
|
3
|
+
export type ImageMime = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/bmp' | 'image/webp' | 'image/svg+xml';
|
|
4
|
+
/** A decoded document is just metadata — the drawable lives separately so the
|
|
5
|
+
* controller stays DOM-free (image.md §2). */
|
|
6
|
+
export interface ImageDocument extends ImageMeta {
|
|
7
|
+
}
|
|
8
|
+
export type ImageLoadResult = ParseResult<ImageDocument>;
|
|
9
|
+
/**
|
|
10
|
+
* Byte-level MIME sniff (image.md §1). Returns null when the bytes match no
|
|
11
|
+
* known raster signature and the content is not SVG text — the caller then
|
|
12
|
+
* declines the image viewer (extension/byte conflict → fallback).
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectImageMime(data: Uint8Array, fileName: string): ImageMime | null;
|
|
15
|
+
/**
|
|
16
|
+
* SVG text probe (image.md §1): scan the first ~1 KiB for an `<svg` root while
|
|
17
|
+
* rejecting `<html` (a document, not an image). BOM / XML declaration /
|
|
18
|
+
* comments before the root are tolerated.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isSvgText(data: Uint8Array): boolean;
|
|
21
|
+
/** Cheap animation sniff for the first-frame policy (image.md I2). Conservative:
|
|
22
|
+
* a false negative just means we show a static frame without the badge. */
|
|
23
|
+
export declare function isAnimated(data: Uint8Array, mime: ImageMime): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Enforce the input-byte cap before touching a decoder (image.md §4). Returns a
|
|
26
|
+
* `failed(limit-exceeded)` result, or null when within budget.
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkInputLimit(data: Uint8Array, maxInputBytes?: number): ImageLoadResult | null;
|
|
29
|
+
/** A decoded image plus its disposable browser resources (mount owns cleanup). */
|
|
30
|
+
export interface DecodedImage {
|
|
31
|
+
load: ImageLoadResult;
|
|
32
|
+
/** Drawable for canvas composition; absent on failure. */
|
|
33
|
+
source?: CanvasImageSource & {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
};
|
|
37
|
+
/** Revoked on dispose. */
|
|
38
|
+
objectUrl?: string;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decode input bytes into a drawable + normalized ImageLoadResult. Never throws
|
|
43
|
+
* on input-caused failure (parser determinism rule): unsupported/corrupt →
|
|
44
|
+
* `invalid-format`/`corrupted`, oversize → `limit-exceeded`, no decoder →
|
|
45
|
+
* `missing-dependency`. Honors `signal` and cleans up on abort.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decodeImage(input: {
|
|
48
|
+
fileName: string;
|
|
49
|
+
data: Uint8Array;
|
|
50
|
+
}, options?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
maxInputBytes?: number;
|
|
53
|
+
maxDecodedPixels?: number;
|
|
54
|
+
}): Promise<DecodedImage>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Image decoding + detection (docs/viewers/image.md §1, §2, §4). The pure
|
|
2
|
+
// helpers (MIME sniffing, SVG probing, animation + limit checks) are engine-
|
|
3
|
+
// independent and unit-tested; the async `decodeImage` uses browser decoders
|
|
4
|
+
// (createImageBitmap / <img> + blob URL) and is exercised by the mount smoke.
|
|
5
|
+
//
|
|
6
|
+
// SVG isolation (image.md I1, §5 보안): SVG is decoded only through an
|
|
7
|
+
// `image/svg+xml` blob URL into <img>/createImageBitmap — never injected into
|
|
8
|
+
// the document DOM. img-loaded SVG neither runs scripts nor fetches external
|
|
9
|
+
// resources, so the decode path itself is the isolation boundary.
|
|
10
|
+
import { IMAGE_LIMITS } from './limits.js';
|
|
11
|
+
const startsWith = (data, sig, offset = 0) => data.length >= offset + sig.length && sig.every((b, i) => data[offset + i] === b);
|
|
12
|
+
/**
|
|
13
|
+
* Byte-level MIME sniff (image.md §1). Returns null when the bytes match no
|
|
14
|
+
* known raster signature and the content is not SVG text — the caller then
|
|
15
|
+
* declines the image viewer (extension/byte conflict → fallback).
|
|
16
|
+
*/
|
|
17
|
+
export function detectImageMime(data, fileName) {
|
|
18
|
+
if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
19
|
+
return 'image/png';
|
|
20
|
+
if (startsWith(data, [0xff, 0xd8, 0xff]))
|
|
21
|
+
return 'image/jpeg';
|
|
22
|
+
if (startsWith(data, [0x47, 0x49, 0x46, 0x38]))
|
|
23
|
+
return 'image/gif';
|
|
24
|
+
if (startsWith(data, [0x42, 0x4d]))
|
|
25
|
+
return 'image/bmp';
|
|
26
|
+
if (startsWith(data, [0x52, 0x49, 0x46, 0x46]) && startsWith(data, [0x57, 0x45, 0x42, 0x50], 8))
|
|
27
|
+
return 'image/webp';
|
|
28
|
+
if (isSvgText(data) && /\.svg$/i.test(fileName))
|
|
29
|
+
return 'image/svg+xml';
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* SVG text probe (image.md §1): scan the first ~1 KiB for an `<svg` root while
|
|
34
|
+
* rejecting `<html` (a document, not an image). BOM / XML declaration /
|
|
35
|
+
* comments before the root are tolerated.
|
|
36
|
+
*/
|
|
37
|
+
export function isSvgText(data) {
|
|
38
|
+
const head = new TextDecoder('utf-8', { fatal: false })
|
|
39
|
+
.decode(data.subarray(0, 1024))
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
if (head.includes('<html'))
|
|
42
|
+
return false;
|
|
43
|
+
return head.includes('<svg');
|
|
44
|
+
}
|
|
45
|
+
/** Cheap animation sniff for the first-frame policy (image.md I2). Conservative:
|
|
46
|
+
* a false negative just means we show a static frame without the badge. */
|
|
47
|
+
export function isAnimated(data, mime) {
|
|
48
|
+
if (mime === 'image/gif') {
|
|
49
|
+
// More than one Graphic Control Extension (0x21 0xF9) ⇒ multiple frames.
|
|
50
|
+
let count = 0;
|
|
51
|
+
for (let i = 0; i + 1 < data.length; i++) {
|
|
52
|
+
if (data[i] === 0x21 && data[i + 1] === 0xf9 && ++count > 1)
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (mime === 'image/webp') {
|
|
58
|
+
// 'ANIM' chunk marks an animated WebP.
|
|
59
|
+
for (let i = 12; i + 3 < data.length && i < 4096; i++) {
|
|
60
|
+
if (data[i] === 0x41 && data[i + 1] === 0x4e && data[i + 2] === 0x49 && data[i + 3] === 0x4d)
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function fail(code, messageKey, args) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'failed',
|
|
69
|
+
failure: { code, retryable: false, messageKey, ...(args ? { args } : {}) },
|
|
70
|
+
diagnostics: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Enforce the input-byte cap before touching a decoder (image.md §4). Returns a
|
|
75
|
+
* `failed(limit-exceeded)` result, or null when within budget.
|
|
76
|
+
*/
|
|
77
|
+
export function checkInputLimit(data, maxInputBytes = IMAGE_LIMITS.maxInputBytes) {
|
|
78
|
+
if (data.byteLength > maxInputBytes) {
|
|
79
|
+
return fail('limit-exceeded', 'diag.limit-exceeded.input', { maxBytes: maxInputBytes });
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decode input bytes into a drawable + normalized ImageLoadResult. Never throws
|
|
85
|
+
* on input-caused failure (parser determinism rule): unsupported/corrupt →
|
|
86
|
+
* `invalid-format`/`corrupted`, oversize → `limit-exceeded`, no decoder →
|
|
87
|
+
* `missing-dependency`. Honors `signal` and cleans up on abort.
|
|
88
|
+
*/
|
|
89
|
+
export async function decodeImage(input, options = {}) {
|
|
90
|
+
const noop = { load: fail('invalid-format', 'diag.image.invalid-format'), dispose() { } };
|
|
91
|
+
const limitHit = checkInputLimit(input.data, options.maxInputBytes);
|
|
92
|
+
if (limitHit)
|
|
93
|
+
return { load: limitHit, dispose() { } };
|
|
94
|
+
const mime = detectImageMime(input.data, input.fileName);
|
|
95
|
+
if (!mime)
|
|
96
|
+
return noop;
|
|
97
|
+
if (typeof createImageBitmap !== 'function' || typeof Blob !== 'function') {
|
|
98
|
+
return { load: fail('missing-dependency', 'diag.image.invalid-format'), dispose() { } };
|
|
99
|
+
}
|
|
100
|
+
const blob = new Blob([input.data.slice().buffer], { type: mime });
|
|
101
|
+
let objectUrl;
|
|
102
|
+
let bitmap;
|
|
103
|
+
const dispose = () => {
|
|
104
|
+
bitmap?.close?.();
|
|
105
|
+
if (objectUrl && typeof URL !== 'undefined')
|
|
106
|
+
URL.revokeObjectURL(objectUrl);
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
// SVG and raster both go through blob decoding — SVG via <img> keeps it
|
|
110
|
+
// isolated (no DOM injection, no script execution).
|
|
111
|
+
bitmap = await createImageBitmap(blob);
|
|
112
|
+
if (options.signal?.aborted) {
|
|
113
|
+
dispose();
|
|
114
|
+
return { load: fail('aborted', 'diag.aborted'), dispose() { } };
|
|
115
|
+
}
|
|
116
|
+
const pixels = bitmap.width * bitmap.height;
|
|
117
|
+
const maxPixels = options.maxDecodedPixels ?? IMAGE_LIMITS.maxDecodedPixels;
|
|
118
|
+
if (pixels > maxPixels) {
|
|
119
|
+
// v1: no downscaler wired yet — decline rather than risk OOM (I6).
|
|
120
|
+
dispose();
|
|
121
|
+
return { load: fail('limit-exceeded', 'diag.image.limit-exceeded.pixels'), dispose() { } };
|
|
122
|
+
}
|
|
123
|
+
const animated = isAnimated(input.data, mime);
|
|
124
|
+
const diagnostics = [];
|
|
125
|
+
if (animated) {
|
|
126
|
+
diagnostics.push({
|
|
127
|
+
severity: 'info',
|
|
128
|
+
code: 'animated-first-frame',
|
|
129
|
+
messageKey: 'diag.image.animated-first-frame'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const doc = {
|
|
133
|
+
mime,
|
|
134
|
+
width: bitmap.width,
|
|
135
|
+
height: bitmap.height,
|
|
136
|
+
byteLength: input.data.byteLength,
|
|
137
|
+
animated
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
load: { status: animated ? 'partial' : 'ok', document: doc, diagnostics },
|
|
141
|
+
source: bitmap,
|
|
142
|
+
...(objectUrl ? { objectUrl } : {}),
|
|
143
|
+
dispose
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
dispose();
|
|
148
|
+
return { load: fail('corrupted', 'diag.image.corrupted'), dispose() { } };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FileSaveService, FileWritebackService, HostContext, ImageBackgroundRemovalService, ImageSpriteDetectionService } from '../../host/index.js';
|
|
2
|
+
import { type MountOptions, type ViewerHandle, type ViewerInput } from '../types.js';
|
|
3
|
+
export { createImageController } from './controller.js';
|
|
4
|
+
export type { ImageController, ImageViewState, ImageAction, Annotation } from './controller.js';
|
|
5
|
+
export { decodeImage, detectImageMime, isSvgText, isAnimated } from './decode.js';
|
|
6
|
+
export type { ImageLoadResult, ImageDocument, ImageMime } from './decode.js';
|
|
7
|
+
export { IMAGE_LIMITS, canvasEditable } from './limits.js';
|
|
8
|
+
export { imageViewerCss } from './styles.js';
|
|
9
|
+
/** Viewer metadata — single source for the registry codegen (DESIGN.md §7). */
|
|
10
|
+
export declare const IMAGE_VIEWER_META: {
|
|
11
|
+
id: string;
|
|
12
|
+
displayNameKey: string;
|
|
13
|
+
extensions: string[];
|
|
14
|
+
priority: number;
|
|
15
|
+
requiredServices: readonly [];
|
|
16
|
+
optionalServices: readonly ["save", "writeback", "backgroundRemoval", "spriteDetection"];
|
|
17
|
+
inputOwnership: "borrows";
|
|
18
|
+
};
|
|
19
|
+
export type ImageViewerContext = HostContext & {
|
|
20
|
+
save?: FileSaveService;
|
|
21
|
+
writeback?: FileWritebackService;
|
|
22
|
+
backgroundRemoval?: ImageBackgroundRemovalService;
|
|
23
|
+
spriteDetection?: ImageSpriteDetectionService;
|
|
24
|
+
};
|
|
25
|
+
export interface ImageViewerHandle extends ViewerHandle {
|
|
26
|
+
isDirty(): boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface ImageMountOptions extends MountOptions {
|
|
29
|
+
maxInputBytes?: number;
|
|
30
|
+
maxDecodedPixels?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Export file name (image.md I7): the edited image always saves as PNG, so the
|
|
34
|
+
* proposed name is coerced to `.png` regardless of the source extension. This
|
|
35
|
+
* is an intentional compatibility change — the UI announces it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function imageExportFileName(sourceName: string): string;
|
|
38
|
+
export declare function mountImageViewer(input: ViewerInput, container: HTMLElement, ctx: ImageViewerContext, options?: ImageMountOptions): Promise<ImageViewerHandle>;
|