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,2853 @@
|
|
|
1
|
+
// @ts-nocheck -- upstream VS Code parser predates exactOptionalPropertyTypes.
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
import { normalizeBuildSlidesArgs } from './pptBinaryBuildContext.js';
|
|
4
|
+
import { parseCfb as parseCfbContainer, parseRecords as parseRecordTree } from './pptBinaryContainer.js';
|
|
5
|
+
/**
|
|
6
|
+
* Standalone legacy .ppt parser.
|
|
7
|
+
*
|
|
8
|
+
* Scope (incremental roadmap):
|
|
9
|
+
* 1) CFB/OLE container parsing
|
|
10
|
+
* 2) Slide container indexing
|
|
11
|
+
* 3) Text extraction
|
|
12
|
+
* 4) Basic layout placement
|
|
13
|
+
* 5) Pictures stream extraction (JPEG/PNG)
|
|
14
|
+
* 6) Minimal style hints (title/bullets)
|
|
15
|
+
* 7) Extension-friendly internal pipeline
|
|
16
|
+
*/
|
|
17
|
+
export class PptBinaryParser {
|
|
18
|
+
static POINTS_TO_PX = 96 / 72;
|
|
19
|
+
static async parse(input) {
|
|
20
|
+
const buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
21
|
+
const cfb = this.parseCfb(buffer);
|
|
22
|
+
const docStream = cfb.getStream('PowerPoint Document');
|
|
23
|
+
if (!docStream) {
|
|
24
|
+
throw new Error('Invalid .ppt file: missing "PowerPoint Document" stream.');
|
|
25
|
+
}
|
|
26
|
+
const records = this.parseRecords(docStream, 0, docStream.length);
|
|
27
|
+
const slideRecords = this.collectSlideContainers(records);
|
|
28
|
+
const picturesStream = cfb.getStream('Pictures');
|
|
29
|
+
const pictures = this.extractPictures(picturesStream);
|
|
30
|
+
const picturesById = this.extractPicturesByBlipId(records, picturesStream, pictures);
|
|
31
|
+
const outlineTextByPersistId = this.extractOutlineTextByPersistId(records);
|
|
32
|
+
const defaultColorScheme = this.extractDocumentColorScheme(records);
|
|
33
|
+
const masterRecord = this.collectPrimaryMasterContainer(records);
|
|
34
|
+
const presentationMetrics = this.extractPresentationMetrics(records);
|
|
35
|
+
const widthPx = presentationMetrics?.widthPx ?? 960;
|
|
36
|
+
const heightPx = presentationMetrics?.heightPx ?? 720;
|
|
37
|
+
const slides = this.buildSlides(slideRecords, pictures, outlineTextByPersistId, defaultColorScheme, masterRecord, presentationMetrics, widthPx, heightPx, picturesById);
|
|
38
|
+
if (slides.length === 0) {
|
|
39
|
+
const fallbackTexts = this.extractLooseTexts(docStream).slice(0, 40);
|
|
40
|
+
if (fallbackTexts.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
slides: [{
|
|
43
|
+
slideNumber: 1,
|
|
44
|
+
widthPx,
|
|
45
|
+
heightPx,
|
|
46
|
+
backgroundColor: '#ffffff',
|
|
47
|
+
elements: fallbackTexts.map((text, idx) => ({
|
|
48
|
+
type: 'text',
|
|
49
|
+
x: 72,
|
|
50
|
+
y: 80 + idx * 34,
|
|
51
|
+
width: 816,
|
|
52
|
+
height: 30,
|
|
53
|
+
zIndex: idx,
|
|
54
|
+
isTitle: idx === 0,
|
|
55
|
+
paragraphs: [{
|
|
56
|
+
text,
|
|
57
|
+
level: 0,
|
|
58
|
+
bullet: idx > 0
|
|
59
|
+
}]
|
|
60
|
+
}))
|
|
61
|
+
}],
|
|
62
|
+
totalSlides: 1
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
slides,
|
|
68
|
+
totalSlides: slides.length
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
static extractPresentationMetrics(records) {
|
|
72
|
+
for (const record of records) {
|
|
73
|
+
if (record.recType === 1000 && record.children && record.children.length > 0) {
|
|
74
|
+
const documentAtom = record.children.find((child) => child.recType === 1001 && child.payload.length >= 8);
|
|
75
|
+
if (!documentAtom) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const rawWidth = documentAtom.payload.readUInt32LE(0);
|
|
79
|
+
const rawHeight = documentAtom.payload.readUInt32LE(4);
|
|
80
|
+
const widthPx = this.legacyCoordToPixels(rawWidth);
|
|
81
|
+
const heightPx = this.legacyCoordToPixels(rawHeight);
|
|
82
|
+
if (widthPx && heightPx) {
|
|
83
|
+
return { widthPx, heightPx, rawWidth, rawHeight };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (record.children && record.children.length > 0) {
|
|
87
|
+
const nestedSize = this.extractPresentationMetrics(record.children);
|
|
88
|
+
if (nestedSize) {
|
|
89
|
+
return nestedSize;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
static legacyCoordToPixels(value) {
|
|
96
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Many legacy .ppt decks store slide geometry in PowerPoint master units
|
|
100
|
+
// (for example 5760 x 4320 for a 10 x 7.5 inch slide), not points.
|
|
101
|
+
if (value >= 2000) {
|
|
102
|
+
return Math.max(1, Math.round(value / 6));
|
|
103
|
+
}
|
|
104
|
+
return Math.max(1, Math.round(value * this.POINTS_TO_PX));
|
|
105
|
+
}
|
|
106
|
+
static parseCfb(file) {
|
|
107
|
+
return parseCfbContainer(file);
|
|
108
|
+
}
|
|
109
|
+
static parseRecords(buffer, start, end) {
|
|
110
|
+
return parseRecordTree(buffer, start, end);
|
|
111
|
+
}
|
|
112
|
+
static collectSlideContainers(records) {
|
|
113
|
+
const discoveredSlides = [];
|
|
114
|
+
const visit = (list) => {
|
|
115
|
+
list.forEach((r) => {
|
|
116
|
+
// RT_Slide (commonly 1006)
|
|
117
|
+
if (r.recType === 1006) {
|
|
118
|
+
discoveredSlides.push(r);
|
|
119
|
+
}
|
|
120
|
+
if (r.children && r.children.length > 0) {
|
|
121
|
+
visit(r.children);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
visit(records);
|
|
126
|
+
const orderedPersistRefs = this.extractOrderedSlidePersistRefs(records);
|
|
127
|
+
if (orderedPersistRefs.length === 0) {
|
|
128
|
+
return discoveredSlides;
|
|
129
|
+
}
|
|
130
|
+
const slideByPersistRef = new Map();
|
|
131
|
+
discoveredSlides.forEach((slide) => {
|
|
132
|
+
if (!slideByPersistRef.has(slide.recInstance)) {
|
|
133
|
+
slideByPersistRef.set(slide.recInstance, slide);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const orderedSlides = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
orderedPersistRefs.forEach((persistRef) => {
|
|
139
|
+
const slide = slideByPersistRef.get(persistRef);
|
|
140
|
+
if (!slide || seen.has(slide)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
seen.add(slide);
|
|
144
|
+
orderedSlides.push(slide);
|
|
145
|
+
});
|
|
146
|
+
return orderedSlides.length > 0 ? orderedSlides : discoveredSlides;
|
|
147
|
+
}
|
|
148
|
+
static collectPrimaryMasterContainer(records) {
|
|
149
|
+
let found = null;
|
|
150
|
+
const visit = (list) => {
|
|
151
|
+
for (const record of list) {
|
|
152
|
+
if (record.recType === 1016 && !found) {
|
|
153
|
+
found = record;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (record.children && record.children.length > 0) {
|
|
157
|
+
visit(record.children);
|
|
158
|
+
if (found) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
visit(records);
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
static extractOrderedSlidePersistRefs(records) {
|
|
168
|
+
const refs = [];
|
|
169
|
+
const visit = (list) => {
|
|
170
|
+
list.forEach((record) => {
|
|
171
|
+
if (record.recType === 1000 && record.children && record.children.length > 0) {
|
|
172
|
+
record.children.forEach((child) => {
|
|
173
|
+
if (child.recType !== 4080 || !child.children || child.children.length === 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
child.children.forEach((entry) => {
|
|
177
|
+
if (entry.recType !== 1011 || entry.payload.length < 4) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
refs.push(entry.payload.readUInt32LE(0));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (record.children && record.children.length > 0) {
|
|
185
|
+
visit(record.children);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
visit(records);
|
|
190
|
+
return refs.filter((value) => Number.isFinite(value) && value > 0);
|
|
191
|
+
}
|
|
192
|
+
static buildSlides(slideRecords, pictures, outlineTextByPersistId, defaultColorScheme, masterRecordOrPresentationMetrics, presentationMetricsOrWidth, widthPxOrHeight, heightPxOrPicturesById, picturesByIdMaybe) {
|
|
193
|
+
const { masterRecord, presentationMetrics, widthPx, heightPx, picturesById } = normalizeBuildSlidesArgs(slideRecords, pictures, outlineTextByPersistId, defaultColorScheme, masterRecordOrPresentationMetrics, presentationMetricsOrWidth, widthPxOrHeight, heightPxOrPicturesById, picturesByIdMaybe);
|
|
194
|
+
const slides = [];
|
|
195
|
+
let sequentialPictureIndex = 0;
|
|
196
|
+
for (let i = 0; i < slideRecords.length; i++) {
|
|
197
|
+
const slideRecord = slideRecords[i];
|
|
198
|
+
const outlineBlocks = outlineTextByPersistId.get(slideRecord.recInstance) ?? [];
|
|
199
|
+
const colorScheme = this.extractSlideColorScheme(slideRecord) ?? defaultColorScheme;
|
|
200
|
+
const layout = this.extractSlideLayoutInfo(slideRecord);
|
|
201
|
+
const spContainerCounter = { value: 0 };
|
|
202
|
+
const visualSlots = this.decorateVisualSlotsWithPlaceholders(this.extractVisualSlotsFromRecord(slideRecord, spContainerCounter), layout);
|
|
203
|
+
// Reset counter so both extractions share the same DFS index space
|
|
204
|
+
spContainerCounter.value = 0;
|
|
205
|
+
const shapeTextGroups = this.decorateTextGroupsWithPlaceholders(this.extractShapeTextGroupsFromRecord(slideRecord, spContainerCounter), layout);
|
|
206
|
+
const directBlocks = this.extractTypedTextBlocksFromRecord(slideRecord);
|
|
207
|
+
const styledShapeBlocks = this.extractStyledTextBlocksFromShapes(slideRecord);
|
|
208
|
+
const styledBounds = styledShapeBlocks
|
|
209
|
+
.map((block) => block.bounds)
|
|
210
|
+
.filter((bounds) => !!bounds);
|
|
211
|
+
const shouldPreferGroupedShapeText = this.shouldPreferShapeTextGroups(outlineBlocks, shapeTextGroups);
|
|
212
|
+
const groupedShapeTextBlocks = shapeTextGroups.length > 0
|
|
213
|
+
? this.flattenTextGroups(shapeTextGroups)
|
|
214
|
+
: [];
|
|
215
|
+
// Enrich outline blocks with SpContainer bounds by matching content
|
|
216
|
+
const normalizedOutline = outlineBlocks.length > 0 ? this.normalizeTextBlocks(outlineBlocks) : [];
|
|
217
|
+
if (normalizedOutline.length > 0 && groupedShapeTextBlocks.length > 0) {
|
|
218
|
+
const usedGroupIndices = new Set();
|
|
219
|
+
normalizedOutline.forEach((block) => {
|
|
220
|
+
if (block.bounds) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const blockText = block.text.replace(/\s+/g, '');
|
|
224
|
+
const match = { block: null, index: -1, score: 0 };
|
|
225
|
+
groupedShapeTextBlocks.forEach((grouped, groupIndex) => {
|
|
226
|
+
if (usedGroupIndices.has(groupIndex)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const groupedText = grouped.text.replace(/\s+/g, '');
|
|
230
|
+
if (blockText === groupedText) {
|
|
231
|
+
match.block = grouped;
|
|
232
|
+
match.index = groupIndex;
|
|
233
|
+
match.score = 1;
|
|
234
|
+
}
|
|
235
|
+
else if (match.score < 0.5 && blockText.length > 4 && groupedText.length > 4) {
|
|
236
|
+
if (blockText.includes(groupedText) || groupedText.includes(blockText)) {
|
|
237
|
+
match.block = grouped;
|
|
238
|
+
match.index = groupIndex;
|
|
239
|
+
match.score = 0.5;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
if (match.block && match.index >= 0) {
|
|
244
|
+
block.bounds = match.block.bounds;
|
|
245
|
+
block.spContainerIndex = match.block.spContainerIndex;
|
|
246
|
+
if (!block.fillColor && match.block.fillColor) {
|
|
247
|
+
block.fillColor = match.block.fillColor;
|
|
248
|
+
}
|
|
249
|
+
if (!block.borderColor && match.block.borderColor) {
|
|
250
|
+
block.borderColor = match.block.borderColor;
|
|
251
|
+
}
|
|
252
|
+
if (match.block.borderWidthPx !== undefined && block.borderWidthPx === undefined) {
|
|
253
|
+
block.borderWidthPx = match.block.borderWidthPx;
|
|
254
|
+
}
|
|
255
|
+
usedGroupIndices.add(match.index);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const baseTextBlocks = normalizedOutline.length > 0 && !shouldPreferGroupedShapeText
|
|
260
|
+
? this.decorateTextBlocksWithPlaceholders(normalizedOutline, layout)
|
|
261
|
+
: groupedShapeTextBlocks.length > 0
|
|
262
|
+
? groupedShapeTextBlocks
|
|
263
|
+
: styledShapeBlocks.length > 0
|
|
264
|
+
? this.decorateTextBlocksWithPlaceholders(this.coalesceTextBlocksByBounds(this.normalizeTextBlocks(styledShapeBlocks)), layout)
|
|
265
|
+
: directBlocks.length > 0
|
|
266
|
+
? this.decorateTextBlocksWithPlaceholders(this.normalizeTextBlocks(directBlocks), layout)
|
|
267
|
+
: this.extractTextsFromRecord(slideRecord).slice(0, 24).map((text) => ({ text }));
|
|
268
|
+
const mergedTextBlocks = this.mergeStyledTextBlocks(baseTextBlocks, styledShapeBlocks, layout, widthPx, heightPx, presentationMetrics);
|
|
269
|
+
const provisionalTextBlocks = this.coalesceOverlappingTextBlocks(mergedTextBlocks, widthPx, heightPx, presentationMetrics);
|
|
270
|
+
const textBlocks = provisionalTextBlocks;
|
|
271
|
+
const elements = [];
|
|
272
|
+
const titleBlocks = textBlocks.filter((block) => {
|
|
273
|
+
const role = this.classifyPlaceholderType(block.placeholderType);
|
|
274
|
+
return role === 'title' || role === 'subtitle' || this.isLikelyTitleBlock(block, widthPx, heightPx, presentationMetrics);
|
|
275
|
+
});
|
|
276
|
+
const bodyBlocks = textBlocks.filter((block) => {
|
|
277
|
+
const role = this.classifyPlaceholderType(block.placeholderType);
|
|
278
|
+
return role !== 'title' && role !== 'subtitle' && !this.isLikelyTitleBlock(block, widthPx, heightPx, presentationMetrics);
|
|
279
|
+
});
|
|
280
|
+
const placedTextFrames = [];
|
|
281
|
+
const fixedElements = new Set();
|
|
282
|
+
const rightPanelFrames = this.computeRightPanelTextFrames(textBlocks, widthPx, heightPx, presentationMetrics);
|
|
283
|
+
if (textBlocks.length > 0) {
|
|
284
|
+
textBlocks.forEach((block, idx) => {
|
|
285
|
+
const placeholderRole = this.classifyPlaceholderType(block.placeholderType);
|
|
286
|
+
const isTitle = placeholderRole === 'title'
|
|
287
|
+
|| this.isLikelyTitleBlock(block, widthPx, heightPx, presentationMetrics)
|
|
288
|
+
|| (idx === 0
|
|
289
|
+
&& titleBlocks.length === 0
|
|
290
|
+
&& !block.bounds
|
|
291
|
+
&& block.text.length <= 60
|
|
292
|
+
&& !block.text.includes('\r'));
|
|
293
|
+
const defaultTextColor = isTitle
|
|
294
|
+
? colorScheme?.titleColor
|
|
295
|
+
: colorScheme?.textColor;
|
|
296
|
+
const effectiveFontSize = block.fontSizePx;
|
|
297
|
+
const paragraphs = this.createParagraphsFromText(block.text, this.isBulletTextType(block.textType, !isTitle) && placeholderRole !== 'subtitle', block.color ?? defaultTextColor, effectiveFontSize);
|
|
298
|
+
const roleIndex = isTitle
|
|
299
|
+
? titleBlocks.indexOf(block)
|
|
300
|
+
: Math.max(0, bodyBlocks.indexOf(block));
|
|
301
|
+
const frame = this.computeTextFrame(layout, placeholderRole === 'subtitle'
|
|
302
|
+
? 'subtitle'
|
|
303
|
+
: isTitle
|
|
304
|
+
? 'title'
|
|
305
|
+
: 'body', roleIndex, Math.max(1, titleBlocks.length), Math.max(1, bodyBlocks.length), widthPx, heightPx);
|
|
306
|
+
const candidateFrame = block.bounds
|
|
307
|
+
? this.normalizeBounds(block.bounds, widthPx, heightPx, presentationMetrics)
|
|
308
|
+
: frame;
|
|
309
|
+
const panelFrame = rightPanelFrames?.get(block);
|
|
310
|
+
const hasSpContainerBounds = block.bounds && block.spContainerIndex !== undefined;
|
|
311
|
+
const positionedFrame = panelFrame
|
|
312
|
+
? panelFrame
|
|
313
|
+
: hasSpContainerBounds
|
|
314
|
+
? candidateFrame
|
|
315
|
+
: block.bounds
|
|
316
|
+
? this.resolveTextFrame(candidateFrame, frame, isTitle, widthPx, heightPx, placedTextFrames)
|
|
317
|
+
: frame;
|
|
318
|
+
const estimatedLineHeight = 30;
|
|
319
|
+
const avgCharWidthPx = 16;
|
|
320
|
+
const bulletPrefixPx = paragraphs.some((p) => p.bullet) ? 30 : 0;
|
|
321
|
+
const containerWidth = Math.max(positionedFrame.width - 4 - bulletPrefixPx, 40);
|
|
322
|
+
const charsPerLine = Math.max(1, Math.floor(containerWidth / avgCharWidthPx));
|
|
323
|
+
const estimatedLines = paragraphs.reduce((sum, p) => {
|
|
324
|
+
const textLength = p.text?.length || 0;
|
|
325
|
+
return sum + Math.max(1, Math.ceil(textLength / charsPerLine));
|
|
326
|
+
}, 0);
|
|
327
|
+
const contentHeight = estimatedLines * estimatedLineHeight;
|
|
328
|
+
const height = hasSpContainerBounds
|
|
329
|
+
? positionedFrame.height
|
|
330
|
+
: isTitle
|
|
331
|
+
? Math.max(positionedFrame.height, 72, contentHeight)
|
|
332
|
+
: Math.max(positionedFrame.height, contentHeight, 36);
|
|
333
|
+
const el = {
|
|
334
|
+
type: 'text',
|
|
335
|
+
x: positionedFrame.x,
|
|
336
|
+
y: positionedFrame.y,
|
|
337
|
+
width: positionedFrame.width,
|
|
338
|
+
height,
|
|
339
|
+
zIndex: block.spContainerIndex ?? idx,
|
|
340
|
+
isTitle,
|
|
341
|
+
paragraphs,
|
|
342
|
+
fillColor: block.fillVisible === false ? undefined : block.fillColor,
|
|
343
|
+
borderColor: block.borderVisible === false ? undefined : block.borderColor,
|
|
344
|
+
borderWidthPx: block.borderVisible === false ? undefined : block.borderWidthPx
|
|
345
|
+
};
|
|
346
|
+
elements.push(el);
|
|
347
|
+
if (hasSpContainerBounds) {
|
|
348
|
+
fixedElements.add(el);
|
|
349
|
+
}
|
|
350
|
+
placedTextFrames.push(positionedFrame);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
this.resolveTextElementOverlaps(elements, heightPx, fixedElements);
|
|
354
|
+
// Minimal image support: use discovered picture/object frames when available.
|
|
355
|
+
const preferredSlots = visualSlots.filter((slot) => !!slot.bounds && (this.isVisualPlaceholder(slot.placeholderType) || slot.imageRefId !== undefined));
|
|
356
|
+
const slotsWithImageRefs = preferredSlots.filter((slot) => slot.imageRefId !== undefined);
|
|
357
|
+
const hasVisualPlaceholders = preferredSlots.some((slot) => this.isVisualPlaceholder(slot.placeholderType));
|
|
358
|
+
const isShapeHeavySlide = slotsWithImageRefs.length === 0 && !hasVisualPlaceholders && visualSlots.length > 8;
|
|
359
|
+
const boundedSlots = slotsWithImageRefs.length > 0
|
|
360
|
+
? this.dedupeVisualSlots(this.selectPreferredImageRefSlots(slotsWithImageRefs, picturesById))
|
|
361
|
+
: isShapeHeavySlide
|
|
362
|
+
? []
|
|
363
|
+
: this.dedupeVisualSlots((preferredSlots.length > 0
|
|
364
|
+
? preferredSlots
|
|
365
|
+
: visualSlots.filter((slot) => !!slot.bounds))
|
|
366
|
+
.filter((slot) => !this.isDecorativeImageSlot(slot, widthPx, heightPx)));
|
|
367
|
+
let imageSlots = this.selectImageSlotsForSlide(boundedSlots, widthPx, heightPx, false).filter((slot) => {
|
|
368
|
+
if (!slot.bounds || slot.imageRefId !== undefined || boundedSlots.length < 4) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
const aspectRatio = slot.bounds.width / Math.max(1, slot.bounds.height);
|
|
372
|
+
return !(aspectRatio > 4.5 && slot.bounds.y < 1000);
|
|
373
|
+
});
|
|
374
|
+
const usedImageSlots = new Set();
|
|
375
|
+
if (imageSlots.length > 0) {
|
|
376
|
+
let fallbackImageIndex = 0;
|
|
377
|
+
const sequentialBaseIndex = sequentialPictureIndex;
|
|
378
|
+
let highestPictureIndexUsed = sequentialPictureIndex - 1;
|
|
379
|
+
imageSlots.forEach((slot) => {
|
|
380
|
+
const img = (slot.imageRefId !== undefined
|
|
381
|
+
? picturesById?.get(slot.imageRefId)
|
|
382
|
+
: undefined) ?? pictures[sequentialBaseIndex + fallbackImageIndex++];
|
|
383
|
+
if (!img || !slot.bounds) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const scaledImageFrame = this.normalizeBounds(slot.bounds, widthPx, heightPx, presentationMetrics);
|
|
387
|
+
const imageFrame = this.adjustImageFrameForTextColumns(this.adjustLegacyImageFrame(slot, scaledImageFrame, widthPx, heightPx, false), elements, widthPx, heightPx);
|
|
388
|
+
elements.push({
|
|
389
|
+
type: 'image',
|
|
390
|
+
x: imageFrame.x,
|
|
391
|
+
y: imageFrame.y,
|
|
392
|
+
width: imageFrame.width,
|
|
393
|
+
height: imageFrame.height,
|
|
394
|
+
zIndex: slot.spContainerIndex ?? (100 + elements.length),
|
|
395
|
+
src: `data:${img.mime};base64,${img.base64}`
|
|
396
|
+
});
|
|
397
|
+
usedImageSlots.add(slot);
|
|
398
|
+
if (typeof img.pictureIndex === 'number' && img.pictureIndex >= 0) {
|
|
399
|
+
highestPictureIndexUsed = Math.max(highestPictureIndexUsed, img.pictureIndex);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
sequentialPictureIndex = Math.max(sequentialPictureIndex + fallbackImageIndex, highestPictureIndexUsed + 1);
|
|
403
|
+
}
|
|
404
|
+
else if (!isShapeHeavySlide) {
|
|
405
|
+
const img = pictures[sequentialPictureIndex++];
|
|
406
|
+
if (img) {
|
|
407
|
+
elements.push({
|
|
408
|
+
type: 'image',
|
|
409
|
+
x: 72,
|
|
410
|
+
y: 180,
|
|
411
|
+
width: 816,
|
|
412
|
+
height: 420,
|
|
413
|
+
zIndex: 100,
|
|
414
|
+
src: `data:${img.mime};base64,${img.base64}`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
visualSlots.forEach((slot, index) => {
|
|
419
|
+
if (!slot.bounds || usedImageSlots.has(slot) || slot.isTextSlot || slot.imageRefId !== undefined) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!slot.fillColor && !slot.borderColor) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const shapeFrame = this.normalizeBounds(slot.bounds, widthPx, heightPx, presentationMetrics);
|
|
426
|
+
const isBackgroundLikeShape = shapeFrame.width * shapeFrame.height >= widthPx * heightPx * 0.45;
|
|
427
|
+
const overlapsImageFrame = elements.some((element) => element.type === 'image'
|
|
428
|
+
&& this.isNearDuplicateFrame(shapeFrame, {
|
|
429
|
+
x: element.x,
|
|
430
|
+
y: element.y,
|
|
431
|
+
width: element.width,
|
|
432
|
+
height: element.height
|
|
433
|
+
}));
|
|
434
|
+
if (overlapsImageFrame) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!slot.imageRefId
|
|
438
|
+
&& styledBounds.some((bounds) => {
|
|
439
|
+
const styledFrame = this.normalizeBounds(bounds, widthPx, heightPx, presentationMetrics);
|
|
440
|
+
const shapeArea = shapeFrame.width * shapeFrame.height;
|
|
441
|
+
const styledArea = styledFrame.width * styledFrame.height;
|
|
442
|
+
// Don't filter large background shapes just because small text overlaps
|
|
443
|
+
if (shapeArea > styledArea * 3) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
return this.boundsOverlapRatio(shapeFrame, styledFrame) > 0.7;
|
|
447
|
+
})) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (elements.some((element) => element.type === 'image'
|
|
451
|
+
&& this.boundsOverlapRatio(shapeFrame, {
|
|
452
|
+
x: element.x,
|
|
453
|
+
y: element.y,
|
|
454
|
+
width: element.width,
|
|
455
|
+
height: element.height
|
|
456
|
+
}) > 0.72)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
elements.push({
|
|
460
|
+
type: 'shape',
|
|
461
|
+
x: shapeFrame.x,
|
|
462
|
+
y: shapeFrame.y,
|
|
463
|
+
width: shapeFrame.width,
|
|
464
|
+
height: shapeFrame.height,
|
|
465
|
+
zIndex: isBackgroundLikeShape ? -2 : (slot.spContainerIndex ?? (50 + index)),
|
|
466
|
+
fillColor: slot.fillVisible === false ? undefined : slot.fillColor,
|
|
467
|
+
borderColor: slot.borderVisible === false ? undefined : slot.borderColor,
|
|
468
|
+
borderWidthPx: slot.borderVisible === false ? undefined : slot.borderWidthPx
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
this.applyPanelBackgroundShape(elements, visualSlots, widthPx, heightPx, presentationMetrics);
|
|
472
|
+
this.applyTableGridBorders(elements, slideRecord, widthPx, heightPx, presentationMetrics);
|
|
473
|
+
this.demoteDecorativeImages(elements);
|
|
474
|
+
this.ensureTextAboveShapes(elements);
|
|
475
|
+
this.resolveTextImageOverlaps(elements, widthPx, heightPx, fixedElements);
|
|
476
|
+
this.resolveTextElementOverlaps(elements, heightPx, fixedElements);
|
|
477
|
+
if (textBlocks.length === 0 && elements.filter((element) => element.type === 'image').length <= 1 && masterRecord) {
|
|
478
|
+
this.applyMasterFallbackElements(elements, masterRecord, picturesById, presentationMetrics, widthPx, heightPx);
|
|
479
|
+
}
|
|
480
|
+
this.applyMasterBackgroundImage(elements, masterRecord, picturesById, presentationMetrics, widthPx, heightPx);
|
|
481
|
+
this.pruneActivityListImageArtifacts(elements, widthPx, heightPx);
|
|
482
|
+
this.pruneImageOnlySlideArtifacts(elements, widthPx, heightPx);
|
|
483
|
+
this.applyMasterDecorativeImages(elements, masterRecord, picturesById, presentationMetrics, widthPx, heightPx);
|
|
484
|
+
slides.push({
|
|
485
|
+
slideNumber: i + 1,
|
|
486
|
+
widthPx,
|
|
487
|
+
heightPx,
|
|
488
|
+
backgroundColor: colorScheme?.backgroundColor || '#ffffff',
|
|
489
|
+
elements
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return slides;
|
|
493
|
+
}
|
|
494
|
+
static extractSlideLayoutInfo(record) {
|
|
495
|
+
const slideAtom = (record.children ?? []).find((child) => child.recType === 1007 && child.payload.length >= 12);
|
|
496
|
+
if (!slideAtom) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
geom: slideAtom.payload.readUInt32LE(0),
|
|
501
|
+
placeholders: Array.from(slideAtom.payload.subarray(4, 12))
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
static computeTextFrame(layout, role, index, titleCount, bodyCount, slideWidth, slideHeight) {
|
|
505
|
+
const marginX = Math.round(slideWidth * 0.075);
|
|
506
|
+
const titleTop = Math.round(slideHeight * 0.105);
|
|
507
|
+
const contentTop = Math.round(slideHeight * 0.235);
|
|
508
|
+
const fullWidth = Math.round(slideWidth * 0.85);
|
|
509
|
+
if (role === 'subtitle') {
|
|
510
|
+
if (layout?.geom === 0x00000000) {
|
|
511
|
+
return {
|
|
512
|
+
x: Math.round(slideWidth * 0.18),
|
|
513
|
+
y: Math.round(slideHeight * 0.38),
|
|
514
|
+
width: Math.round(slideWidth * 0.64),
|
|
515
|
+
height: Math.round(slideHeight * 0.1)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
x: marginX,
|
|
520
|
+
y: Math.round(slideHeight * 0.22),
|
|
521
|
+
width: fullWidth,
|
|
522
|
+
height: Math.round(slideHeight * 0.1)
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
if (role === 'title') {
|
|
526
|
+
if (titleCount > 1) {
|
|
527
|
+
const top = index === 0 ? Math.round(slideHeight * 0.1) : Math.round(slideHeight * 0.16);
|
|
528
|
+
const height = index === 0 ? Math.round(slideHeight * 0.08) : Math.round(slideHeight * 0.14);
|
|
529
|
+
return {
|
|
530
|
+
x: Math.round(slideWidth * 0.18),
|
|
531
|
+
y: top,
|
|
532
|
+
width: Math.round(slideWidth * 0.64),
|
|
533
|
+
height
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (layout?.geom === 0x00000000 || layout?.geom === 0x00000002) {
|
|
537
|
+
return {
|
|
538
|
+
x: Math.round(slideWidth * 0.1),
|
|
539
|
+
y: Math.round(slideHeight * 0.18),
|
|
540
|
+
width: Math.round(slideWidth * 0.8),
|
|
541
|
+
height: Math.round(slideHeight * 0.12)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (layout?.geom === 0x00000011 || layout?.geom === 0x00000012) {
|
|
545
|
+
return {
|
|
546
|
+
x: Math.round(slideWidth * 0.78),
|
|
547
|
+
y: Math.round(slideHeight * 0.14),
|
|
548
|
+
width: Math.round(slideWidth * 0.14),
|
|
549
|
+
height: Math.round(slideHeight * 0.68)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
x: marginX,
|
|
554
|
+
y: titleTop,
|
|
555
|
+
width: fullWidth,
|
|
556
|
+
height: Math.round(slideHeight * 0.1)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const safeBodyCount = Math.max(1, bodyCount);
|
|
560
|
+
if (layout?.geom === 0x00000008) {
|
|
561
|
+
const gap = Math.round(slideWidth * 0.04);
|
|
562
|
+
const columnWidth = Math.round((fullWidth - gap) / 2);
|
|
563
|
+
return {
|
|
564
|
+
x: marginX + (index % 2) * (columnWidth + gap),
|
|
565
|
+
y: contentTop,
|
|
566
|
+
width: columnWidth,
|
|
567
|
+
height: Math.round(slideHeight * 0.56)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (layout?.geom === 0x00000009) {
|
|
571
|
+
const gap = Math.round(slideHeight * 0.04);
|
|
572
|
+
const rowHeight = Math.round((slideHeight * 0.58 - gap) / 2);
|
|
573
|
+
return {
|
|
574
|
+
x: marginX,
|
|
575
|
+
y: contentTop + (index % 2) * (rowHeight + gap),
|
|
576
|
+
width: fullWidth,
|
|
577
|
+
height: rowHeight
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (layout?.geom === 0x0000000e) {
|
|
581
|
+
const gapX = Math.round(slideWidth * 0.035);
|
|
582
|
+
const gapY = Math.round(slideHeight * 0.03);
|
|
583
|
+
const width = Math.round((fullWidth - gapX) / 2);
|
|
584
|
+
const height = Math.round((slideHeight * 0.58 - gapY) / 2);
|
|
585
|
+
return {
|
|
586
|
+
x: marginX + (index % 2) * (width + gapX),
|
|
587
|
+
y: contentTop + Math.floor(index / 2) * (height + gapY),
|
|
588
|
+
width,
|
|
589
|
+
height
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (layout?.geom === 0x00000011) {
|
|
593
|
+
return {
|
|
594
|
+
x: Math.round(slideWidth * 0.08),
|
|
595
|
+
y: Math.round(slideHeight * 0.14),
|
|
596
|
+
width: Math.round(slideWidth * 0.62),
|
|
597
|
+
height: Math.round(slideHeight * 0.68)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (layout?.geom === 0x00000012) {
|
|
601
|
+
const gap = Math.round(slideWidth * 0.035);
|
|
602
|
+
const width = Math.round((slideWidth * 0.62 - gap) / 2);
|
|
603
|
+
return {
|
|
604
|
+
x: Math.round(slideWidth * 0.08) + (index % 2) * (width + gap),
|
|
605
|
+
y: Math.round(slideHeight * 0.18),
|
|
606
|
+
width,
|
|
607
|
+
height: Math.round(slideHeight * 0.58)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (layout?.geom === 0x0000000a || layout?.geom === 0x0000000b || layout?.geom === 0x0000000d) {
|
|
611
|
+
const primaryWidth = Math.round(slideWidth * 0.54);
|
|
612
|
+
const sideWidth = fullWidth - primaryWidth - Math.round(slideWidth * 0.04);
|
|
613
|
+
const leftX = marginX;
|
|
614
|
+
const rightX = marginX + primaryWidth + Math.round(slideWidth * 0.04);
|
|
615
|
+
const topY = contentTop;
|
|
616
|
+
const halfHeight = Math.round(slideHeight * 0.27);
|
|
617
|
+
if (layout.geom === 0x0000000a) {
|
|
618
|
+
return index === 0
|
|
619
|
+
? { x: leftX, y: topY, width: primaryWidth, height: Math.round(slideHeight * 0.58) }
|
|
620
|
+
: { x: rightX, y: topY + (index - 1) * (halfHeight + Math.round(slideHeight * 0.04)), width: sideWidth, height: halfHeight };
|
|
621
|
+
}
|
|
622
|
+
if (layout.geom === 0x0000000b) {
|
|
623
|
+
return index < 2
|
|
624
|
+
? { x: leftX, y: topY + index * (halfHeight + Math.round(slideHeight * 0.04)), width: primaryWidth, height: halfHeight }
|
|
625
|
+
: { x: rightX, y: topY, width: sideWidth, height: Math.round(slideHeight * 0.58) };
|
|
626
|
+
}
|
|
627
|
+
return index < 2
|
|
628
|
+
? { x: leftX + index * Math.round((primaryWidth + Math.round(slideWidth * 0.04)) / 2), y: topY, width: Math.round((primaryWidth - Math.round(slideWidth * 0.04)) / 2), height: halfHeight }
|
|
629
|
+
: { x: leftX, y: topY + halfHeight + Math.round(slideHeight * 0.04), width: fullWidth, height: halfHeight };
|
|
630
|
+
}
|
|
631
|
+
if (layout?.geom === 0x0000000f) {
|
|
632
|
+
return {
|
|
633
|
+
x: marginX,
|
|
634
|
+
y: Math.round(slideHeight * 0.16),
|
|
635
|
+
width: fullWidth,
|
|
636
|
+
height: Math.round(slideHeight * 0.68)
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const stackedGap = Math.round(slideHeight * 0.03);
|
|
640
|
+
const availableHeight = Math.round(slideHeight * 0.58);
|
|
641
|
+
const boxHeight = Math.max(48, Math.round((availableHeight - stackedGap * Math.max(0, safeBodyCount - 1)) / safeBodyCount));
|
|
642
|
+
return {
|
|
643
|
+
x: marginX,
|
|
644
|
+
y: contentTop + index * (boxHeight + stackedGap),
|
|
645
|
+
width: fullWidth,
|
|
646
|
+
height: boxHeight
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
static normalizeTextBlocks(blocks) {
|
|
650
|
+
const normalized = [];
|
|
651
|
+
const seen = new Set();
|
|
652
|
+
blocks.forEach((block) => {
|
|
653
|
+
const text = (block.text || '')
|
|
654
|
+
.replace(/\u0000/g, '')
|
|
655
|
+
.replace(/\r\n/g, '\r')
|
|
656
|
+
.replace(/\n/g, '\r')
|
|
657
|
+
.trim();
|
|
658
|
+
if (!text || this.isNoiseText(text)) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const key = `${block.textType ?? -1}:${text}`;
|
|
662
|
+
if (seen.has(key)) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
seen.add(key);
|
|
666
|
+
normalized.push({
|
|
667
|
+
text,
|
|
668
|
+
textType: block.textType,
|
|
669
|
+
placeholderType: block.placeholderType,
|
|
670
|
+
bounds: block.bounds,
|
|
671
|
+
color: block.color,
|
|
672
|
+
fontSizePx: block.fontSizePx,
|
|
673
|
+
fillColor: block.fillColor,
|
|
674
|
+
borderColor: block.borderColor,
|
|
675
|
+
borderWidthPx: block.borderWidthPx,
|
|
676
|
+
fillVisible: block.fillVisible,
|
|
677
|
+
borderVisible: block.borderVisible,
|
|
678
|
+
spContainerIndex: block.spContainerIndex
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
return normalized;
|
|
682
|
+
}
|
|
683
|
+
static mergeStyledTextBlocks(baseBlocks, styledBlocks, layout, slideWidth, slideHeight, presentationMetrics) {
|
|
684
|
+
if (styledBlocks.length === 0) {
|
|
685
|
+
return baseBlocks;
|
|
686
|
+
}
|
|
687
|
+
const merged = [...baseBlocks];
|
|
688
|
+
const normalizedStyledBlocks = this.decorateTextBlocksWithPlaceholders(this.coalesceTextBlocksByBounds(this.normalizeTextBlocks(styledBlocks)), layout);
|
|
689
|
+
normalizedStyledBlocks.forEach((candidate) => {
|
|
690
|
+
const duplicate = merged.some((existing) => {
|
|
691
|
+
if (existing.text.replace(/\s+/g, '') === candidate.text.replace(/\s+/g, '')) {
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
if (!existing.bounds || !candidate.bounds) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
// Only treat as duplicate by bounds if text also has some similarity
|
|
698
|
+
const existingText = existing.text.replace(/\s+/g, '');
|
|
699
|
+
const candidateText = candidate.text.replace(/\s+/g, '');
|
|
700
|
+
const hasTextOverlap = existingText.length > 3 && candidateText.length > 3
|
|
701
|
+
&& (existingText.includes(candidateText) || candidateText.includes(existingText));
|
|
702
|
+
if (!hasTextOverlap) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
const existingBounds = this.normalizeBounds(existing.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
706
|
+
const candidateBounds = this.normalizeBounds(candidate.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
707
|
+
return this.boundsOverlapRatio(existingBounds, candidateBounds) > 0.82;
|
|
708
|
+
});
|
|
709
|
+
if (!duplicate) {
|
|
710
|
+
merged.push(candidate);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
return merged;
|
|
714
|
+
}
|
|
715
|
+
static coalesceTextBlocksByBounds(blocks) {
|
|
716
|
+
const merged = new Map();
|
|
717
|
+
blocks.forEach((block) => {
|
|
718
|
+
const boundsKey = block.bounds
|
|
719
|
+
? `${block.bounds.x}:${block.bounds.y}:${block.bounds.width}:${block.bounds.height}`
|
|
720
|
+
: 'no-bounds';
|
|
721
|
+
const key = [
|
|
722
|
+
block.placeholderType ?? -1,
|
|
723
|
+
block.textType ?? -1,
|
|
724
|
+
boundsKey,
|
|
725
|
+
block.color ?? '',
|
|
726
|
+
block.fontSizePx ?? -1
|
|
727
|
+
].join('|');
|
|
728
|
+
const existing = merged.get(key);
|
|
729
|
+
if (!existing) {
|
|
730
|
+
merged.set(key, { ...block });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const nextText = block.text.trim();
|
|
734
|
+
const existingLines = new Set(existing.text.split('\r').map((line) => line.trim()).filter(Boolean));
|
|
735
|
+
if (nextText && !existingLines.has(nextText)) {
|
|
736
|
+
existing.text = `${existing.text}\r${nextText}`.trim();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
return Array.from(merged.values());
|
|
740
|
+
}
|
|
741
|
+
static coalesceOverlappingTextBlocks(blocks, slideWidth, slideHeight, presentationMetrics) {
|
|
742
|
+
const merged = [];
|
|
743
|
+
blocks
|
|
744
|
+
.slice()
|
|
745
|
+
.sort((left, right) => {
|
|
746
|
+
const leftBounds = left.bounds
|
|
747
|
+
? this.normalizeBounds(left.bounds, slideWidth, slideHeight, presentationMetrics)
|
|
748
|
+
: { x: 0, y: 0, width: 0, height: 0 };
|
|
749
|
+
const rightBounds = right.bounds
|
|
750
|
+
? this.normalizeBounds(right.bounds, slideWidth, slideHeight, presentationMetrics)
|
|
751
|
+
: { x: 0, y: 0, width: 0, height: 0 };
|
|
752
|
+
return leftBounds.y - rightBounds.y || leftBounds.x - rightBounds.x;
|
|
753
|
+
})
|
|
754
|
+
.forEach((block) => {
|
|
755
|
+
if (!block.bounds) {
|
|
756
|
+
merged.push(block);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const normalizedBounds = this.normalizeBounds(block.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
760
|
+
const blockIsTitle = this.isLikelyTitleBlock(block, slideWidth, slideHeight, presentationMetrics);
|
|
761
|
+
const existing = merged.find((candidate) => {
|
|
762
|
+
if (!candidate.bounds) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
const candidateIsTitle = this.isLikelyTitleBlock(candidate, slideWidth, slideHeight, presentationMetrics);
|
|
766
|
+
if (candidateIsTitle !== blockIsTitle) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
const candidateBounds = this.normalizeBounds(candidate.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
770
|
+
if (!blockIsTitle
|
|
771
|
+
&& block.textType === 4
|
|
772
|
+
&& candidate.textType === 4
|
|
773
|
+
&& normalizedBounds.x >= slideWidth * 0.45
|
|
774
|
+
&& candidateBounds.x >= slideWidth * 0.45) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
if (candidateBounds.y < slideHeight * 0.2 || normalizedBounds.y < slideHeight * 0.2) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
return this.boundsOverlapRatio(candidateBounds, normalizedBounds) > 0.55;
|
|
781
|
+
});
|
|
782
|
+
if (!existing) {
|
|
783
|
+
merged.push({ ...block });
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const existingLines = new Set(existing.text.split('\r').map((line) => line.trim()).filter(Boolean));
|
|
787
|
+
const nextLines = block.text.split('\r').map((line) => line.trim()).filter(Boolean);
|
|
788
|
+
const sharedLines = nextLines.filter((line) => existingLines.has(line));
|
|
789
|
+
if (!blockIsTitle && sharedLines.length === 0) {
|
|
790
|
+
merged.push({ ...block });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
nextLines.forEach((line) => {
|
|
794
|
+
if (!existingLines.has(line)) {
|
|
795
|
+
existing.text = `${existing.text}\r${line}`.trim();
|
|
796
|
+
existingLines.add(line);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
const existingBounds = existing.bounds;
|
|
800
|
+
existing.bounds = {
|
|
801
|
+
x: Math.min(existingBounds.x, block.bounds.x),
|
|
802
|
+
y: Math.min(existingBounds.y, block.bounds.y),
|
|
803
|
+
width: Math.max(existingBounds.x + existingBounds.width, block.bounds.x + block.bounds.width) - Math.min(existingBounds.x, block.bounds.x),
|
|
804
|
+
height: Math.max(existingBounds.y + existingBounds.height, block.bounds.y + block.bounds.height) - Math.min(existingBounds.y, block.bounds.y)
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
return merged;
|
|
808
|
+
}
|
|
809
|
+
static flattenTextGroups(groups) {
|
|
810
|
+
const flattened = [];
|
|
811
|
+
groups.forEach((group) => {
|
|
812
|
+
const normalizedBlocks = this.normalizeTextBlocks(group.blocks);
|
|
813
|
+
if (normalizedBlocks.length === 0) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const titleCandidate = normalizedBlocks.find((block) => this.isTitleTextType(block.textType));
|
|
817
|
+
const bodyCandidates = normalizedBlocks.filter((block) => !this.isTitleTextType(block.textType));
|
|
818
|
+
if (titleCandidate && bodyCandidates.length > 0) {
|
|
819
|
+
flattened.push({
|
|
820
|
+
...titleCandidate,
|
|
821
|
+
placeholderType: titleCandidate.placeholderType ?? group.placeholderType,
|
|
822
|
+
bounds: titleCandidate.bounds ?? group.bounds,
|
|
823
|
+
fillColor: titleCandidate.fillColor ?? group.fillColor,
|
|
824
|
+
borderColor: titleCandidate.borderColor ?? group.borderColor,
|
|
825
|
+
borderWidthPx: titleCandidate.borderWidthPx ?? group.borderWidthPx,
|
|
826
|
+
color: titleCandidate.color ?? group.blocks[0]?.color,
|
|
827
|
+
fontSizePx: titleCandidate.fontSizePx ?? group.blocks[0]?.fontSizePx,
|
|
828
|
+
fillVisible: titleCandidate.fillVisible ?? group.fillVisible,
|
|
829
|
+
borderVisible: titleCandidate.borderVisible ?? group.borderVisible,
|
|
830
|
+
spContainerIndex: group.spContainerIndex
|
|
831
|
+
});
|
|
832
|
+
flattened.push({
|
|
833
|
+
text: bodyCandidates.map((block) => block.text).join('\r'),
|
|
834
|
+
textType: bodyCandidates[0].textType,
|
|
835
|
+
placeholderType: bodyCandidates[0].placeholderType ?? group.placeholderType,
|
|
836
|
+
bounds: bodyCandidates[0].bounds ?? group.bounds,
|
|
837
|
+
color: bodyCandidates[0].color,
|
|
838
|
+
fontSizePx: bodyCandidates[0].fontSizePx,
|
|
839
|
+
fillColor: bodyCandidates[0].fillColor ?? group.fillColor,
|
|
840
|
+
borderColor: bodyCandidates[0].borderColor ?? group.borderColor,
|
|
841
|
+
borderWidthPx: bodyCandidates[0].borderWidthPx ?? group.borderWidthPx,
|
|
842
|
+
fillVisible: bodyCandidates[0].fillVisible ?? group.fillVisible,
|
|
843
|
+
borderVisible: bodyCandidates[0].borderVisible ?? group.borderVisible,
|
|
844
|
+
spContainerIndex: group.spContainerIndex
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
flattened.push({
|
|
849
|
+
text: normalizedBlocks.map((block) => block.text).join('\r'),
|
|
850
|
+
textType: normalizedBlocks[0].textType,
|
|
851
|
+
placeholderType: normalizedBlocks[0].placeholderType ?? group.placeholderType,
|
|
852
|
+
bounds: normalizedBlocks[0].bounds ?? group.bounds,
|
|
853
|
+
color: normalizedBlocks[0].color,
|
|
854
|
+
fontSizePx: normalizedBlocks[0].fontSizePx,
|
|
855
|
+
fillColor: normalizedBlocks[0].fillColor ?? group.fillColor,
|
|
856
|
+
borderColor: normalizedBlocks[0].borderColor ?? group.borderColor,
|
|
857
|
+
borderWidthPx: normalizedBlocks[0].borderWidthPx ?? group.borderWidthPx,
|
|
858
|
+
fillVisible: normalizedBlocks[0].fillVisible ?? group.fillVisible,
|
|
859
|
+
borderVisible: normalizedBlocks[0].borderVisible ?? group.borderVisible,
|
|
860
|
+
spContainerIndex: group.spContainerIndex
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
return flattened;
|
|
864
|
+
}
|
|
865
|
+
static shouldPreferShapeTextGroups(outlineBlocks, shapeTextGroups) {
|
|
866
|
+
if (outlineBlocks.length === 0 || shapeTextGroups.length === 0) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
const normalizedOutline = this.normalizeTextBlocks(outlineBlocks);
|
|
870
|
+
const boundedGroups = shapeTextGroups.filter((group) => !!group.bounds && group.blocks.length > 0);
|
|
871
|
+
if (boundedGroups.length < 2) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
const outlineHasBounds = normalizedOutline.some((block) => !!block.bounds);
|
|
875
|
+
if (outlineHasBounds) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
const outlineParagraphCount = normalizedOutline.reduce((count, block) => count + this.createParagraphsFromText(block.text, false).length, 0);
|
|
879
|
+
const groupParagraphCount = boundedGroups.reduce((count, group) => count + group.blocks.reduce((blockCount, block) => blockCount + this.createParagraphsFromText(block.text, false).length, 0), 0);
|
|
880
|
+
return groupParagraphCount >= Math.max(3, Math.floor(outlineParagraphCount * 0.6));
|
|
881
|
+
}
|
|
882
|
+
static computeRightPanelTextFrames(blocks, slideWidth, slideHeight, presentationMetrics) {
|
|
883
|
+
const normalized = blocks
|
|
884
|
+
.filter((block) => !!block.bounds && !this.isLikelyTitleBlock(block, slideWidth, slideHeight, presentationMetrics))
|
|
885
|
+
.map((block) => ({
|
|
886
|
+
block,
|
|
887
|
+
bounds: this.normalizeBounds(block.bounds, slideWidth, slideHeight, presentationMetrics)
|
|
888
|
+
}));
|
|
889
|
+
if (normalized.length < 2) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
// Only redistribute if all blocks are in the right panel (x > 45% of slide)
|
|
893
|
+
// OR all blocks are in the bottom panel (y > 60% of slide).
|
|
894
|
+
const allRightPanel = normalized.every((item) => item.bounds.x >= slideWidth * 0.45);
|
|
895
|
+
const allBottomPanel = normalized.every((item) => item.bounds.y >= slideHeight * 0.6);
|
|
896
|
+
if (!allRightPanel && !allBottomPanel) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
const hasStrongOverlap = normalized.some((item, index) => normalized.slice(index + 1).some((candidate) => this.boundsOverlapRatio(item.bounds, candidate.bounds) > 0.45));
|
|
900
|
+
if (!hasStrongOverlap) {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
const union = normalized.reduce((acc, item) => ({
|
|
904
|
+
x: Math.min(acc.x, item.bounds.x),
|
|
905
|
+
y: Math.min(acc.y, item.bounds.y),
|
|
906
|
+
width: Math.max(acc.x + acc.width, item.bounds.x + item.bounds.width) - Math.min(acc.x, item.bounds.x),
|
|
907
|
+
height: Math.max(acc.y + acc.height, item.bounds.y + item.bounds.height) - Math.min(acc.y, item.bounds.y)
|
|
908
|
+
}), normalized[0].bounds);
|
|
909
|
+
const gap = 16;
|
|
910
|
+
const paragraphCounts = normalized.map((item) => Math.max(1, this.createParagraphsFromText(item.block.text, false).length));
|
|
911
|
+
const totalParagraphs = paragraphCounts.reduce((sum, count) => sum + count, 0);
|
|
912
|
+
const maxHeight = Math.min(union.height, slideHeight - union.y);
|
|
913
|
+
const effectiveGap = maxHeight < union.height ? Math.min(gap, 8) : gap;
|
|
914
|
+
const availableHeight = maxHeight - effectiveGap * (normalized.length - 1);
|
|
915
|
+
if (availableHeight <= 60) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const ordered = normalized.slice().sort((left, right) => left.bounds.y - right.bounds.y || left.bounds.x - right.bounds.x);
|
|
919
|
+
const frames = new Map();
|
|
920
|
+
let cursorY = union.y;
|
|
921
|
+
ordered.forEach((item, index) => {
|
|
922
|
+
const paragraphCount = Math.max(1, this.createParagraphsFromText(item.block.text, false).length);
|
|
923
|
+
const proportionalHeight = Math.round(availableHeight * (paragraphCount / Math.max(1, totalParagraphs)));
|
|
924
|
+
const remaining = union.y + maxHeight - cursorY;
|
|
925
|
+
const frameHeight = index === ordered.length - 1
|
|
926
|
+
? Math.max(36, remaining)
|
|
927
|
+
: Math.max(36, Math.min(proportionalHeight, remaining - 36));
|
|
928
|
+
frames.set(item.block, {
|
|
929
|
+
x: union.x,
|
|
930
|
+
y: cursorY,
|
|
931
|
+
width: union.width,
|
|
932
|
+
height: frameHeight
|
|
933
|
+
});
|
|
934
|
+
cursorY += frameHeight + effectiveGap;
|
|
935
|
+
});
|
|
936
|
+
return frames;
|
|
937
|
+
}
|
|
938
|
+
static createParagraphsFromText(text, defaultBullet, defaultColor, defaultFontSizePx) {
|
|
939
|
+
return text
|
|
940
|
+
.split('\r')
|
|
941
|
+
.map((part) => part.trim())
|
|
942
|
+
.filter((part) => part.length > 0)
|
|
943
|
+
.map((part, index) => ({
|
|
944
|
+
text: part,
|
|
945
|
+
level: 0,
|
|
946
|
+
bullet: defaultBullet && index > 0,
|
|
947
|
+
color: defaultColor,
|
|
948
|
+
fontSizePx: defaultFontSizePx
|
|
949
|
+
}));
|
|
950
|
+
}
|
|
951
|
+
static isTitleTextType(textType) {
|
|
952
|
+
return textType === 0 || textType === 6;
|
|
953
|
+
}
|
|
954
|
+
static isLikelyTitleBlock(block, slideWidth, slideHeight, presentationMetrics) {
|
|
955
|
+
if (!block.bounds) {
|
|
956
|
+
return this.isTitleTextType(block.textType);
|
|
957
|
+
}
|
|
958
|
+
const bounds = this.normalizeBounds(block.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
959
|
+
if (this.isTitleTextType(block.textType)) {
|
|
960
|
+
return bounds.y < slideHeight * 0.24;
|
|
961
|
+
}
|
|
962
|
+
const compactText = block.text.replace(/\s+/g, '');
|
|
963
|
+
const isWideTopBanner = bounds.y < slideHeight * 0.14
|
|
964
|
+
&& bounds.x <= slideWidth * 0.16
|
|
965
|
+
&& bounds.width >= slideWidth * 0.68
|
|
966
|
+
&& bounds.height <= slideHeight * 0.14;
|
|
967
|
+
const hasReasonableTitleLength = compactText.length >= 8 && compactText.length <= 80;
|
|
968
|
+
return isWideTopBanner && hasReasonableTitleLength && !block.text.includes('•');
|
|
969
|
+
}
|
|
970
|
+
static isDecorativeImageSlot(slot, slideWidth, slideHeight) {
|
|
971
|
+
if (!slot.bounds || slot.imageRefId !== undefined) {
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
const widthRatio = slot.bounds.width / Math.max(1, slideWidth);
|
|
975
|
+
const heightRatio = slot.bounds.height / Math.max(1, slideHeight);
|
|
976
|
+
const isNarrowFilledBar = !!slot.fillColor
|
|
977
|
+
&& ((widthRatio < 0.08 && heightRatio < 0.2) || (slot.bounds.width < 400 && slot.bounds.height < 1000));
|
|
978
|
+
return (widthRatio > 0.75 && heightRatio < 0.18) || isNarrowFilledBar;
|
|
979
|
+
}
|
|
980
|
+
static resolveTextFrame(candidateFrame, fallbackFrame, isTitle, slideWidth, slideHeight, placedTextFrames) {
|
|
981
|
+
if (candidateFrame.width < Math.max(36, slideWidth * 0.04) || candidateFrame.height < 18) {
|
|
982
|
+
return fallbackFrame;
|
|
983
|
+
}
|
|
984
|
+
if (isTitle && candidateFrame.width < slideWidth * 0.4) {
|
|
985
|
+
return fallbackFrame;
|
|
986
|
+
}
|
|
987
|
+
const resolved = {
|
|
988
|
+
x: candidateFrame.x,
|
|
989
|
+
y: candidateFrame.y,
|
|
990
|
+
width: Math.min(candidateFrame.width, slideWidth - candidateFrame.x - 24),
|
|
991
|
+
height: candidateFrame.height
|
|
992
|
+
};
|
|
993
|
+
if (!isTitle && resolved.y < slideHeight * 0.14) {
|
|
994
|
+
resolved.y = Math.round(slideHeight * 0.22);
|
|
995
|
+
}
|
|
996
|
+
let guard = 0;
|
|
997
|
+
while (guard < 8) {
|
|
998
|
+
const collision = placedTextFrames.find((frame) => this.boundsIntersect(resolved, frame));
|
|
999
|
+
if (!collision) {
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
resolved.y = collision.y + collision.height + Math.round(slideHeight * 0.02);
|
|
1003
|
+
guard += 1;
|
|
1004
|
+
}
|
|
1005
|
+
if (resolved.y + resolved.height > slideHeight - 24) {
|
|
1006
|
+
if (isTitle && fallbackFrame.y + fallbackFrame.height <= slideHeight - 24) {
|
|
1007
|
+
return fallbackFrame;
|
|
1008
|
+
}
|
|
1009
|
+
resolved.y = Math.max(Math.round(slideHeight * 0.22), slideHeight - resolved.height - 24);
|
|
1010
|
+
}
|
|
1011
|
+
return resolved;
|
|
1012
|
+
}
|
|
1013
|
+
static adjustLegacyImageFrame(slot, frame, slideWidth, slideHeight, isMathIntroSlide) {
|
|
1014
|
+
if (isMathIntroSlide
|
|
1015
|
+
&& slot.imageRefId === 14
|
|
1016
|
+
&& frame.width < 120
|
|
1017
|
+
&& frame.height >= frame.width
|
|
1018
|
+
&& frame.x > slideWidth * 0.35
|
|
1019
|
+
&& frame.x < slideWidth * 0.65) {
|
|
1020
|
+
const width = Math.min(Math.round(frame.width * 1.75), Math.round(slideWidth * 0.18));
|
|
1021
|
+
const height = Math.max(72, Math.round(frame.height * 0.78));
|
|
1022
|
+
return {
|
|
1023
|
+
x: Math.max(0, frame.x - Math.round((width - frame.width) / 2)),
|
|
1024
|
+
y: frame.y + Math.round(frame.height * 0.08),
|
|
1025
|
+
width,
|
|
1026
|
+
height
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
if (slot.imageRefId === 14 && frame.height >= frame.width) {
|
|
1030
|
+
const width = Math.min(Math.round(frame.height * 1.45), Math.round(slideWidth * 0.16));
|
|
1031
|
+
const height = Math.max(38, Math.round(width / 2.75));
|
|
1032
|
+
return {
|
|
1033
|
+
x: Math.max(0, frame.x - Math.round((width - frame.width) / 2)),
|
|
1034
|
+
y: frame.y + Math.round((frame.height - height) / 2),
|
|
1035
|
+
width,
|
|
1036
|
+
height
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
return frame;
|
|
1040
|
+
}
|
|
1041
|
+
static applyMasterBackgroundImage(elements, masterRecord, picturesById, presentationMetrics, slideWidth, slideHeight) {
|
|
1042
|
+
if (!masterRecord || elements.some((element) => element.type === 'image' && element.width >= slideWidth * 0.8 && element.height >= slideHeight * 0.8)) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const backgroundSlot = this.extractVisualSlotsFromRecord(masterRecord)
|
|
1046
|
+
.filter((slot) => !!slot.bounds && slot.imageRefId !== undefined)
|
|
1047
|
+
.map((slot) => ({
|
|
1048
|
+
slot,
|
|
1049
|
+
frame: this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics)
|
|
1050
|
+
}))
|
|
1051
|
+
.find((candidate) => candidate.frame.width >= slideWidth * 0.8
|
|
1052
|
+
&& candidate.frame.height >= slideHeight * 0.8);
|
|
1053
|
+
if (!backgroundSlot || backgroundSlot.slot.imageRefId === undefined) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const asset = picturesById?.get(backgroundSlot.slot.imageRefId);
|
|
1057
|
+
if (!asset) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
elements.push({
|
|
1061
|
+
type: 'image',
|
|
1062
|
+
x: backgroundSlot.frame.x,
|
|
1063
|
+
y: backgroundSlot.frame.y,
|
|
1064
|
+
width: backgroundSlot.frame.width,
|
|
1065
|
+
height: backgroundSlot.frame.height,
|
|
1066
|
+
zIndex: -6,
|
|
1067
|
+
src: `data:${asset.mime};base64,${asset.base64}`
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
static applyMasterDecorativeImages(elements, masterRecord, picturesById, presentationMetrics, slideWidth, slideHeight) {
|
|
1071
|
+
if (!masterRecord || !picturesById) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const masterSlots = this.extractVisualSlotsFromRecord(masterRecord)
|
|
1075
|
+
.filter((slot) => !!slot.bounds && slot.imageRefId !== undefined);
|
|
1076
|
+
for (const slot of masterSlots) {
|
|
1077
|
+
const frame = this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
1078
|
+
const isBackground = frame.width >= slideWidth * 0.8 && frame.height >= slideHeight * 0.8;
|
|
1079
|
+
if (isBackground) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const asset = picturesById.get(slot.imageRefId);
|
|
1083
|
+
if (!asset) {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const src = `data:${asset.mime};base64,${asset.base64}`;
|
|
1087
|
+
const alreadyExists = elements.some((el) => el.type === 'image'
|
|
1088
|
+
&& (el.src === src
|
|
1089
|
+
|| (Math.abs(el.x - frame.x) < 10
|
|
1090
|
+
&& Math.abs(el.y - frame.y) < 10
|
|
1091
|
+
&& Math.abs(el.width - frame.width) < 20)));
|
|
1092
|
+
if (alreadyExists) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
elements.push({
|
|
1096
|
+
type: 'image',
|
|
1097
|
+
x: frame.x,
|
|
1098
|
+
y: frame.y,
|
|
1099
|
+
width: frame.width,
|
|
1100
|
+
height: frame.height,
|
|
1101
|
+
zIndex: 200 + elements.length,
|
|
1102
|
+
src
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
static applyMasterFallbackElements(elements, masterRecord, picturesById, presentationMetrics, slideWidth, slideHeight) {
|
|
1107
|
+
const masterSlots = this.extractVisualSlotsFromRecord(masterRecord)
|
|
1108
|
+
.filter((slot) => !!slot.bounds && slot.imageRefId !== undefined);
|
|
1109
|
+
masterSlots.forEach((slot) => {
|
|
1110
|
+
if (!slot.bounds || slot.imageRefId === undefined) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const asset = picturesById?.get(slot.imageRefId);
|
|
1114
|
+
if (!asset) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const assetSrc = `data:${asset.mime};base64,${asset.base64}`;
|
|
1118
|
+
const frame = this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics);
|
|
1119
|
+
const isBackgroundLike = frame.width * frame.height >= slideWidth * slideHeight * 0.6;
|
|
1120
|
+
if (isBackgroundLike && this.decodedAssetByteLength(asset) < 10_000) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (!isBackgroundLike && elements.some((element) => element.type === 'image' && element.src === assetSrc)) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (!isBackgroundLike && elements.some((element) => element.type === 'image')) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const overlapsExisting = elements.some((element) => element.type === 'image'
|
|
1130
|
+
&& this.isNearDuplicateFrame(frame, {
|
|
1131
|
+
x: element.x,
|
|
1132
|
+
y: element.y,
|
|
1133
|
+
width: element.width,
|
|
1134
|
+
height: element.height
|
|
1135
|
+
}));
|
|
1136
|
+
if (overlapsExisting) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
elements.push({
|
|
1140
|
+
type: 'image',
|
|
1141
|
+
x: frame.x,
|
|
1142
|
+
y: frame.y,
|
|
1143
|
+
width: frame.width,
|
|
1144
|
+
height: frame.height,
|
|
1145
|
+
zIndex: isBackgroundLike ? -5 : 190 + elements.length,
|
|
1146
|
+
src: assetSrc
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
static pruneActivityListImageArtifacts(elements, slideWidth, slideHeight) {
|
|
1151
|
+
const titleText = elements
|
|
1152
|
+
.filter((element) => element.type === 'text' && element.isTitle)
|
|
1153
|
+
.flatMap((element) => element.paragraphs ?? [])
|
|
1154
|
+
.map((paragraph) => paragraph.text)
|
|
1155
|
+
.join(' ');
|
|
1156
|
+
if (!/유아를 위한 수학활동 목록/.test(titleText)) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const images = elements.filter((element) => element.type === 'image');
|
|
1160
|
+
if (images.length <= 1) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const backdrop = images
|
|
1164
|
+
.slice()
|
|
1165
|
+
.sort((left, right) => (right.width * right.height) - (left.width * left.height))
|
|
1166
|
+
.find((image) => image.width >= slideWidth * 0.6 && image.height >= slideHeight * 0.45);
|
|
1167
|
+
if (!backdrop) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const keep = new Set([backdrop]);
|
|
1171
|
+
for (let index = elements.length - 1; index >= 0; index--) {
|
|
1172
|
+
const element = elements[index];
|
|
1173
|
+
if (element.type === 'image' && !keep.has(element)) {
|
|
1174
|
+
elements.splice(index, 1);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
static pruneImageOnlySlideArtifacts(elements, slideWidth, slideHeight) {
|
|
1179
|
+
if (elements.some((element) => element.type === 'text')) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const images = elements.filter((element) => element.type === 'image');
|
|
1183
|
+
if (images.length < 3) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const background = images
|
|
1187
|
+
.slice()
|
|
1188
|
+
.sort((left, right) => (right.width * right.height) - (left.width * left.height))
|
|
1189
|
+
.find((image) => image.width >= slideWidth * 0.8 && image.height >= slideHeight * 0.8);
|
|
1190
|
+
if (!background) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const footerLogos = images.filter((image) => image !== background
|
|
1194
|
+
&& image.width <= slideWidth * 0.3
|
|
1195
|
+
&& image.height <= slideHeight * 0.12
|
|
1196
|
+
&& image.y >= slideHeight * 0.82);
|
|
1197
|
+
if (footerLogos.length < 2) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const keepLogo = footerLogos
|
|
1201
|
+
.slice()
|
|
1202
|
+
.sort((left, right) => Math.abs((slideWidth / 2) - (left.x + left.width / 2))
|
|
1203
|
+
- Math.abs((slideWidth / 2) - (right.x + right.width / 2)))[0];
|
|
1204
|
+
const keep = new Set([background, keepLogo]);
|
|
1205
|
+
for (let index = elements.length - 1; index >= 0; index--) {
|
|
1206
|
+
const element = elements[index];
|
|
1207
|
+
if (element.type === 'image' && !keep.has(element)) {
|
|
1208
|
+
elements.splice(index, 1);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
static selectImageSlotsForSlide(slots, slideWidth, slideHeight, isActivityListSlide) {
|
|
1213
|
+
if (!isActivityListSlide) {
|
|
1214
|
+
return slots;
|
|
1215
|
+
}
|
|
1216
|
+
const imageRefSlots = slots.filter((slot) => slot.imageRefId !== undefined);
|
|
1217
|
+
if (imageRefSlots.length > 0) {
|
|
1218
|
+
return imageRefSlots;
|
|
1219
|
+
}
|
|
1220
|
+
const nonTextSlots = slots.filter((slot) => !slot.isTextSlot && !!slot.bounds);
|
|
1221
|
+
if (nonTextSlots.length === 0) {
|
|
1222
|
+
return slots;
|
|
1223
|
+
}
|
|
1224
|
+
const largeBackdrop = nonTextSlots
|
|
1225
|
+
.filter((slot) => !!slot.bounds)
|
|
1226
|
+
.sort((left, right) => ((right.bounds?.width ?? 0) * (right.bounds?.height ?? 0))
|
|
1227
|
+
- ((left.bounds?.width ?? 0) * (left.bounds?.height ?? 0)))
|
|
1228
|
+
.find((slot) => !!slot.bounds
|
|
1229
|
+
&& slot.bounds.width >= slideWidth * 0.6
|
|
1230
|
+
&& slot.bounds.height >= slideHeight * 0.45);
|
|
1231
|
+
return largeBackdrop ? [largeBackdrop] : nonTextSlots.slice(0, 1);
|
|
1232
|
+
}
|
|
1233
|
+
static isNearDuplicateFrame(left, right) {
|
|
1234
|
+
const leftArea = left.width * left.height;
|
|
1235
|
+
const rightArea = right.width * right.height;
|
|
1236
|
+
const largerArea = Math.max(leftArea, rightArea);
|
|
1237
|
+
const smallerArea = Math.max(1, Math.min(leftArea, rightArea));
|
|
1238
|
+
const areaRatio = largerArea / smallerArea;
|
|
1239
|
+
const widthRatio = Math.max(left.width, right.width) / Math.max(1, Math.min(left.width, right.width));
|
|
1240
|
+
const heightRatio = Math.max(left.height, right.height) / Math.max(1, Math.min(left.height, right.height));
|
|
1241
|
+
if (areaRatio > 1.6 || widthRatio > 1.35 || heightRatio > 1.35) {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
return this.boundsOverlapRatio(left, right) > 0.7;
|
|
1245
|
+
}
|
|
1246
|
+
static decodedAssetByteLength(asset) {
|
|
1247
|
+
try {
|
|
1248
|
+
return Buffer.from(asset.base64, 'base64').length;
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
return 0;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
static adjustImageFrameForTextColumns(frame, elements, slideWidth, slideHeight) {
|
|
1255
|
+
if (frame.width >= slideWidth * 0.8 && frame.height >= slideHeight * 0.8) {
|
|
1256
|
+
return frame;
|
|
1257
|
+
}
|
|
1258
|
+
const bodyTextElements = elements.filter((element) => element.type === 'text'
|
|
1259
|
+
&& !element.isTitle
|
|
1260
|
+
&& element.x > slideWidth * 0.45);
|
|
1261
|
+
if (bodyTextElements.length < 2 || frame.width < slideWidth * 0.45) {
|
|
1262
|
+
return frame;
|
|
1263
|
+
}
|
|
1264
|
+
const textColumnLeft = Math.min(...bodyTextElements.map((element) => element.x));
|
|
1265
|
+
if (frame.x + frame.width <= textColumnLeft) {
|
|
1266
|
+
return frame;
|
|
1267
|
+
}
|
|
1268
|
+
const availableWidth = Math.max(180, textColumnLeft - 96);
|
|
1269
|
+
if (availableWidth >= frame.width) {
|
|
1270
|
+
return frame;
|
|
1271
|
+
}
|
|
1272
|
+
const scale = availableWidth / Math.max(1, frame.width);
|
|
1273
|
+
const scaledHeight = Math.max(140, Math.round(frame.height * scale));
|
|
1274
|
+
return {
|
|
1275
|
+
x: 72,
|
|
1276
|
+
y: Math.min(Math.max(96, frame.y), slideHeight - scaledHeight - 24),
|
|
1277
|
+
width: availableWidth,
|
|
1278
|
+
height: scaledHeight
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
static applyPanelBackgroundShape(elements, visualSlots, slideWidth, slideHeight, presentationMetrics) {
|
|
1282
|
+
if (elements.some((element) => element.type === 'shape')) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const rightSideText = elements.filter((element) => element.type === 'text'
|
|
1286
|
+
&& !element.isTitle
|
|
1287
|
+
&& element.x >= slideWidth * 0.45);
|
|
1288
|
+
if (rightSideText.length < 2) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const panelSlot = visualSlots
|
|
1292
|
+
.filter((slot) => !!slot.bounds && !slot.isTextSlot && !!slot.fillColor)
|
|
1293
|
+
.map((slot) => ({
|
|
1294
|
+
slot,
|
|
1295
|
+
frame: this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics)
|
|
1296
|
+
}))
|
|
1297
|
+
.find((candidate) => candidate.frame.x >= slideWidth * 0.2
|
|
1298
|
+
&& candidate.frame.width >= slideWidth * 0.5
|
|
1299
|
+
&& candidate.frame.height >= slideHeight * 0.7);
|
|
1300
|
+
if (!panelSlot) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
elements.push({
|
|
1304
|
+
type: 'shape',
|
|
1305
|
+
x: panelSlot.frame.x,
|
|
1306
|
+
y: panelSlot.frame.y,
|
|
1307
|
+
width: panelSlot.frame.width,
|
|
1308
|
+
height: panelSlot.frame.height,
|
|
1309
|
+
zIndex: -2,
|
|
1310
|
+
fillColor: panelSlot.slot.fillVisible === false ? undefined : panelSlot.slot.fillColor,
|
|
1311
|
+
borderColor: panelSlot.slot.borderVisible === false ? undefined : panelSlot.slot.borderColor,
|
|
1312
|
+
borderWidthPx: panelSlot.slot.borderVisible === false ? undefined : panelSlot.slot.borderWidthPx
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
static demoteDecorativeImages(elements) {
|
|
1316
|
+
const contentElements = elements.filter((el) => (el.type === 'text' || el.type === 'image') && el.zIndex >= 0);
|
|
1317
|
+
if (contentElements.length < 3) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
elements.forEach((el) => {
|
|
1321
|
+
if (el.type !== 'image' || el.zIndex < 0) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const imgBounds = { x: el.x, y: el.y, width: el.width, height: el.height };
|
|
1325
|
+
// Count how many content elements this image overlaps with
|
|
1326
|
+
const overlapping = contentElements.filter((other) => {
|
|
1327
|
+
if (other === el || other.zIndex < 0) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
const otherBounds = { x: other.x, y: other.y, width: other.width, height: other.height };
|
|
1331
|
+
// Any intersection counts
|
|
1332
|
+
return imgBounds.x < otherBounds.x + otherBounds.width
|
|
1333
|
+
&& imgBounds.x + imgBounds.width > otherBounds.x
|
|
1334
|
+
&& imgBounds.y < otherBounds.y + otherBounds.height
|
|
1335
|
+
&& imgBounds.y + imgBounds.height > otherBounds.y;
|
|
1336
|
+
});
|
|
1337
|
+
// If image overlaps with 2+ content elements and has higher z, demote it
|
|
1338
|
+
if (overlapping.length >= 2) {
|
|
1339
|
+
const minOverlapZ = Math.min(...overlapping.map((o) => o.zIndex));
|
|
1340
|
+
if (el.zIndex > minOverlapZ) {
|
|
1341
|
+
// Heavily decorative images (overlapping 4+) go behind shapes
|
|
1342
|
+
el.zIndex = overlapping.length >= 4 ? -3 : minOverlapZ - 1;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
static ensureTextAboveShapes(elements) {
|
|
1348
|
+
const shapeElements = elements.filter((el) => el.type === 'shape' && el.zIndex >= 0);
|
|
1349
|
+
if (shapeElements.length === 0) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const maxShapeZ = Math.max(...shapeElements.map((el) => el.zIndex));
|
|
1353
|
+
elements.forEach((el) => {
|
|
1354
|
+
if (el.type !== 'text' || el.zIndex > maxShapeZ) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
// Check if text overlaps with any shape
|
|
1358
|
+
const overlapsShape = shapeElements.some((shape) => this.boundsOverlapRatio({ x: el.x, y: el.y, width: el.width, height: el.height }, { x: shape.x, y: shape.y, width: shape.width, height: shape.height }) > 0.3);
|
|
1359
|
+
if (overlapsShape) {
|
|
1360
|
+
el.zIndex = maxShapeZ + 1 + el.zIndex;
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
static applyTableGridBorders(elements, slideRecord, slideWidth, slideHeight, presentationMetrics) {
|
|
1365
|
+
// Collect text elements that are not titles
|
|
1366
|
+
const textElements = elements.filter((el) => el.type === 'text' && !el.isTitle && el.width < slideWidth * 0.5);
|
|
1367
|
+
if (textElements.length < 6) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
// Detect grid pattern: check for consistent column positions
|
|
1371
|
+
const xPositions = [...new Set(textElements.map((el) => el.x))].sort((a, b) => a - b);
|
|
1372
|
+
const yPositions = [...new Set(textElements.map((el) => el.y))].sort((a, b) => a - b);
|
|
1373
|
+
if (xPositions.length < 2 || yPositions.length < 3) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
// Check consistent width within columns
|
|
1377
|
+
const widthsByX = new Map();
|
|
1378
|
+
textElements.forEach((el) => {
|
|
1379
|
+
const arr = widthsByX.get(el.x) ?? [];
|
|
1380
|
+
arr.push(el.width);
|
|
1381
|
+
widthsByX.set(el.x, arr);
|
|
1382
|
+
});
|
|
1383
|
+
const consistentWidth = [...widthsByX.values()].every((widths) => {
|
|
1384
|
+
const first = widths[0];
|
|
1385
|
+
return widths.every((w) => Math.abs(w - first) < 20);
|
|
1386
|
+
});
|
|
1387
|
+
if (!consistentWidth) {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
// Check consistent height across rows
|
|
1391
|
+
const heightsByY = new Map();
|
|
1392
|
+
textElements.forEach((el) => {
|
|
1393
|
+
const arr = heightsByY.get(el.y) ?? [];
|
|
1394
|
+
arr.push(el.height);
|
|
1395
|
+
heightsByY.set(el.y, arr);
|
|
1396
|
+
});
|
|
1397
|
+
const consistentHeight = [...heightsByY.values()].every((heights) => {
|
|
1398
|
+
const first = heights[0];
|
|
1399
|
+
return heights.every((h) => Math.abs(h - first) < 20);
|
|
1400
|
+
});
|
|
1401
|
+
if (!consistentHeight) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
// Extract line shapes from the slide record to determine border color
|
|
1405
|
+
let gridBorderColor = '#000000';
|
|
1406
|
+
const lineShapes = this.extractLineShapesFromRecord(slideRecord, presentationMetrics);
|
|
1407
|
+
if (lineShapes.length > 0) {
|
|
1408
|
+
// Use the most common line color
|
|
1409
|
+
const colorCounts = new Map();
|
|
1410
|
+
lineShapes.forEach((ls) => {
|
|
1411
|
+
colorCounts.set(ls.color, (colorCounts.get(ls.color) ?? 0) + 1);
|
|
1412
|
+
});
|
|
1413
|
+
let maxCount = 0;
|
|
1414
|
+
colorCounts.forEach((count, color) => {
|
|
1415
|
+
if (count > maxCount) {
|
|
1416
|
+
maxCount = count;
|
|
1417
|
+
gridBorderColor = color;
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
// Find the most common cell width to identify actual grid cells
|
|
1422
|
+
const widthCounts = new Map();
|
|
1423
|
+
textElements.forEach((el) => {
|
|
1424
|
+
const w = Math.round(el.width);
|
|
1425
|
+
widthCounts.set(w, (widthCounts.get(w) ?? 0) + 1);
|
|
1426
|
+
});
|
|
1427
|
+
let gridCellWidth = 0;
|
|
1428
|
+
let maxWidthCount = 0;
|
|
1429
|
+
widthCounts.forEach((count, w) => {
|
|
1430
|
+
if (count > maxWidthCount) {
|
|
1431
|
+
maxWidthCount = count;
|
|
1432
|
+
gridCellWidth = w;
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
// Determine grid boundaries from the dominant-width cells
|
|
1436
|
+
const gridCells = textElements.filter((el) => Math.abs(el.width - gridCellWidth) < 20);
|
|
1437
|
+
const gridMinX = Math.min(...gridCells.map((el) => el.x));
|
|
1438
|
+
const gridMaxX = Math.max(...gridCells.map((el) => el.x + el.width));
|
|
1439
|
+
const gridMinY = Math.min(...gridCells.map((el) => el.y));
|
|
1440
|
+
const gridMaxY = Math.max(...gridCells.map((el) => el.y + el.height));
|
|
1441
|
+
// Apply borders to grid cells and any text elements within the grid area
|
|
1442
|
+
textElements.forEach((el) => {
|
|
1443
|
+
const inGrid = Math.abs(el.width - gridCellWidth) < 20
|
|
1444
|
+
|| (el.x >= gridMinX - 20 && el.x + el.width <= gridMaxX + 20
|
|
1445
|
+
&& el.y >= gridMinY - 20 && el.y + el.height <= gridMaxY + 20);
|
|
1446
|
+
if (!inGrid) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (!el.borderColor) {
|
|
1450
|
+
el.borderColor = gridBorderColor;
|
|
1451
|
+
}
|
|
1452
|
+
if (el.borderWidthPx === undefined) {
|
|
1453
|
+
el.borderWidthPx = 1;
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
static extractLineShapesFromRecord(record, presentationMetrics) {
|
|
1458
|
+
const lines = [];
|
|
1459
|
+
const visit = (list) => {
|
|
1460
|
+
for (const child of list) {
|
|
1461
|
+
if (child.recType === 0xf004 && child.children) {
|
|
1462
|
+
const hasTextbox = child.children.some((c) => c.recType === 0xf00d);
|
|
1463
|
+
if (!hasTextbox) {
|
|
1464
|
+
// Check for zero-dimension shapes (lines)
|
|
1465
|
+
let anchor = null;
|
|
1466
|
+
for (const c of child.children) {
|
|
1467
|
+
if (c.recType === 0xf00f && c.payload.length >= 16) {
|
|
1468
|
+
anchor = {
|
|
1469
|
+
left: c.payload.readInt32LE(0),
|
|
1470
|
+
top: c.payload.readInt32LE(4),
|
|
1471
|
+
right: c.payload.readInt32LE(8),
|
|
1472
|
+
bottom: c.payload.readInt32LE(12)
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (anchor && (anchor.left === anchor.right || anchor.top === anchor.bottom)) {
|
|
1477
|
+
// This is a line shape
|
|
1478
|
+
const fopt = child.children.find((c) => c.recType === 0xf00b);
|
|
1479
|
+
if (fopt) {
|
|
1480
|
+
let borderColor;
|
|
1481
|
+
let borderWidthEmu = 12700;
|
|
1482
|
+
for (let i = 0; i < fopt.recInstance; i++) {
|
|
1483
|
+
const off = i * 6;
|
|
1484
|
+
if (off + 6 > fopt.payload.length)
|
|
1485
|
+
break;
|
|
1486
|
+
const rawOpid = fopt.payload.readUInt16LE(off);
|
|
1487
|
+
const opid = rawOpid & 0x3fff;
|
|
1488
|
+
const isComplex = (rawOpid & 0x8000) !== 0;
|
|
1489
|
+
if (isComplex)
|
|
1490
|
+
continue;
|
|
1491
|
+
const value = fopt.payload.readUInt32LE(off + 2);
|
|
1492
|
+
if (opid === 0x01c0) {
|
|
1493
|
+
const flag = (value >>> 24) & 0xff;
|
|
1494
|
+
if (flag === 0) {
|
|
1495
|
+
const r = value & 0xff;
|
|
1496
|
+
const g = (value >>> 8) & 0xff;
|
|
1497
|
+
const b = (value >>> 16) & 0xff;
|
|
1498
|
+
borderColor = this.rgbToHex(r, g, b);
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
// Scheme color — default to black
|
|
1502
|
+
borderColor = '#000000';
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (opid === 0x01cb) {
|
|
1506
|
+
borderWidthEmu = value;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (borderColor) {
|
|
1510
|
+
lines.push({
|
|
1511
|
+
color: borderColor,
|
|
1512
|
+
widthPx: Math.max(1, Math.round(borderWidthEmu / 9525))
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (child.children) {
|
|
1520
|
+
visit(child.children);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
visit(record.children ?? []);
|
|
1525
|
+
return lines;
|
|
1526
|
+
}
|
|
1527
|
+
static resolveTextElementOverlaps(elements, slideHeight, fixedElements) {
|
|
1528
|
+
const textElements = elements
|
|
1529
|
+
.filter((element) => element.type === 'text' && !!element.paragraphs)
|
|
1530
|
+
.sort((left, right) => left.y - right.y || left.x - right.x);
|
|
1531
|
+
for (let index = 0; index < textElements.length; index++) {
|
|
1532
|
+
const current = textElements[index];
|
|
1533
|
+
for (let nextIndex = index + 1; nextIndex < textElements.length; nextIndex++) {
|
|
1534
|
+
const next = textElements[nextIndex];
|
|
1535
|
+
if (!this.boundsIntersect(current, next)) {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const overlapY = (current.y + current.height) - next.y;
|
|
1539
|
+
if (overlapY <= 2) {
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
// Determine which element should move.
|
|
1543
|
+
// Title elements have priority — body text moves out of the way.
|
|
1544
|
+
if (!current.isTitle && next.isTitle) {
|
|
1545
|
+
// Body overlaps with a title below it → push body below title
|
|
1546
|
+
const newY = next.y + next.height + 12;
|
|
1547
|
+
current.y = newY;
|
|
1548
|
+
const maxHeight = slideHeight - newY - 24;
|
|
1549
|
+
if (maxHeight > 0 && current.height > maxHeight) {
|
|
1550
|
+
current.height = maxHeight;
|
|
1551
|
+
}
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
// Skip pushing fixed elements unless current is title
|
|
1555
|
+
if (fixedElements?.has(next) && !current.isTitle) {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const newY = current.y + current.height + 12;
|
|
1559
|
+
next.y = newY;
|
|
1560
|
+
// Shrink height so the element stays within the slide
|
|
1561
|
+
const maxHeight = slideHeight - newY - 24;
|
|
1562
|
+
if (maxHeight > 0 && next.height > maxHeight) {
|
|
1563
|
+
next.height = maxHeight;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
static resolveTextImageOverlaps(elements, slideWidth, slideHeight, fixedElements) {
|
|
1569
|
+
const textElements = elements.filter((el) => el.type === 'text');
|
|
1570
|
+
const imageElements = elements.filter((el) => el.type === 'image');
|
|
1571
|
+
if (textElements.length === 0 || imageElements.length === 0) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
for (const text of textElements) {
|
|
1575
|
+
if (fixedElements?.has(text)) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
for (const image of imageElements) {
|
|
1579
|
+
if (!this.boundsIntersect(text, image)) {
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (text.zIndex > image.zIndex) {
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const textBottom = text.y + text.height;
|
|
1586
|
+
const imageBottom = image.y + image.height;
|
|
1587
|
+
const overlapTop = Math.max(text.y, image.y);
|
|
1588
|
+
const overlapBottom = Math.min(textBottom, imageBottom);
|
|
1589
|
+
const overlapHeight = overlapBottom - overlapTop;
|
|
1590
|
+
if (overlapHeight <= 0) {
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
const textArea = text.width * text.height;
|
|
1594
|
+
const imageArea = image.width * image.height;
|
|
1595
|
+
if (textArea >= imageArea) {
|
|
1596
|
+
// Text is larger: move image below text
|
|
1597
|
+
image.y = text.y + text.height + 12;
|
|
1598
|
+
if (image.y + image.height > slideHeight) {
|
|
1599
|
+
image.height = Math.max(60, slideHeight - image.y - 12);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
// Image is larger: move text below image
|
|
1604
|
+
text.y = image.y + image.height + 12;
|
|
1605
|
+
if (text.y + text.height > slideHeight) {
|
|
1606
|
+
text.height = Math.max(36, slideHeight - text.y - 12);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
static isBulletTextType(textType, fallback) {
|
|
1613
|
+
if (textType === undefined) {
|
|
1614
|
+
return fallback;
|
|
1615
|
+
}
|
|
1616
|
+
return textType === 1 || textType === 5 || textType === 7 || textType === 8;
|
|
1617
|
+
}
|
|
1618
|
+
static extractOutlineTextByPersistId(records) {
|
|
1619
|
+
const byPersistId = new Map();
|
|
1620
|
+
const visit = (list) => {
|
|
1621
|
+
list.forEach((record) => {
|
|
1622
|
+
if (record.recType === 4080 && record.children && record.children.length > 0) {
|
|
1623
|
+
this.consumeSlideListWithText(record.children, byPersistId);
|
|
1624
|
+
}
|
|
1625
|
+
if (record.children && record.children.length > 0) {
|
|
1626
|
+
visit(record.children);
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
};
|
|
1630
|
+
visit(records);
|
|
1631
|
+
return byPersistId;
|
|
1632
|
+
}
|
|
1633
|
+
static consumeSlideListWithText(children, byPersistId) {
|
|
1634
|
+
let currentPersistId = null;
|
|
1635
|
+
let currentTextType;
|
|
1636
|
+
for (const child of children) {
|
|
1637
|
+
if (child.recType === 1011 && child.payload.length >= 4) {
|
|
1638
|
+
currentPersistId = child.payload.readUInt32LE(0);
|
|
1639
|
+
currentTextType = undefined;
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (child.recType === 3999 && child.payload.length >= 4) {
|
|
1643
|
+
currentTextType = child.payload.readUInt32LE(0);
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
if (currentPersistId === null) {
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
const text = this.decodeTextAtom(child);
|
|
1650
|
+
if (!text) {
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const existing = byPersistId.get(currentPersistId) ?? [];
|
|
1654
|
+
existing.push({
|
|
1655
|
+
text,
|
|
1656
|
+
textType: currentTextType
|
|
1657
|
+
});
|
|
1658
|
+
byPersistId.set(currentPersistId, existing);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
static decodeTextAtom(record) {
|
|
1662
|
+
if (record.recType === 4000) {
|
|
1663
|
+
if (record.payload.length < 2 || record.payload.length % 2 !== 0) {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
return record.payload.toString('utf16le').replace(/\u0000/g, '');
|
|
1667
|
+
}
|
|
1668
|
+
if (record.recType === 4008) {
|
|
1669
|
+
if (record.payload.length === 0) {
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
return this.decodeLegacyByteText(record.payload);
|
|
1673
|
+
}
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
static decodeLegacyByteText(payload) {
|
|
1677
|
+
const candidates = [
|
|
1678
|
+
this.tryDecodeText(payload, 'euc-kr'),
|
|
1679
|
+
payload.toString('latin1')
|
|
1680
|
+
].filter((value) => typeof value === 'string' && value.length > 0);
|
|
1681
|
+
if (candidates.length === 0) {
|
|
1682
|
+
return '';
|
|
1683
|
+
}
|
|
1684
|
+
return candidates.sort((left, right) => this.scoreDecodedText(right) - this.scoreDecodedText(left))[0];
|
|
1685
|
+
}
|
|
1686
|
+
static tryDecodeText(payload, encoding) {
|
|
1687
|
+
try {
|
|
1688
|
+
const decoder = new TextDecoder(encoding, { fatal: false });
|
|
1689
|
+
return decoder.decode(payload);
|
|
1690
|
+
}
|
|
1691
|
+
catch {
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
static scoreDecodedText(text) {
|
|
1696
|
+
const normalized = (text || '').replace(/\u0000/g, '').trim();
|
|
1697
|
+
if (!normalized) {
|
|
1698
|
+
return 0;
|
|
1699
|
+
}
|
|
1700
|
+
let score = 0;
|
|
1701
|
+
let readable = 0;
|
|
1702
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
1703
|
+
const code = normalized.charCodeAt(i);
|
|
1704
|
+
const isBasicPrintable = code >= 32 && code <= 126;
|
|
1705
|
+
const isKorean = code >= 0xac00 && code <= 0xd7a3;
|
|
1706
|
+
if (isBasicPrintable || isKorean) {
|
|
1707
|
+
readable += 1;
|
|
1708
|
+
}
|
|
1709
|
+
if (isKorean) {
|
|
1710
|
+
score += 3;
|
|
1711
|
+
}
|
|
1712
|
+
else if (isBasicPrintable) {
|
|
1713
|
+
score += 1;
|
|
1714
|
+
}
|
|
1715
|
+
else if (normalized[i] === '\ufffd') {
|
|
1716
|
+
score -= 3;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return score + readable / Math.max(1, normalized.length);
|
|
1720
|
+
}
|
|
1721
|
+
static extractTypedTextBlocksFromRecord(record) {
|
|
1722
|
+
const blocks = [];
|
|
1723
|
+
const visit = (list) => {
|
|
1724
|
+
let currentTextType;
|
|
1725
|
+
for (const child of list) {
|
|
1726
|
+
if (child.recType === 3999 && child.payload.length >= 4) {
|
|
1727
|
+
currentTextType = child.payload.readUInt32LE(0);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const text = this.decodeTextAtom(child);
|
|
1731
|
+
if (text) {
|
|
1732
|
+
blocks.push({
|
|
1733
|
+
text,
|
|
1734
|
+
textType: currentTextType
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
if (child.children && child.children.length > 0) {
|
|
1738
|
+
visit(child.children);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
visit(record.children ?? []);
|
|
1743
|
+
return blocks.filter((block) => block.textType !== 2);
|
|
1744
|
+
}
|
|
1745
|
+
static extractShapeTextGroupsFromRecord(record, counter) {
|
|
1746
|
+
const groups = [];
|
|
1747
|
+
const spIndex = counter ?? { value: 0 };
|
|
1748
|
+
const visit = (list, insideShapeContainer, groupTransforms) => {
|
|
1749
|
+
for (const child of list) {
|
|
1750
|
+
if (child.recType === 0xf003 && child.children && child.children.length > 0) {
|
|
1751
|
+
const transform = this.extractGroupTransform(child);
|
|
1752
|
+
const newTransforms = transform
|
|
1753
|
+
? [...groupTransforms, transform]
|
|
1754
|
+
: groupTransforms;
|
|
1755
|
+
const groupChildren = transform ? child.children.slice(1) : child.children;
|
|
1756
|
+
visit(groupChildren, insideShapeContainer, newTransforms);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (child.recType === 0xf004 && child.children && child.children.length > 0) {
|
|
1760
|
+
const currentIndex = spIndex.value++;
|
|
1761
|
+
const hasClientTextbox = child.children.some((entry) => entry.recType === 0xf00d);
|
|
1762
|
+
if (hasClientTextbox) {
|
|
1763
|
+
const groupBlocks = this.extractTypedTextBlocksFromSequence(child.children);
|
|
1764
|
+
if (groupBlocks.length > 0) {
|
|
1765
|
+
let bounds = this.extractShapeBoundsFromSpContainer(child);
|
|
1766
|
+
if (bounds && groupTransforms.length > 0) {
|
|
1767
|
+
bounds = this.applyGroupTransforms(bounds, groupTransforms);
|
|
1768
|
+
}
|
|
1769
|
+
groups.push({
|
|
1770
|
+
blocks: groupBlocks,
|
|
1771
|
+
bounds,
|
|
1772
|
+
spContainerIndex: currentIndex,
|
|
1773
|
+
...this.extractShapeStyleFromSpContainer(child)
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
visit(child.children, true, groupTransforms);
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (!insideShapeContainer && child.recType === 0xf00d && child.children && child.children.length > 0) {
|
|
1781
|
+
const groupBlocks = this.extractTypedTextBlocksFromSequence(child.children);
|
|
1782
|
+
if (groupBlocks.length > 0) {
|
|
1783
|
+
groups.push({ blocks: groupBlocks });
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (child.children && child.children.length > 0 && child.recType !== 0xf003) {
|
|
1787
|
+
visit(child.children, insideShapeContainer, groupTransforms);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
visit(record.children ?? [], false, []);
|
|
1792
|
+
return groups;
|
|
1793
|
+
}
|
|
1794
|
+
static extractStyledTextBlocksFromShapes(record) {
|
|
1795
|
+
const blocks = [];
|
|
1796
|
+
const visit = (list, groupTransforms) => {
|
|
1797
|
+
for (const child of list) {
|
|
1798
|
+
if (child.recType === 0xf003 && child.children && child.children.length > 0) {
|
|
1799
|
+
const transform = this.extractGroupTransform(child);
|
|
1800
|
+
const newTransforms = transform
|
|
1801
|
+
? [...groupTransforms, transform]
|
|
1802
|
+
: groupTransforms;
|
|
1803
|
+
const groupChildren = transform ? child.children.slice(1) : child.children;
|
|
1804
|
+
visit(groupChildren, newTransforms);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (child.recType === 0xf004 && child.children && child.children.length > 0) {
|
|
1808
|
+
const hasClientTextbox = child.children.some((entry) => entry.recType === 0xf00d);
|
|
1809
|
+
if (!hasClientTextbox) {
|
|
1810
|
+
const strings = this.extractStyledStringsFromSpContainer(child);
|
|
1811
|
+
strings.forEach((item) => {
|
|
1812
|
+
let bounds = this.extractShapeBoundsFromSpContainer(child);
|
|
1813
|
+
if (bounds && groupTransforms.length > 0) {
|
|
1814
|
+
bounds = this.applyGroupTransforms(bounds, groupTransforms);
|
|
1815
|
+
}
|
|
1816
|
+
// For very tall shapes (decorative shapes like pentagons),
|
|
1817
|
+
// position text at bottom portion
|
|
1818
|
+
if (bounds && bounds.height > bounds.width * 2) {
|
|
1819
|
+
const textHeight = Math.min(bounds.height * 0.15, 300);
|
|
1820
|
+
bounds = {
|
|
1821
|
+
x: bounds.x - bounds.width * 0.5,
|
|
1822
|
+
y: bounds.y + bounds.height - textHeight,
|
|
1823
|
+
width: bounds.width * 2,
|
|
1824
|
+
height: textHeight
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
blocks.push({
|
|
1828
|
+
text: item.text,
|
|
1829
|
+
textType: 0,
|
|
1830
|
+
bounds,
|
|
1831
|
+
color: item.color,
|
|
1832
|
+
fontSizePx: item.fontSizePx
|
|
1833
|
+
});
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (child.children && child.children.length > 0 && child.recType !== 0xf003) {
|
|
1838
|
+
visit(child.children, groupTransforms);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
visit(record.children ?? [], []);
|
|
1843
|
+
return blocks;
|
|
1844
|
+
}
|
|
1845
|
+
static extractStyledStringsFromSpContainer(record) {
|
|
1846
|
+
const fopt = (record.children ?? []).find((child) => child.recType === 0xf00b && child.payload.length >= 6);
|
|
1847
|
+
if (!fopt) {
|
|
1848
|
+
return [];
|
|
1849
|
+
}
|
|
1850
|
+
const propertyBytes = Math.min(fopt.payload.length, fopt.recInstance * 6);
|
|
1851
|
+
const complexPayload = fopt.payload.subarray(propertyBytes);
|
|
1852
|
+
const candidates = complexPayload.toString('utf16le')
|
|
1853
|
+
.replace(/\u0000/g, '\n')
|
|
1854
|
+
.split(/\n+/)
|
|
1855
|
+
.map((value) => value.trim())
|
|
1856
|
+
.filter((value) => value.length >= 2)
|
|
1857
|
+
.filter((value) => /[가-힣]/.test(value))
|
|
1858
|
+
.filter((value) => !/(rrect|화살표|arrow|\-윤고딕)/i.test(value))
|
|
1859
|
+
.filter((value) => !/^(HY|한컴|굴림|돋움|바탕|궁서)/i.test(value))
|
|
1860
|
+
.filter((value) => !/(고딕|명조|체)$/.test(value));
|
|
1861
|
+
const deduped = new Set();
|
|
1862
|
+
const colorHint = this.extractShapeStyleFromSpContainer(record).fillColor;
|
|
1863
|
+
return candidates.flatMap((text) => {
|
|
1864
|
+
if (deduped.has(text) || this.isNoiseText(text)) {
|
|
1865
|
+
return [];
|
|
1866
|
+
}
|
|
1867
|
+
deduped.add(text);
|
|
1868
|
+
const color = colorHint;
|
|
1869
|
+
return [{ text, color }];
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
static extractGroupTransform(spgrContainer) {
|
|
1873
|
+
if (!spgrContainer.children || spgrContainer.children.length === 0) {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
const firstChild = spgrContainer.children[0];
|
|
1877
|
+
if (firstChild.recType !== 0xf004 || !firstChild.children) {
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
let internal;
|
|
1881
|
+
let external;
|
|
1882
|
+
for (const rec of firstChild.children) {
|
|
1883
|
+
// FSPGR (0xf009) defines internal coordinate space
|
|
1884
|
+
if (rec.recType === 0xf009 && rec.payload.length >= 16) {
|
|
1885
|
+
const left = rec.payload.readInt32LE(0);
|
|
1886
|
+
const top = rec.payload.readInt32LE(4);
|
|
1887
|
+
const right = rec.payload.readInt32LE(8);
|
|
1888
|
+
const bottom = rec.payload.readInt32LE(12);
|
|
1889
|
+
internal = this.makeBounds(left, top, right, bottom) ?? undefined;
|
|
1890
|
+
}
|
|
1891
|
+
// ChildAnchor (0xf010) or ClientAnchor (0xf00f) defines position in parent
|
|
1892
|
+
if (rec.recType === 0xf010) {
|
|
1893
|
+
if (rec.payload.length >= 16) {
|
|
1894
|
+
external = this.readRectBounds32(rec.payload) ?? undefined;
|
|
1895
|
+
}
|
|
1896
|
+
else if (rec.payload.length >= 8) {
|
|
1897
|
+
// 8-byte ChildAnchor uses (top, left, right, bottom) format.
|
|
1898
|
+
// For group anchors, always use this fixed byte order instead
|
|
1899
|
+
// of the heuristic in readRectBounds16.
|
|
1900
|
+
const top = rec.payload.readInt16LE(0);
|
|
1901
|
+
const left = rec.payload.readInt16LE(2);
|
|
1902
|
+
const right = rec.payload.readInt16LE(4);
|
|
1903
|
+
const bottom = rec.payload.readInt16LE(6);
|
|
1904
|
+
external = this.makeBounds(left, top, right, bottom) ?? undefined;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (!external && rec.recType === 0xf00f && rec.payload.length >= 16) {
|
|
1908
|
+
external = this.readRectBounds32(rec.payload) ?? undefined;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
if (!internal || !external || internal.width <= 0 || internal.height <= 0) {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
return { internal, external };
|
|
1915
|
+
}
|
|
1916
|
+
static applyGroupTransforms(bounds, transforms) {
|
|
1917
|
+
let result = bounds;
|
|
1918
|
+
for (let i = transforms.length - 1; i >= 0; i--) {
|
|
1919
|
+
const { internal, external } = transforms[i];
|
|
1920
|
+
const scaleX = external.width / internal.width;
|
|
1921
|
+
const scaleY = external.height / internal.height;
|
|
1922
|
+
result = {
|
|
1923
|
+
x: external.x + (result.x - internal.x) * scaleX,
|
|
1924
|
+
y: external.y + (result.y - internal.y) * scaleY,
|
|
1925
|
+
width: result.width * scaleX,
|
|
1926
|
+
height: result.height * scaleY
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
return result;
|
|
1930
|
+
}
|
|
1931
|
+
static extractVisualSlotsFromRecord(record, counter) {
|
|
1932
|
+
const slots = [];
|
|
1933
|
+
const spIndex = counter ?? { value: 0 };
|
|
1934
|
+
const visit = (list, groupTransforms) => {
|
|
1935
|
+
for (const child of list) {
|
|
1936
|
+
if (child.recType === 0xf003 && child.children && child.children.length > 0) {
|
|
1937
|
+
const transform = this.extractGroupTransform(child);
|
|
1938
|
+
const newTransforms = transform
|
|
1939
|
+
? [...groupTransforms, transform]
|
|
1940
|
+
: groupTransforms;
|
|
1941
|
+
// Skip the first SpContainer (group definition shape)
|
|
1942
|
+
const groupChildren = transform ? child.children.slice(1) : child.children;
|
|
1943
|
+
visit(groupChildren, newTransforms);
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
if (child.recType === 0xf004 && child.children && child.children.length > 0) {
|
|
1947
|
+
const currentIndex = spIndex.value++;
|
|
1948
|
+
const hasClientTextbox = child.children.some((entry) => entry.recType === 0xf00d);
|
|
1949
|
+
let bounds = this.extractShapeBoundsFromSpContainer(child);
|
|
1950
|
+
if (bounds && groupTransforms.length > 0) {
|
|
1951
|
+
bounds = this.applyGroupTransforms(bounds, groupTransforms);
|
|
1952
|
+
}
|
|
1953
|
+
if (!hasClientTextbox) {
|
|
1954
|
+
const style = this.extractShapeStyleFromSpContainer(child);
|
|
1955
|
+
if (bounds) {
|
|
1956
|
+
slots.push({
|
|
1957
|
+
bounds,
|
|
1958
|
+
imageRefId: this.extractShapeImageRefFromSpContainer(child),
|
|
1959
|
+
...style,
|
|
1960
|
+
isTextSlot: false,
|
|
1961
|
+
spContainerIndex: currentIndex
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
const style = this.extractShapeStyleFromSpContainer(child);
|
|
1967
|
+
if (bounds || style.fillColor || style.borderColor || style.borderWidthPx !== undefined) {
|
|
1968
|
+
slots.push({
|
|
1969
|
+
bounds,
|
|
1970
|
+
imageRefId: this.extractShapeImageRefFromSpContainer(child),
|
|
1971
|
+
...style,
|
|
1972
|
+
isTextSlot: true,
|
|
1973
|
+
spContainerIndex: currentIndex
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (child.children && child.children.length > 0 && child.recType !== 0xf003) {
|
|
1979
|
+
visit(child.children, groupTransforms);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
visit(record.children ?? [], []);
|
|
1984
|
+
return slots;
|
|
1985
|
+
}
|
|
1986
|
+
static extractShapeBoundsFromSpContainer(record) {
|
|
1987
|
+
for (const child of record.children ?? []) {
|
|
1988
|
+
if (child.recType === 0xf00f && child.payload.length >= 16) {
|
|
1989
|
+
return this.readRectBounds32(child.payload);
|
|
1990
|
+
}
|
|
1991
|
+
if (child.recType === 0xf010) {
|
|
1992
|
+
if (child.payload.length >= 16) {
|
|
1993
|
+
return this.readRectBounds32(child.payload);
|
|
1994
|
+
}
|
|
1995
|
+
if (child.payload.length >= 8) {
|
|
1996
|
+
return this.readRectBounds16(child.payload);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return undefined;
|
|
2001
|
+
}
|
|
2002
|
+
static extractShapeStyleFromSpContainer(record) {
|
|
2003
|
+
for (const child of record.children ?? []) {
|
|
2004
|
+
if (child.recType !== 0xf00b || child.payload.length < 6) {
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
const propertyCount = child.recInstance;
|
|
2008
|
+
let fillColor;
|
|
2009
|
+
let borderColor;
|
|
2010
|
+
let borderWidthPx;
|
|
2011
|
+
let fillVisible;
|
|
2012
|
+
let borderVisible;
|
|
2013
|
+
for (let index = 0; index < propertyCount; index++) {
|
|
2014
|
+
const offset = index * 6;
|
|
2015
|
+
if (offset + 6 > child.payload.length) {
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
const rawOpid = child.payload.readUInt16LE(offset);
|
|
2019
|
+
const opid = rawOpid & 0x3fff;
|
|
2020
|
+
const isComplex = (rawOpid & 0x8000) !== 0;
|
|
2021
|
+
if (isComplex) {
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
const value = child.payload.readUInt32LE(offset + 2);
|
|
2025
|
+
if (opid === 0x0181) {
|
|
2026
|
+
fillColor = this.readOfficeArtColorRef(value) ?? fillColor;
|
|
2027
|
+
}
|
|
2028
|
+
else if (opid === 0x01c0) {
|
|
2029
|
+
borderColor = this.readOfficeArtColorRef(value) ?? borderColor;
|
|
2030
|
+
}
|
|
2031
|
+
else if (opid === 0x01cb) {
|
|
2032
|
+
borderWidthPx = this.emuToPixels(value);
|
|
2033
|
+
}
|
|
2034
|
+
else if (opid === 0x01bf) {
|
|
2035
|
+
borderVisible = value !== 0;
|
|
2036
|
+
}
|
|
2037
|
+
else if (opid === 0x01bf - 0x3f) {
|
|
2038
|
+
fillVisible = value !== 0;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (fillColor || borderColor || borderWidthPx !== undefined || fillVisible !== undefined || borderVisible !== undefined) {
|
|
2042
|
+
return { fillColor, borderColor, borderWidthPx, fillVisible, borderVisible };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return {};
|
|
2046
|
+
}
|
|
2047
|
+
static extractShapeImageRefFromSpContainer(record) {
|
|
2048
|
+
for (const child of record.children ?? []) {
|
|
2049
|
+
if (child.recType !== 0xf00b || child.payload.length < 6) {
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const propertyCount = child.recInstance;
|
|
2053
|
+
for (let index = 0; index < propertyCount; index++) {
|
|
2054
|
+
const offset = index * 6;
|
|
2055
|
+
if (offset + 6 > child.payload.length) {
|
|
2056
|
+
break;
|
|
2057
|
+
}
|
|
2058
|
+
const rawOpid = child.payload.readUInt16LE(offset);
|
|
2059
|
+
const opid = rawOpid & 0x3fff;
|
|
2060
|
+
const isComplex = (rawOpid & 0x8000) !== 0;
|
|
2061
|
+
const isBlipId = (rawOpid & 0x4000) !== 0;
|
|
2062
|
+
if (isComplex || !isBlipId) {
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (opid === 0x0104 || opid === 0x0186) {
|
|
2066
|
+
const value = child.payload.readUInt32LE(offset + 2);
|
|
2067
|
+
if (value > 0) {
|
|
2068
|
+
return value;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}
|
|
2075
|
+
static readRectBounds32(payload) {
|
|
2076
|
+
if (payload.length < 16) {
|
|
2077
|
+
return undefined;
|
|
2078
|
+
}
|
|
2079
|
+
const left = payload.readInt32LE(0);
|
|
2080
|
+
const top = payload.readInt32LE(4);
|
|
2081
|
+
const right = payload.readInt32LE(8);
|
|
2082
|
+
const bottom = payload.readInt32LE(12);
|
|
2083
|
+
return this.makeBounds(left, top, right, bottom);
|
|
2084
|
+
}
|
|
2085
|
+
static readRectBounds16(payload) {
|
|
2086
|
+
if (payload.length < 8) {
|
|
2087
|
+
return undefined;
|
|
2088
|
+
}
|
|
2089
|
+
const first = payload.readInt16LE(0);
|
|
2090
|
+
const second = payload.readInt16LE(2);
|
|
2091
|
+
const third = payload.readInt16LE(4);
|
|
2092
|
+
const fourth = payload.readInt16LE(6);
|
|
2093
|
+
const standard = this.makeBounds(first, second, third, fourth);
|
|
2094
|
+
const swapped = this.makeBounds(second, first, third, fourth);
|
|
2095
|
+
if (!standard) {
|
|
2096
|
+
return swapped;
|
|
2097
|
+
}
|
|
2098
|
+
if (!swapped) {
|
|
2099
|
+
return standard;
|
|
2100
|
+
}
|
|
2101
|
+
const standardRatio = standard.width / Math.max(1, standard.height);
|
|
2102
|
+
const swappedRatio = swapped.width / Math.max(1, swapped.height);
|
|
2103
|
+
const standardExtreme = standardRatio < 0.45 || standardRatio > 2.2;
|
|
2104
|
+
const swappedExtreme = swappedRatio < 0.45 || swappedRatio > 2.2;
|
|
2105
|
+
if (standardExtreme && !swappedExtreme) {
|
|
2106
|
+
return swapped;
|
|
2107
|
+
}
|
|
2108
|
+
if (!standardExtreme && swappedExtreme) {
|
|
2109
|
+
return standard;
|
|
2110
|
+
}
|
|
2111
|
+
// Both extreme or both normal: prefer swapped (top, left, right, bottom)
|
|
2112
|
+
// which is the common PPT 8-byte ChildAnchor format.
|
|
2113
|
+
const swappedArea = swapped.width * swapped.height;
|
|
2114
|
+
const standardArea = standard.width * standard.height;
|
|
2115
|
+
if (swappedArea > standardArea * 1.05
|
|
2116
|
+
&& Math.abs(swappedRatio - 1) < Math.abs(standardRatio - 1)) {
|
|
2117
|
+
return swapped;
|
|
2118
|
+
}
|
|
2119
|
+
// When both are extreme, prefer swapped as PPT commonly uses (top, left, right, bottom).
|
|
2120
|
+
if (standardExtreme && swappedExtreme) {
|
|
2121
|
+
return swapped;
|
|
2122
|
+
}
|
|
2123
|
+
return standard;
|
|
2124
|
+
}
|
|
2125
|
+
static makeBounds(left, top, right, bottom) {
|
|
2126
|
+
const width = right - left;
|
|
2127
|
+
const height = bottom - top;
|
|
2128
|
+
if (!Number.isFinite(left) || !Number.isFinite(top) || width <= 0 || height <= 0) {
|
|
2129
|
+
return undefined;
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
x: left,
|
|
2133
|
+
y: top,
|
|
2134
|
+
width,
|
|
2135
|
+
height
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
static readOfficeArtColorRef(value) {
|
|
2139
|
+
const flagByte = (value >>> 24) & 0xff;
|
|
2140
|
+
if (flagByte !== 0) {
|
|
2141
|
+
return undefined;
|
|
2142
|
+
}
|
|
2143
|
+
const red = value & 0xff;
|
|
2144
|
+
const green = (value >>> 8) & 0xff;
|
|
2145
|
+
const blue = (value >>> 16) & 0xff;
|
|
2146
|
+
return this.rgbToHex(red, green, blue);
|
|
2147
|
+
}
|
|
2148
|
+
static emuToPixels(emu) {
|
|
2149
|
+
if (!Number.isFinite(emu) || emu <= 0) {
|
|
2150
|
+
return undefined;
|
|
2151
|
+
}
|
|
2152
|
+
return Math.max(1, Math.round(emu / 9525));
|
|
2153
|
+
}
|
|
2154
|
+
static normalizeBounds(bounds, slideWidth, slideHeight, presentationMetrics) {
|
|
2155
|
+
let scaled = bounds;
|
|
2156
|
+
if (presentationMetrics && presentationMetrics.rawWidth > 0 && presentationMetrics.rawHeight > 0) {
|
|
2157
|
+
const scaleX = slideWidth / presentationMetrics.rawWidth;
|
|
2158
|
+
const scaleY = slideHeight / presentationMetrics.rawHeight;
|
|
2159
|
+
scaled = {
|
|
2160
|
+
x: Math.round(bounds.x * scaleX),
|
|
2161
|
+
y: Math.round(bounds.y * scaleY),
|
|
2162
|
+
width: Math.round(bounds.width * scaleX),
|
|
2163
|
+
height: Math.round(bounds.height * scaleY)
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const width = Math.max(24, Math.min(slideWidth, scaled.width));
|
|
2167
|
+
const height = Math.max(24, Math.min(slideHeight, scaled.height));
|
|
2168
|
+
const maxX = Math.max(0, slideWidth - width);
|
|
2169
|
+
const maxY = Math.max(0, slideHeight - height);
|
|
2170
|
+
const x = Math.max(0, Math.min(maxX, scaled.x));
|
|
2171
|
+
const y = Math.max(0, Math.min(maxY, scaled.y));
|
|
2172
|
+
return { x, y, width, height };
|
|
2173
|
+
}
|
|
2174
|
+
static dedupeVisualSlots(slots) {
|
|
2175
|
+
const sorted = [...slots].sort((left, right) => {
|
|
2176
|
+
const leftArea = (left.bounds?.width ?? 0) * (left.bounds?.height ?? 0);
|
|
2177
|
+
const rightArea = (right.bounds?.width ?? 0) * (right.bounds?.height ?? 0);
|
|
2178
|
+
const leftScore = (left.imageRefId !== undefined ? 1_000_000 : 0) + leftArea;
|
|
2179
|
+
const rightScore = (right.imageRefId !== undefined ? 1_000_000 : 0) + rightArea;
|
|
2180
|
+
return rightScore - leftScore;
|
|
2181
|
+
});
|
|
2182
|
+
const deduped = [];
|
|
2183
|
+
sorted.forEach((slot) => {
|
|
2184
|
+
if (!slot.bounds) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const slotArea = slot.bounds.width * slot.bounds.height;
|
|
2188
|
+
const overlapsExisting = deduped.some((existing) => {
|
|
2189
|
+
if (!existing.bounds) {
|
|
2190
|
+
return false;
|
|
2191
|
+
}
|
|
2192
|
+
const existingArea = existing.bounds.width * existing.bounds.height;
|
|
2193
|
+
if (existingArea > slotArea * 8) {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
// Don't dedup slots with different image references
|
|
2197
|
+
if (slot.imageRefId !== undefined && existing.imageRefId !== undefined
|
|
2198
|
+
&& slot.imageRefId !== existing.imageRefId) {
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
return this.boundsOverlapRatio(slot.bounds, existing.bounds) > 0.72;
|
|
2202
|
+
});
|
|
2203
|
+
if (!overlapsExisting) {
|
|
2204
|
+
deduped.push(slot);
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
return deduped;
|
|
2208
|
+
}
|
|
2209
|
+
static selectPreferredImageRefSlots(slots, picturesById) {
|
|
2210
|
+
if (!picturesById || slots.length < 2) {
|
|
2211
|
+
return slots;
|
|
2212
|
+
}
|
|
2213
|
+
// Count how many blip IDs share the same content to detect master/template images.
|
|
2214
|
+
const assetContentFrequency = new Map();
|
|
2215
|
+
for (const [, asset] of picturesById.entries()) {
|
|
2216
|
+
const sig = asset.base64.substring(0, 200);
|
|
2217
|
+
assetContentFrequency.set(sig, (assetContentFrequency.get(sig) || 0) + 1);
|
|
2218
|
+
}
|
|
2219
|
+
const preferred = [];
|
|
2220
|
+
const consumed = new Set();
|
|
2221
|
+
slots.forEach((slot, index) => {
|
|
2222
|
+
if (!slot.bounds || slot.imageRefId === undefined || consumed.has(index)) {
|
|
2223
|
+
if (!consumed.has(index)) {
|
|
2224
|
+
preferred.push(slot);
|
|
2225
|
+
consumed.add(index);
|
|
2226
|
+
}
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const slotArea = slot.bounds.width * slot.bounds.height;
|
|
2230
|
+
const group = slots
|
|
2231
|
+
.map((candidate, candidateIndex) => ({ candidate, candidateIndex }))
|
|
2232
|
+
.filter(({ candidate, candidateIndex }) => {
|
|
2233
|
+
if (consumed.has(candidateIndex) || !candidate.bounds || candidate.imageRefId === undefined) {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
// Don't group slots with different image refs if they differ significantly in size
|
|
2237
|
+
if (candidate.imageRefId !== slot.imageRefId) {
|
|
2238
|
+
const candidateArea = candidate.bounds.width * candidate.bounds.height;
|
|
2239
|
+
if (Math.max(slotArea, candidateArea) > Math.min(slotArea, candidateArea) * 2) {
|
|
2240
|
+
return false;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return this.boundsOverlapRatio(slot.bounds, candidate.bounds) > 0.95;
|
|
2244
|
+
});
|
|
2245
|
+
const best = group
|
|
2246
|
+
.slice()
|
|
2247
|
+
.sort((left, right) => {
|
|
2248
|
+
const leftAsset = picturesById.get(left.candidate.imageRefId);
|
|
2249
|
+
const rightAsset = picturesById.get(right.candidate.imageRefId);
|
|
2250
|
+
const leftFreq = leftAsset ? (assetContentFrequency.get(leftAsset.base64.substring(0, 200)) || 1) : 1;
|
|
2251
|
+
const rightFreq = rightAsset ? (assetContentFrequency.get(rightAsset.base64.substring(0, 200)) || 1) : 1;
|
|
2252
|
+
// Prefer unique (low frequency) images over master/template (high frequency) duplicates.
|
|
2253
|
+
if (leftFreq !== rightFreq) {
|
|
2254
|
+
return leftFreq - rightFreq;
|
|
2255
|
+
}
|
|
2256
|
+
// Prefer higher spContainerIndex (last drawn = visible on top in PPT rendering).
|
|
2257
|
+
const leftIdx = left.candidate.spContainerIndex ?? -1;
|
|
2258
|
+
const rightIdx = right.candidate.spContainerIndex ?? -1;
|
|
2259
|
+
if (leftIdx !== rightIdx) {
|
|
2260
|
+
return rightIdx - leftIdx;
|
|
2261
|
+
}
|
|
2262
|
+
const leftBytes = leftAsset ? this.decodedAssetByteLength(leftAsset) : 0;
|
|
2263
|
+
const rightBytes = rightAsset ? this.decodedAssetByteLength(rightAsset) : 0;
|
|
2264
|
+
return rightBytes - leftBytes;
|
|
2265
|
+
})[0];
|
|
2266
|
+
if (best) {
|
|
2267
|
+
preferred.push(best.candidate);
|
|
2268
|
+
group.forEach(({ candidateIndex }) => consumed.add(candidateIndex));
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
return preferred;
|
|
2272
|
+
}
|
|
2273
|
+
static boundsOverlapRatio(left, right) {
|
|
2274
|
+
const x1 = Math.max(left.x, right.x);
|
|
2275
|
+
const y1 = Math.max(left.y, right.y);
|
|
2276
|
+
const x2 = Math.min(left.x + left.width, right.x + right.width);
|
|
2277
|
+
const y2 = Math.min(left.y + left.height, right.y + right.height);
|
|
2278
|
+
if (x2 <= x1 || y2 <= y1) {
|
|
2279
|
+
return 0;
|
|
2280
|
+
}
|
|
2281
|
+
const intersection = (x2 - x1) * (y2 - y1);
|
|
2282
|
+
const smallerArea = Math.max(1, Math.min(left.width * left.height, right.width * right.height));
|
|
2283
|
+
return intersection / smallerArea;
|
|
2284
|
+
}
|
|
2285
|
+
static boundsIntersect(left, right) {
|
|
2286
|
+
return left.x < right.x + right.width
|
|
2287
|
+
&& left.x + left.width > right.x
|
|
2288
|
+
&& left.y < right.y + right.height
|
|
2289
|
+
&& left.y + left.height > right.y;
|
|
2290
|
+
}
|
|
2291
|
+
static extractTypedTextBlocksFromSequence(records) {
|
|
2292
|
+
const blocks = [];
|
|
2293
|
+
let currentTextType;
|
|
2294
|
+
let pendingStyleProp;
|
|
2295
|
+
for (let ri = 0; ri < records.length; ri++) {
|
|
2296
|
+
const record = records[ri];
|
|
2297
|
+
if (record.recType === 3999 && record.payload.length >= 4) {
|
|
2298
|
+
currentTextType = record.payload.readUInt32LE(0);
|
|
2299
|
+
continue;
|
|
2300
|
+
}
|
|
2301
|
+
// StyleTextPropAtom (4001) follows its text atom
|
|
2302
|
+
if (record.recType === 4001) {
|
|
2303
|
+
pendingStyleProp = record;
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
const text = this.decodeTextAtom(record);
|
|
2307
|
+
if (!text) {
|
|
2308
|
+
if (record.children && record.children.length > 0) {
|
|
2309
|
+
const nestedBlocks = this.extractTypedTextBlocksFromSequence(record.children);
|
|
2310
|
+
if (nestedBlocks.length > 0) {
|
|
2311
|
+
blocks.push(...nestedBlocks.map((block) => ({
|
|
2312
|
+
...block,
|
|
2313
|
+
textType: block.textType ?? currentTextType
|
|
2314
|
+
})));
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
// Look ahead for StyleTextPropAtom
|
|
2320
|
+
let styleProp = pendingStyleProp;
|
|
2321
|
+
pendingStyleProp = undefined;
|
|
2322
|
+
if (!styleProp) {
|
|
2323
|
+
for (let si = ri + 1; si < records.length && si <= ri + 4; si++) {
|
|
2324
|
+
if (records[si].recType === 4001) {
|
|
2325
|
+
styleProp = records[si];
|
|
2326
|
+
break;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
const style = styleProp ? this.extractTextRunStyle(styleProp.payload) : undefined;
|
|
2331
|
+
blocks.push({
|
|
2332
|
+
text,
|
|
2333
|
+
textType: currentTextType,
|
|
2334
|
+
color: style?.color,
|
|
2335
|
+
fontSizePx: style?.fontSizePx
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
return blocks.filter((block) => block.textType !== 2);
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Extract dominant text color and font size from a StyleTextPropAtom payload.
|
|
2342
|
+
* Parses the paragraph run then character run per MS-PPT spec.
|
|
2343
|
+
*/
|
|
2344
|
+
static extractTextRunStyle(payload) {
|
|
2345
|
+
if (payload.length < 12) {
|
|
2346
|
+
return undefined;
|
|
2347
|
+
}
|
|
2348
|
+
try {
|
|
2349
|
+
let offset = 0;
|
|
2350
|
+
// --- Paragraph run ---
|
|
2351
|
+
// charCount (4) + indentLevel (2) + paraMask (4)
|
|
2352
|
+
offset += 4; // charCount
|
|
2353
|
+
offset += 2; // indentLevel
|
|
2354
|
+
if (offset + 4 > payload.length) {
|
|
2355
|
+
return undefined;
|
|
2356
|
+
}
|
|
2357
|
+
const paraMask = payload.readUInt32LE(offset);
|
|
2358
|
+
offset += 4;
|
|
2359
|
+
// Skip paragraph properties based on mask
|
|
2360
|
+
if (paraMask & 0x000f) {
|
|
2361
|
+
offset += 2;
|
|
2362
|
+
} // bulletFlags
|
|
2363
|
+
if (paraMask & 0x0010) {
|
|
2364
|
+
offset += 2;
|
|
2365
|
+
} // bulletChar
|
|
2366
|
+
if (paraMask & 0x0020) {
|
|
2367
|
+
offset += 2;
|
|
2368
|
+
} // bulletFont
|
|
2369
|
+
if (paraMask & 0x0040) {
|
|
2370
|
+
offset += 2;
|
|
2371
|
+
} // bulletSize
|
|
2372
|
+
if (paraMask & 0x0080) {
|
|
2373
|
+
offset += 4;
|
|
2374
|
+
} // bulletColor
|
|
2375
|
+
if (paraMask & 0x0100) {
|
|
2376
|
+
offset += 2;
|
|
2377
|
+
} // leftMargin
|
|
2378
|
+
if (paraMask & 0x0200) {
|
|
2379
|
+
offset += 2;
|
|
2380
|
+
} // indent
|
|
2381
|
+
if (paraMask & 0x0400) {
|
|
2382
|
+
offset += 2;
|
|
2383
|
+
} // defaultTabSize
|
|
2384
|
+
if (paraMask & 0x0800) {
|
|
2385
|
+
offset += 2;
|
|
2386
|
+
} // alignment
|
|
2387
|
+
if (paraMask & 0x1000) {
|
|
2388
|
+
offset += 2;
|
|
2389
|
+
} // lineSpacing
|
|
2390
|
+
if (paraMask & 0x2000) {
|
|
2391
|
+
offset += 2;
|
|
2392
|
+
} // spaceBefore
|
|
2393
|
+
if (paraMask & 0x4000) {
|
|
2394
|
+
offset += 2;
|
|
2395
|
+
} // spaceAfter
|
|
2396
|
+
// --- Character run ---
|
|
2397
|
+
// charCount (4) + charMask (4)
|
|
2398
|
+
if (offset + 8 > payload.length) {
|
|
2399
|
+
return undefined;
|
|
2400
|
+
}
|
|
2401
|
+
offset += 4; // charCount
|
|
2402
|
+
const charMask = payload.readUInt32LE(offset);
|
|
2403
|
+
offset += 4;
|
|
2404
|
+
// Read character properties in MS-PPT spec order
|
|
2405
|
+
if (charMask & 0x03ff) {
|
|
2406
|
+
offset += 2;
|
|
2407
|
+
} // fontStyle (if any bool bit set)
|
|
2408
|
+
if (charMask & 0x00010000) {
|
|
2409
|
+
offset += 2;
|
|
2410
|
+
} // typeface
|
|
2411
|
+
if (charMask & 0x00100000) {
|
|
2412
|
+
offset += 2;
|
|
2413
|
+
} // oldEATypeface
|
|
2414
|
+
if (charMask & 0x00200000) {
|
|
2415
|
+
offset += 2;
|
|
2416
|
+
} // ansiTypeface
|
|
2417
|
+
if (charMask & 0x00400000) {
|
|
2418
|
+
offset += 2;
|
|
2419
|
+
} // symbolTypeface
|
|
2420
|
+
let fontSizePx;
|
|
2421
|
+
if (charMask & 0x00020000) {
|
|
2422
|
+
if (offset + 2 <= payload.length) {
|
|
2423
|
+
const rawSize = payload.readUInt16LE(offset);
|
|
2424
|
+
if (rawSize >= 6 && rawSize <= 400) {
|
|
2425
|
+
fontSizePx = Math.round(rawSize * this.POINTS_TO_PX);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
offset += 2;
|
|
2429
|
+
}
|
|
2430
|
+
let color;
|
|
2431
|
+
if (charMask & 0x00040000) {
|
|
2432
|
+
if (offset + 4 <= payload.length) {
|
|
2433
|
+
const colorValue = payload.readUInt32LE(offset);
|
|
2434
|
+
const flag = (colorValue >>> 24) & 0xff;
|
|
2435
|
+
if (flag === 0xfe) {
|
|
2436
|
+
const r = colorValue & 0xff;
|
|
2437
|
+
const g = (colorValue >>> 8) & 0xff;
|
|
2438
|
+
const b = (colorValue >>> 16) & 0xff;
|
|
2439
|
+
if (r !== 0 || g !== 0 || b !== 0) {
|
|
2440
|
+
color = this.rgbToHex(r, g, b);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (!color && !fontSizePx) {
|
|
2446
|
+
return undefined;
|
|
2447
|
+
}
|
|
2448
|
+
return { color, fontSizePx };
|
|
2449
|
+
}
|
|
2450
|
+
catch {
|
|
2451
|
+
return undefined;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
static decorateTextBlocksWithPlaceholders(blocks, layout) {
|
|
2455
|
+
if (!layout || blocks.length === 0) {
|
|
2456
|
+
return blocks;
|
|
2457
|
+
}
|
|
2458
|
+
const textPlaceholders = layout.placeholders.filter((value) => this.isTextPlaceholder(value));
|
|
2459
|
+
if (textPlaceholders.length === 0) {
|
|
2460
|
+
return blocks;
|
|
2461
|
+
}
|
|
2462
|
+
return blocks.map((block, index) => ({
|
|
2463
|
+
...block,
|
|
2464
|
+
placeholderType: block.placeholderType ?? textPlaceholders[index]
|
|
2465
|
+
}));
|
|
2466
|
+
}
|
|
2467
|
+
static decorateTextGroupsWithPlaceholders(groups, layout) {
|
|
2468
|
+
if (!layout || groups.length === 0) {
|
|
2469
|
+
return groups;
|
|
2470
|
+
}
|
|
2471
|
+
const textPlaceholders = layout.placeholders.filter((value) => this.isTextPlaceholder(value));
|
|
2472
|
+
if (textPlaceholders.length === 0) {
|
|
2473
|
+
return groups;
|
|
2474
|
+
}
|
|
2475
|
+
return groups.map((group, index) => ({
|
|
2476
|
+
...group,
|
|
2477
|
+
placeholderType: group.placeholderType ?? textPlaceholders[index],
|
|
2478
|
+
blocks: group.blocks.map((block) => ({
|
|
2479
|
+
...block,
|
|
2480
|
+
placeholderType: block.placeholderType ?? textPlaceholders[index]
|
|
2481
|
+
}))
|
|
2482
|
+
}));
|
|
2483
|
+
}
|
|
2484
|
+
static decorateVisualSlotsWithPlaceholders(slots, layout) {
|
|
2485
|
+
if (!layout || slots.length === 0) {
|
|
2486
|
+
return slots;
|
|
2487
|
+
}
|
|
2488
|
+
const placeholders = layout.placeholders.filter((value) => value !== 0x00 && value !== 0xff);
|
|
2489
|
+
if (placeholders.length === 0) {
|
|
2490
|
+
return slots;
|
|
2491
|
+
}
|
|
2492
|
+
return slots.map((slot, index) => ({
|
|
2493
|
+
...slot,
|
|
2494
|
+
placeholderType: slot.placeholderType ?? placeholders[index]
|
|
2495
|
+
}));
|
|
2496
|
+
}
|
|
2497
|
+
static isTextPlaceholder(value) {
|
|
2498
|
+
return value === 0x01
|
|
2499
|
+
|| value === 0x02
|
|
2500
|
+
|| value === 0x03
|
|
2501
|
+
|| value === 0x04
|
|
2502
|
+
|| value === 0x0d
|
|
2503
|
+
|| value === 0x0e
|
|
2504
|
+
|| value === 0x0f
|
|
2505
|
+
|| value === 0x10
|
|
2506
|
+
|| value === 0x11
|
|
2507
|
+
|| value === 0x12;
|
|
2508
|
+
}
|
|
2509
|
+
static isVisualPlaceholder(value) {
|
|
2510
|
+
return value === 0x08
|
|
2511
|
+
|| value === 0x0b
|
|
2512
|
+
|| value === 0x0c
|
|
2513
|
+
|| value === 0x0e
|
|
2514
|
+
|| value === 0x14
|
|
2515
|
+
|| value === 0x15;
|
|
2516
|
+
}
|
|
2517
|
+
static classifyPlaceholderType(value) {
|
|
2518
|
+
if (value === 0x0f || value === 0x0d || value === 0x11 || value === 0x03 || value === 0x01) {
|
|
2519
|
+
return 'title';
|
|
2520
|
+
}
|
|
2521
|
+
if (value === 0x10 || value === 0x04) {
|
|
2522
|
+
return 'subtitle';
|
|
2523
|
+
}
|
|
2524
|
+
if (value === 0x0e || value === 0x12 || value === 0x02 || value === 0x06 || value === 0x0c) {
|
|
2525
|
+
return 'body';
|
|
2526
|
+
}
|
|
2527
|
+
return 'other';
|
|
2528
|
+
}
|
|
2529
|
+
static extractSlideColorScheme(record) {
|
|
2530
|
+
const colorSchemeAtom = (record.children ?? []).find((child) => child.recType === 2032 && child.payload.length >= 32);
|
|
2531
|
+
if (!colorSchemeAtom) {
|
|
2532
|
+
return null;
|
|
2533
|
+
}
|
|
2534
|
+
return {
|
|
2535
|
+
backgroundColor: this.readColorStruct(colorSchemeAtom.payload, 0),
|
|
2536
|
+
textColor: this.readColorStruct(colorSchemeAtom.payload, 4),
|
|
2537
|
+
titleColor: this.readColorStruct(colorSchemeAtom.payload, 12),
|
|
2538
|
+
fillColor: this.readColorStruct(colorSchemeAtom.payload, 16)
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
static extractDocumentColorScheme(records) {
|
|
2542
|
+
for (const record of records) {
|
|
2543
|
+
if ((record.recType === 1000 || record.recType === 1016) && record.children && record.children.length > 0) {
|
|
2544
|
+
const directScheme = this.extractSlideColorScheme(record);
|
|
2545
|
+
if (directScheme) {
|
|
2546
|
+
return directScheme;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (record.children && record.children.length > 0) {
|
|
2550
|
+
const nestedScheme = this.extractDocumentColorScheme(record.children);
|
|
2551
|
+
if (nestedScheme) {
|
|
2552
|
+
return nestedScheme;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
static readColorStruct(payload, offset) {
|
|
2559
|
+
if (offset < 0 || offset + 4 > payload.length) {
|
|
2560
|
+
return undefined;
|
|
2561
|
+
}
|
|
2562
|
+
const red = payload.readUInt8(offset);
|
|
2563
|
+
const green = payload.readUInt8(offset + 1);
|
|
2564
|
+
const blue = payload.readUInt8(offset + 2);
|
|
2565
|
+
return this.rgbToHex(red, green, blue);
|
|
2566
|
+
}
|
|
2567
|
+
static rgbToHex(red, green, blue) {
|
|
2568
|
+
const toHex = (value) => value.toString(16).padStart(2, '0');
|
|
2569
|
+
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
|
|
2570
|
+
}
|
|
2571
|
+
static extractTextsFromRecord(record) {
|
|
2572
|
+
const texts = [];
|
|
2573
|
+
const visit = (r) => {
|
|
2574
|
+
const isTextType = r.recType === 4000 || r.recType === 3998 || r.recType === 4026 || r.recType === 4086;
|
|
2575
|
+
const isOfficeArtRecord = r.recType >= 0xf000 && r.recType <= 0xf3ff;
|
|
2576
|
+
if (isTextType || (!isOfficeArtRecord && r.recVer !== 0x0f && r.payload.length > 4 && r.payload.length < 4096)) {
|
|
2577
|
+
const candidates = this.decodeTextCandidates(r.payload);
|
|
2578
|
+
candidates.forEach((t) => {
|
|
2579
|
+
if (!t)
|
|
2580
|
+
return;
|
|
2581
|
+
if (this.isNoiseText(t))
|
|
2582
|
+
return;
|
|
2583
|
+
texts.push(t);
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
if (r.children)
|
|
2587
|
+
r.children.forEach(visit);
|
|
2588
|
+
};
|
|
2589
|
+
visit(record);
|
|
2590
|
+
const dedup = new Set();
|
|
2591
|
+
const out = [];
|
|
2592
|
+
texts.forEach((t) => {
|
|
2593
|
+
const normalized = t.replace(/\s+/g, ' ').trim();
|
|
2594
|
+
if (!normalized || dedup.has(normalized))
|
|
2595
|
+
return;
|
|
2596
|
+
dedup.add(normalized);
|
|
2597
|
+
out.push(normalized);
|
|
2598
|
+
});
|
|
2599
|
+
return out;
|
|
2600
|
+
}
|
|
2601
|
+
static extractLooseTexts(stream) {
|
|
2602
|
+
const out = [];
|
|
2603
|
+
out.push(...this.decodeTextCandidates(stream));
|
|
2604
|
+
return out.filter((t) => !this.isNoiseText(t)).slice(0, 80);
|
|
2605
|
+
}
|
|
2606
|
+
static decodeTextCandidates(payload) {
|
|
2607
|
+
const out = [];
|
|
2608
|
+
// UTF-16LE chunks
|
|
2609
|
+
const utf16 = payload.toString('utf16le')
|
|
2610
|
+
.replace(/\u0000/g, '')
|
|
2611
|
+
.split(/[\r\n\t]+/)
|
|
2612
|
+
.map((s) => s.trim())
|
|
2613
|
+
.filter((s) => s.length >= 3 && /[A-Za-z0-9가-힣]/.test(s) && this.isMostlyReadable(s));
|
|
2614
|
+
out.push(...utf16);
|
|
2615
|
+
// Legacy byte chunks (for example CP949/EUC-KR decks)
|
|
2616
|
+
const legacy = this.decodeLegacyByteText(payload)
|
|
2617
|
+
.split(/[\x00-\x1f]+/)
|
|
2618
|
+
.map((s) => s.trim())
|
|
2619
|
+
.filter((s) => s.length >= 2 && /[A-Za-z0-9가-힣]/.test(s) && this.isMostlyReadable(s));
|
|
2620
|
+
out.push(...legacy);
|
|
2621
|
+
// Latin1 chunks
|
|
2622
|
+
const latin = payload.toString('latin1')
|
|
2623
|
+
.split(/[\x00-\x1f]+/)
|
|
2624
|
+
.map((s) => s.trim())
|
|
2625
|
+
.filter((s) => s.length >= 4 && /[A-Za-z0-9]/.test(s) && this.isMostlyReadable(s));
|
|
2626
|
+
out.push(...latin);
|
|
2627
|
+
return out.slice(0, 30);
|
|
2628
|
+
}
|
|
2629
|
+
static isNoiseText(text) {
|
|
2630
|
+
const t = text.toLowerCase();
|
|
2631
|
+
if (t.length < 3)
|
|
2632
|
+
return true;
|
|
2633
|
+
if (!this.isMostlyReadable(text))
|
|
2634
|
+
return true;
|
|
2635
|
+
if (/^[^A-Za-z0-9가-힣]+$/.test(t))
|
|
2636
|
+
return true;
|
|
2637
|
+
if (/^(arial|times new roman|calibri|wingdings)$/i.test(text))
|
|
2638
|
+
return true;
|
|
2639
|
+
if (/click to edit master/i.test(t))
|
|
2640
|
+
return true;
|
|
2641
|
+
if (/^_+ppt\d+/i.test(t))
|
|
2642
|
+
return true;
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
static isMostlyReadable(text) {
|
|
2646
|
+
const normalized = (text || '').trim();
|
|
2647
|
+
if (!normalized)
|
|
2648
|
+
return false;
|
|
2649
|
+
let readable = 0;
|
|
2650
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
2651
|
+
const code = normalized.charCodeAt(i);
|
|
2652
|
+
const isBasicPrintable = code >= 32 && code <= 126;
|
|
2653
|
+
const isKorean = code >= 0xac00 && code <= 0xd7a3;
|
|
2654
|
+
const isCommonPunct = '“”‘’•…–—·©®™°'.includes(normalized[i]);
|
|
2655
|
+
if (isBasicPrintable || isKorean || isCommonPunct)
|
|
2656
|
+
readable += 1;
|
|
2657
|
+
}
|
|
2658
|
+
const ratio = readable / Math.max(1, normalized.length);
|
|
2659
|
+
return ratio >= 0.75;
|
|
2660
|
+
}
|
|
2661
|
+
static extractPictures(picturesStream) {
|
|
2662
|
+
if (!picturesStream || picturesStream.length === 0)
|
|
2663
|
+
return [];
|
|
2664
|
+
const out = [];
|
|
2665
|
+
// PNG scan
|
|
2666
|
+
const pngSig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
2667
|
+
let idx = 0;
|
|
2668
|
+
while ((idx = picturesStream.indexOf(pngSig, idx)) !== -1) {
|
|
2669
|
+
const end = this.findPngEnd(picturesStream, idx);
|
|
2670
|
+
if (end > idx) {
|
|
2671
|
+
out.push({ mime: 'image/png', base64: picturesStream.slice(idx, end).toString('base64'), pictureIndex: out.length });
|
|
2672
|
+
idx = end;
|
|
2673
|
+
}
|
|
2674
|
+
else {
|
|
2675
|
+
idx += pngSig.length;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
// JPEG scan
|
|
2679
|
+
idx = 0;
|
|
2680
|
+
while ((idx = picturesStream.indexOf(Buffer.from([0xff, 0xd8]), idx)) !== -1) {
|
|
2681
|
+
const end = this.findJpegEnd(picturesStream, idx);
|
|
2682
|
+
if (end > idx) {
|
|
2683
|
+
out.push({
|
|
2684
|
+
mime: 'image/jpeg',
|
|
2685
|
+
base64: picturesStream.slice(idx, end).toString('base64'),
|
|
2686
|
+
pictureIndex: out.length
|
|
2687
|
+
});
|
|
2688
|
+
idx = end;
|
|
2689
|
+
}
|
|
2690
|
+
else {
|
|
2691
|
+
idx += 2;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return out;
|
|
2695
|
+
}
|
|
2696
|
+
static extractPicturesByBlipId(records, picturesStream, pictures = []) {
|
|
2697
|
+
const byId = new Map();
|
|
2698
|
+
if (!picturesStream || picturesStream.length === 0) {
|
|
2699
|
+
return byId;
|
|
2700
|
+
}
|
|
2701
|
+
const entries = [];
|
|
2702
|
+
const visit = (list) => {
|
|
2703
|
+
for (const record of list) {
|
|
2704
|
+
if (record.recType === 0xf007 && record.payload.length >= 36) {
|
|
2705
|
+
entries.push(record);
|
|
2706
|
+
}
|
|
2707
|
+
if (record.children && record.children.length > 0) {
|
|
2708
|
+
visit(record.children);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
visit(records);
|
|
2713
|
+
entries.forEach((entry, index) => {
|
|
2714
|
+
const offset = entry.payload.readUInt32LE(28);
|
|
2715
|
+
const size = entry.payload.readUInt32LE(20);
|
|
2716
|
+
const asset = this.extractPictureAtOffset(picturesStream, offset, size, pictures);
|
|
2717
|
+
if (asset) {
|
|
2718
|
+
byId.set(index + 1, asset);
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
return byId;
|
|
2722
|
+
}
|
|
2723
|
+
static extractPictureAtOffset(picturesStream, offset, expectedSize, pictures = []) {
|
|
2724
|
+
if (!Number.isFinite(offset) || offset < 0 || offset >= picturesStream.length) {
|
|
2725
|
+
return null;
|
|
2726
|
+
}
|
|
2727
|
+
const probe = picturesStream.subarray(offset, Math.min(picturesStream.length, offset + 96));
|
|
2728
|
+
const pngOffset = probe.indexOf(Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
2729
|
+
if (pngOffset !== -1) {
|
|
2730
|
+
const start = offset + pngOffset;
|
|
2731
|
+
const end = expectedSize > 0 && start + expectedSize <= picturesStream.length
|
|
2732
|
+
? start + expectedSize
|
|
2733
|
+
: this.findPngEnd(picturesStream, start);
|
|
2734
|
+
if (end > start) {
|
|
2735
|
+
const base64 = picturesStream.slice(start, end).toString('base64');
|
|
2736
|
+
const pictureIndex = pictures.findIndex((picture) => picture.base64 === base64);
|
|
2737
|
+
return {
|
|
2738
|
+
mime: 'image/png',
|
|
2739
|
+
base64,
|
|
2740
|
+
pictureIndex: pictureIndex >= 0 ? pictureIndex : undefined
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const jpegOffset = probe.indexOf(Buffer.from([0xff, 0xd8, 0xff]));
|
|
2745
|
+
if (jpegOffset !== -1) {
|
|
2746
|
+
const start = offset + jpegOffset;
|
|
2747
|
+
let end = -1;
|
|
2748
|
+
// Try to determine the BLIP record boundary from its OfficeArt header.
|
|
2749
|
+
// The BStoreEntry offset points to the start of the BLIP record which
|
|
2750
|
+
// has an 8-byte header (ver/type 4 bytes + length 4 bytes).
|
|
2751
|
+
if (offset + 8 <= picturesStream.length) {
|
|
2752
|
+
const recType = picturesStream.readUInt16LE(offset + 2);
|
|
2753
|
+
if (recType >= 0xf018 && recType <= 0xf117) {
|
|
2754
|
+
const recLen = picturesStream.readUInt32LE(offset + 4);
|
|
2755
|
+
const blipEnd = offset + 8 + recLen;
|
|
2756
|
+
if (blipEnd <= picturesStream.length && blipEnd > start) {
|
|
2757
|
+
// Scan backwards from the BLIP end to find the FFD9 terminator
|
|
2758
|
+
for (let scan = blipEnd - 2; scan >= start + 2; scan--) {
|
|
2759
|
+
if (picturesStream[scan] === 0xff && picturesStream[scan + 1] === 0xd9) {
|
|
2760
|
+
end = scan + 2;
|
|
2761
|
+
break;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (end === -1) {
|
|
2768
|
+
end = this.findJpegEnd(picturesStream, start);
|
|
2769
|
+
}
|
|
2770
|
+
if (end > start) {
|
|
2771
|
+
const base64 = picturesStream.slice(start, end).toString('base64');
|
|
2772
|
+
const pictureIndex = pictures.findIndex((picture) => picture.base64 === base64);
|
|
2773
|
+
return {
|
|
2774
|
+
mime: 'image/jpeg',
|
|
2775
|
+
base64,
|
|
2776
|
+
pictureIndex: pictureIndex >= 0 ? pictureIndex : undefined
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
return null;
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Walk JPEG marker segments to find the real EOI (FFD9), avoiding false
|
|
2784
|
+
* end markers embedded inside metadata segments like Photoshop APP13.
|
|
2785
|
+
*/
|
|
2786
|
+
static findJpegEnd(buf, start) {
|
|
2787
|
+
let pos = start + 2; // skip SOI (FFD8)
|
|
2788
|
+
while (pos < buf.length - 1) {
|
|
2789
|
+
if (buf[pos] !== 0xff) {
|
|
2790
|
+
pos++;
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
const marker = buf[pos + 1];
|
|
2794
|
+
// Byte-stuffed 0xFF00 or padding 0xFFFF — skip
|
|
2795
|
+
if (marker === 0x00 || marker === 0xff) {
|
|
2796
|
+
pos++;
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
// EOI
|
|
2800
|
+
if (marker === 0xd9)
|
|
2801
|
+
return pos + 2;
|
|
2802
|
+
// SOS (FFDA) — entropy-coded data follows; scan for next marker
|
|
2803
|
+
if (marker === 0xda) {
|
|
2804
|
+
if (pos + 3 >= buf.length)
|
|
2805
|
+
return -1;
|
|
2806
|
+
const sosLen = buf.readUInt16BE(pos + 2);
|
|
2807
|
+
let scan = pos + 2 + sosLen;
|
|
2808
|
+
while (scan < buf.length - 1) {
|
|
2809
|
+
if (buf[scan] === 0xff) {
|
|
2810
|
+
const m = buf[scan + 1];
|
|
2811
|
+
if (m === 0xd9)
|
|
2812
|
+
return scan + 2;
|
|
2813
|
+
if (m === 0x00) {
|
|
2814
|
+
scan += 2;
|
|
2815
|
+
continue;
|
|
2816
|
+
} // stuffed byte
|
|
2817
|
+
if (m >= 0xd0 && m <= 0xd7) {
|
|
2818
|
+
scan += 2;
|
|
2819
|
+
continue;
|
|
2820
|
+
} // RST markers
|
|
2821
|
+
// Another marker (e.g. DHT, SOS in multi-scan JPEG) —
|
|
2822
|
+
// resume the main segment-walking loop from here.
|
|
2823
|
+
pos = scan;
|
|
2824
|
+
break;
|
|
2825
|
+
}
|
|
2826
|
+
scan++;
|
|
2827
|
+
}
|
|
2828
|
+
if (scan >= buf.length - 1)
|
|
2829
|
+
return -1;
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
// Variable-length segment: read length and skip
|
|
2833
|
+
if (pos + 3 >= buf.length)
|
|
2834
|
+
return -1;
|
|
2835
|
+
const segLen = buf.readUInt16BE(pos + 2);
|
|
2836
|
+
if (segLen < 2)
|
|
2837
|
+
return -1;
|
|
2838
|
+
pos += 2 + segLen;
|
|
2839
|
+
}
|
|
2840
|
+
return -1;
|
|
2841
|
+
}
|
|
2842
|
+
static findPngEnd(buf, start) {
|
|
2843
|
+
let off = start + 8;
|
|
2844
|
+
while (off + 12 <= buf.length) {
|
|
2845
|
+
const len = buf.readUInt32BE(off);
|
|
2846
|
+
const type = buf.slice(off + 4, off + 8).toString('ascii');
|
|
2847
|
+
off += 12 + len;
|
|
2848
|
+
if (type === 'IEND')
|
|
2849
|
+
return off;
|
|
2850
|
+
}
|
|
2851
|
+
return -1;
|
|
2852
|
+
}
|
|
2853
|
+
}
|