@segnavia/idml 0.0.0-stage → 0.29.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/dist/frame-preferences.js +45 -0
- package/dist/import.d.ts +13 -0
- package/dist/import.js +27 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/limits.d.ts +15 -0
- package/dist/limits.js +15 -0
- package/dist/map-book.js +73 -0
- package/dist/model.d.ts +44 -0
- package/dist/model.js +268 -0
- package/dist/refusal.js +6 -0
- package/dist/report.d.ts +17 -0
- package/dist/report.js +75 -0
- package/dist/stories.js +141 -0
- package/dist/story-model.js +53 -0
- package/dist/style-mappings.js +52 -0
- package/dist/style-property-report.js +236 -0
- package/dist/styles.js +285 -0
- package/dist/types.d.ts +35 -0
- package/dist/unsupported-elements.js +49 -0
- package/dist/xml.js +18 -0
- package/dist/zip.js +142 -0
- package/package.json +33 -4
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 modstudio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { localName, numberAttribute } from "./xml.js";
|
|
2
|
+
import { isTag } from "domhandler";
|
|
3
|
+
//#region src/frame-preferences.ts
|
|
4
|
+
function preferenceElement(frame) {
|
|
5
|
+
const pending = [...frame.children];
|
|
6
|
+
while (pending.length > 0) {
|
|
7
|
+
const node = pending.pop();
|
|
8
|
+
if (!node || !isTag(node)) continue;
|
|
9
|
+
if (localName(node.name) === "TextFramePreference") return node;
|
|
10
|
+
pending.push(...node.children);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function framePreference(frame, location) {
|
|
14
|
+
const element = preferenceElement(frame);
|
|
15
|
+
if (!element) return void 0;
|
|
16
|
+
return {
|
|
17
|
+
columns: numberAttribute(element, "TextColumnCount", 1),
|
|
18
|
+
gutter: numberAttribute(element, "TextColumnGutter", 0),
|
|
19
|
+
location
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
var FramePreferenceIndex = class {
|
|
23
|
+
byStory = /* @__PURE__ */ new Map();
|
|
24
|
+
add(story, preference) {
|
|
25
|
+
if (!preference || preference.columns < 2) return;
|
|
26
|
+
const values = this.byStory.get(story) ?? [];
|
|
27
|
+
values.push(preference);
|
|
28
|
+
this.byStory.set(story, values);
|
|
29
|
+
}
|
|
30
|
+
preferred(story) {
|
|
31
|
+
const values = this.byStory.get(story) ?? [];
|
|
32
|
+
const counts = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const value of values) {
|
|
34
|
+
const key = `${value.columns}\u0000${value.gutter}`;
|
|
35
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
36
|
+
}
|
|
37
|
+
const selected = [...counts].sort((left, right) => right[1] - left[1])[0]?.[0];
|
|
38
|
+
return values.find((value) => `${value.columns}\u0000${value.gutter}` === selected);
|
|
39
|
+
}
|
|
40
|
+
values() {
|
|
41
|
+
return [...this.byStory.values()].flat();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { FramePreferenceIndex, framePreference };
|
package/dist/import.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IdmlReportData } from "./report.js";
|
|
2
|
+
import { Book } from "@segnavia/format";
|
|
3
|
+
//#region src/import.d.ts
|
|
4
|
+
export type IdmlImportOptions = Record<never, never>;
|
|
5
|
+
export interface IdmlImportReport extends IdmlReportData {
|
|
6
|
+
partsRead: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface IdmlImportResult {
|
|
9
|
+
book: Book;
|
|
10
|
+
report: IdmlImportReport;
|
|
11
|
+
}
|
|
12
|
+
export declare function importIdml(bytes: Uint8Array, _options?: IdmlImportOptions): Promise<IdmlImportResult>;
|
|
13
|
+
//#endregion
|
package/dist/import.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { modelToBook } from "./map-book.js";
|
|
4
|
+
import { idmlModel } from "./model.js";
|
|
5
|
+
import { readZip } from "./zip.js";
|
|
6
|
+
//#region src/import.ts
|
|
7
|
+
const IDML_MIMETYPE = "application/vnd.adobe.indesign-idml-package";
|
|
8
|
+
async function importIdml(bytes, _options = {}) {
|
|
9
|
+
const entries = await readZip(bytes);
|
|
10
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry.bytes]));
|
|
11
|
+
const mimetype = byName.get("mimetype");
|
|
12
|
+
if (!mimetype) throw refusal("the IDML mimetype entry is missing", "add the required mimetype entry");
|
|
13
|
+
const value = new TextDecoder().decode(mimetype);
|
|
14
|
+
if (value !== IDML_MIMETYPE) throw refusal(`the IDML mimetype entry is ${JSON.stringify(value)}, not ${IDML_MIMETYPE}`, "use the IDML package mimetype");
|
|
15
|
+
if (!byName.has("designmap.xml")) throw refusal("the IDML designmap.xml entry is missing", "add the required designmap.xml entry");
|
|
16
|
+
const model = idmlModel(entries);
|
|
17
|
+
const collector = new ReportCollector(model.report);
|
|
18
|
+
return {
|
|
19
|
+
book: modelToBook(model, collector),
|
|
20
|
+
report: {
|
|
21
|
+
partsRead: model.partsRead,
|
|
22
|
+
...collector.data()
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { importIdml };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IdmlFontUse, IdmlReportData } from "./report.js";
|
|
2
|
+
import { IdmlImportOptions, IdmlImportReport, IdmlImportResult, importIdml } from "./import.js";
|
|
3
|
+
import { IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH } from "./limits.js";
|
|
4
|
+
import { IdmlStory } from "./types.js";
|
|
5
|
+
import { IdmlDocumentPreferences, IdmlMargin, IdmlMasterSpread, IdmlModel, IdmlPage, IdmlSpread } from "./model.js";
|
|
6
|
+
export { IDML_LIMITS, type IdmlDocumentPreferences, type IdmlFontUse, type IdmlImportOptions, type IdmlImportReport, type IdmlImportResult, type IdmlMargin, type IdmlMasterSpread, type IdmlModel, type IdmlPage, type IdmlReportData, type IdmlSpread, type IdmlStory, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH, importIdml };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH } from "./limits.js";
|
|
2
|
+
import { importIdml } from "./import.js";
|
|
3
|
+
export { IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH, importIdml };
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/limits.d.ts
|
|
2
|
+
export declare const MAX_IDML_PACKAGE_BYTES: number;
|
|
3
|
+
export declare const MAX_IDML_ENTRIES = 4096;
|
|
4
|
+
export declare const MAX_IDML_PART_BYTES: number;
|
|
5
|
+
export declare const MAX_IDML_TOTAL_INFLATED_BYTES: number;
|
|
6
|
+
export declare const MAX_IDML_XML_DEPTH = 256;
|
|
7
|
+
export interface IdmlLimits {
|
|
8
|
+
packageBytes: number;
|
|
9
|
+
entries: number;
|
|
10
|
+
partBytes: number;
|
|
11
|
+
totalInflatedBytes: number;
|
|
12
|
+
xmlDepth: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const IDML_LIMITS: Readonly<IdmlLimits>;
|
|
15
|
+
//#endregion
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/limits.ts
|
|
2
|
+
const MAX_IDML_PACKAGE_BYTES = 67108864;
|
|
3
|
+
const MAX_IDML_ENTRIES = 4096;
|
|
4
|
+
const MAX_IDML_PART_BYTES = 33554432;
|
|
5
|
+
const MAX_IDML_TOTAL_INFLATED_BYTES = 268435456;
|
|
6
|
+
const MAX_IDML_XML_DEPTH = 256;
|
|
7
|
+
const IDML_LIMITS = {
|
|
8
|
+
packageBytes: MAX_IDML_PACKAGE_BYTES,
|
|
9
|
+
entries: MAX_IDML_ENTRIES,
|
|
10
|
+
partBytes: MAX_IDML_PART_BYTES,
|
|
11
|
+
totalInflatedBytes: MAX_IDML_TOTAL_INFLATED_BYTES,
|
|
12
|
+
xmlDepth: 256
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH };
|
package/dist/map-book.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { storyHtml } from "./stories.js";
|
|
4
|
+
import { StyleMapper } from "./styles.js";
|
|
5
|
+
import { Book, MM_PER_PT } from "@segnavia/format";
|
|
6
|
+
import { findActiveContent } from "@segnavia/html";
|
|
7
|
+
//#region src/map-book.ts
|
|
8
|
+
function orderedStories(model) {
|
|
9
|
+
const order = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const spread of model.spreads) for (const page of spread.pages) for (const story of page.stories) if (!order.has(story)) order.set(story, order.size);
|
|
11
|
+
const masterStories = new Set(model.masterStoryIds);
|
|
12
|
+
return model.stories.filter((story) => !masterStories.has(story.id)).sort((left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER));
|
|
13
|
+
}
|
|
14
|
+
function bodyColumnGap(model, stories, report) {
|
|
15
|
+
const storyFrames = stories.flatMap((story) => story.frame?.columns && story.frame.columns >= 2 ? [story.frame] : []);
|
|
16
|
+
const frames = (model.bodyFramePreferences ?? storyFrames).filter((frame) => frame.columns >= 2);
|
|
17
|
+
const representativeFrames = storyFrames.length > 0 ? storyFrames : frames;
|
|
18
|
+
const counts = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const frame of representativeFrames) counts.set(frame.gutter, (counts.get(frame.gutter) ?? 0) + 1);
|
|
20
|
+
const selected = [...counts].sort((left, right) => right[1] - left[1])[0]?.[0];
|
|
21
|
+
if (selected === void 0) return void 0;
|
|
22
|
+
for (const gutter of new Set(frames.map((frame) => frame.gutter).filter((gutter) => gutter !== selected))) {
|
|
23
|
+
const frame = frames.find((candidate) => candidate.gutter === gutter);
|
|
24
|
+
report.add(report.unsupportedElements, "differing body frame gutters", frame?.location ?? model.id);
|
|
25
|
+
}
|
|
26
|
+
return selected;
|
|
27
|
+
}
|
|
28
|
+
function modelToBook(model, report = new ReportCollector(model.report)) {
|
|
29
|
+
const preferences = model.preferences;
|
|
30
|
+
const rightBinding = preferences.binding === "right-to-left";
|
|
31
|
+
const bookDirection = rightBinding ? "rtl" : "ltr";
|
|
32
|
+
const styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection);
|
|
33
|
+
const bookLanguage = styles.rootLanguage();
|
|
34
|
+
const stories = orderedStories(model);
|
|
35
|
+
const chapters = stories.map((story) => ({
|
|
36
|
+
story,
|
|
37
|
+
mapped: storyHtml(story, styles, report, bookDirection, bookLanguage)
|
|
38
|
+
})).map(({ story, mapped: value }) => ({
|
|
39
|
+
title: story.id,
|
|
40
|
+
html: value.html
|
|
41
|
+
}));
|
|
42
|
+
if (chapters.length === 0) throw refusal("the IDML has no stories", "add at least one Story");
|
|
43
|
+
const html = chapters.map((chapter) => chapter.html).join("");
|
|
44
|
+
const active = findActiveContent(html);
|
|
45
|
+
if (active.length > 0) throw refusal(`the imported Book contains active content (${active.join(", ")})`, "remove that active content");
|
|
46
|
+
const columnGap = bodyColumnGap(model, stories, report);
|
|
47
|
+
const css = styles.css();
|
|
48
|
+
return Book.parse({
|
|
49
|
+
title: model.id,
|
|
50
|
+
mode: "paginated",
|
|
51
|
+
page: {
|
|
52
|
+
paper: {
|
|
53
|
+
width: preferences.pageWidth,
|
|
54
|
+
height: preferences.pageHeight,
|
|
55
|
+
unit: "pt"
|
|
56
|
+
},
|
|
57
|
+
margin: {
|
|
58
|
+
top: preferences.margin.top * MM_PER_PT,
|
|
59
|
+
bottom: preferences.margin.bottom * MM_PER_PT,
|
|
60
|
+
inner: (rightBinding ? preferences.margin.right : preferences.margin.left) * MM_PER_PT,
|
|
61
|
+
outer: (rightBinding ? preferences.margin.left : preferences.margin.right) * MM_PER_PT
|
|
62
|
+
},
|
|
63
|
+
...columnGap === void 0 ? {} : { columnGap: columnGap * MM_PER_PT }
|
|
64
|
+
},
|
|
65
|
+
dir: bookDirection,
|
|
66
|
+
...bookLanguage ? { lang: bookLanguage } : {},
|
|
67
|
+
...css ? { css } : {},
|
|
68
|
+
rectoOpenings: preferences.facingPages,
|
|
69
|
+
chapters
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { modelToBook };
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { IdmlReportData } from "./report.js";
|
|
2
|
+
import { IdmlFramePreference, IdmlStory, IdmlStyle, IdmlSwatch } from "./types.js";
|
|
3
|
+
//#region src/model.d.ts
|
|
4
|
+
export interface IdmlMargin {
|
|
5
|
+
top: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
left: number;
|
|
8
|
+
right: number;
|
|
9
|
+
}
|
|
10
|
+
export interface IdmlDocumentPreferences {
|
|
11
|
+
pageWidth: number;
|
|
12
|
+
pageHeight: number;
|
|
13
|
+
facingPages: boolean;
|
|
14
|
+
binding: 'left-to-right' | 'right-to-left';
|
|
15
|
+
margin: IdmlMargin;
|
|
16
|
+
}
|
|
17
|
+
export interface IdmlPage {
|
|
18
|
+
id: string;
|
|
19
|
+
appliedMaster?: string;
|
|
20
|
+
stories: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface IdmlSpread {
|
|
23
|
+
id: string;
|
|
24
|
+
pages: IdmlPage[];
|
|
25
|
+
}
|
|
26
|
+
export interface IdmlMasterSpread {
|
|
27
|
+
id: string;
|
|
28
|
+
pages: IdmlPage[];
|
|
29
|
+
}
|
|
30
|
+
export interface IdmlModel {
|
|
31
|
+
id: string;
|
|
32
|
+
preferences: IdmlDocumentPreferences;
|
|
33
|
+
spreads: IdmlSpread[];
|
|
34
|
+
masterSpreads: IdmlMasterSpread[];
|
|
35
|
+
stories: IdmlStory[];
|
|
36
|
+
masterStoryIds: string[];
|
|
37
|
+
partsRead: string[];
|
|
38
|
+
styles?: IdmlStyle[];
|
|
39
|
+
swatches?: IdmlSwatch[];
|
|
40
|
+
bodyFramePreferences?: IdmlFramePreference[];
|
|
41
|
+
report?: IdmlReportData;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export type { IdmlStory };
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { localName, numberAttribute } from "./xml.js";
|
|
4
|
+
import { parseStyles, parseSwatches } from "./styles.js";
|
|
5
|
+
import { FramePreferenceIndex, framePreference } from "./frame-preferences.js";
|
|
6
|
+
import { IDML_LIMITS } from "./limits.js";
|
|
7
|
+
import { parseStories } from "./story-model.js";
|
|
8
|
+
import { collectUnsupportedElements } from "./unsupported-elements.js";
|
|
9
|
+
import { isTag } from "domhandler";
|
|
10
|
+
import { Parser, parseDocument } from "htmlparser2";
|
|
11
|
+
//#region src/model.ts
|
|
12
|
+
function elements(root, name) {
|
|
13
|
+
const found = [];
|
|
14
|
+
const pending = [root];
|
|
15
|
+
while (pending.length > 0) {
|
|
16
|
+
const node = pending.pop();
|
|
17
|
+
if (!node) continue;
|
|
18
|
+
if (isTag(node)) {
|
|
19
|
+
if (!name || localName(node.name) === name) found.push(node);
|
|
20
|
+
}
|
|
21
|
+
if ("children" in node) for (let index = node.children.length - 1; index >= 0; index -= 1) pending.push(node.children[index]);
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
function attribute(element, ...names) {
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
const value = element.attribs[name];
|
|
28
|
+
if (value !== void 0) return value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function boolAttribute(element, name, fallback) {
|
|
32
|
+
const raw = attribute(element, name);
|
|
33
|
+
if (raw === void 0) return fallback;
|
|
34
|
+
if (raw === "true") return true;
|
|
35
|
+
if (raw === "false") return false;
|
|
36
|
+
throw refusal(`IDML ${name} must be true or false`, `set ${name} to true or false`);
|
|
37
|
+
}
|
|
38
|
+
function checkXmlDepth(source, part, limit) {
|
|
39
|
+
let depth = 0;
|
|
40
|
+
new Parser({
|
|
41
|
+
onopentag() {
|
|
42
|
+
depth += 1;
|
|
43
|
+
if (depth > limit) throw refusal(`IDML XML part ${part} exceeds the ${limit} level XML depth limit`, `reduce its nesting to ${limit} levels or fewer`);
|
|
44
|
+
},
|
|
45
|
+
onclosetag() {
|
|
46
|
+
depth -= 1;
|
|
47
|
+
}
|
|
48
|
+
}, {
|
|
49
|
+
xmlMode: true,
|
|
50
|
+
recognizeSelfClosing: true
|
|
51
|
+
}).end(source);
|
|
52
|
+
}
|
|
53
|
+
function parseParts(entries, limits) {
|
|
54
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
55
|
+
return entries.filter((entry) => entry.name.toLowerCase().endsWith(".xml")).map((entry) => {
|
|
56
|
+
let source;
|
|
57
|
+
try {
|
|
58
|
+
source = decoder.decode(entry.bytes);
|
|
59
|
+
} catch {
|
|
60
|
+
throw refusal(`IDML XML part ${entry.name} is not valid UTF-8`, "export that XML part as UTF-8");
|
|
61
|
+
}
|
|
62
|
+
let document;
|
|
63
|
+
try {
|
|
64
|
+
checkXmlDepth(source, entry.name, limits.xmlDepth);
|
|
65
|
+
document = parseDocument(source, {
|
|
66
|
+
xmlMode: true,
|
|
67
|
+
recognizeSelfClosing: true
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof Error && error.message.includes("XML depth limit")) throw error;
|
|
71
|
+
throw refusal(`IDML XML part ${entry.name} could not be parsed: ${String(error)}`, "export well-formed XML");
|
|
72
|
+
}
|
|
73
|
+
const all = elements(document);
|
|
74
|
+
if (all.length === 0) throw refusal(`IDML XML part ${entry.name} has no root element`, "export a non-empty well-formed XML part");
|
|
75
|
+
return {
|
|
76
|
+
name: entry.name,
|
|
77
|
+
document,
|
|
78
|
+
all
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function page(element) {
|
|
83
|
+
const id = attribute(element, "Self");
|
|
84
|
+
if (!id) throw refusal("an IDML Page has no Self identifier", "give every Page a Self identifier");
|
|
85
|
+
return {
|
|
86
|
+
id,
|
|
87
|
+
appliedMaster: attribute(element, "AppliedMaster"),
|
|
88
|
+
stories: []
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const IDENTITY = [
|
|
92
|
+
1,
|
|
93
|
+
0,
|
|
94
|
+
0,
|
|
95
|
+
1,
|
|
96
|
+
0,
|
|
97
|
+
0
|
|
98
|
+
];
|
|
99
|
+
function transform(element) {
|
|
100
|
+
const values = (attribute(element, "ItemTransform") ?? "").trim().split(/\s+/).map(Number);
|
|
101
|
+
return values.length === 6 && values.every(Number.isFinite) ? values : IDENTITY;
|
|
102
|
+
}
|
|
103
|
+
function compose(parent, child) {
|
|
104
|
+
const [a, b, c, d, x, y] = parent;
|
|
105
|
+
const [e, f, g, h, u, v] = child;
|
|
106
|
+
return [
|
|
107
|
+
a * e + c * f,
|
|
108
|
+
b * e + d * f,
|
|
109
|
+
a * g + c * h,
|
|
110
|
+
b * g + d * h,
|
|
111
|
+
a * u + c * v + x,
|
|
112
|
+
b * u + d * v + y
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function apply(value, point) {
|
|
116
|
+
return [value[0] * point[0] + value[2] * point[1] + value[4], value[1] * point[0] + value[3] * point[1] + value[5]];
|
|
117
|
+
}
|
|
118
|
+
function pageArea(element) {
|
|
119
|
+
const bounds = (attribute(element, "GeometricBounds") ?? "").trim().split(/\s+/).map(Number);
|
|
120
|
+
if (bounds.length !== 4 || !bounds.every(Number.isFinite)) return void 0;
|
|
121
|
+
const [top, left, bottom, right] = bounds;
|
|
122
|
+
const matrix = transform(element);
|
|
123
|
+
return [
|
|
124
|
+
apply(matrix, [left, top]),
|
|
125
|
+
apply(matrix, [right, top]),
|
|
126
|
+
apply(matrix, [right, bottom]),
|
|
127
|
+
apply(matrix, [left, bottom])
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
function frameCenter(element, matrix) {
|
|
131
|
+
const anchors = elements(element).map((candidate) => attribute(candidate, "Anchor")).filter((value) => value !== void 0).map((value) => value.trim().split(/\s+/).map(Number)).filter((value) => value.length === 2 && value.every(Number.isFinite)).map((point) => apply(matrix, point));
|
|
132
|
+
if (anchors.length === 0) return void 0;
|
|
133
|
+
const xs = anchors.map(([x]) => x);
|
|
134
|
+
const ys = anchors.map(([, y]) => y);
|
|
135
|
+
return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
|
|
136
|
+
}
|
|
137
|
+
function contains(area, [x, y]) {
|
|
138
|
+
let inside = false;
|
|
139
|
+
for (let index = 0, previous = area.length - 1; index < area.length; previous = index, index += 1) {
|
|
140
|
+
const [xi, yi] = area[index];
|
|
141
|
+
const [xj, yj] = area[previous];
|
|
142
|
+
if (yi === yj && y === yi && x >= Math.min(xi, xj) && x <= Math.max(xi, xj)) return true;
|
|
143
|
+
if (yi > y !== yj > y && x <= (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
144
|
+
}
|
|
145
|
+
return inside;
|
|
146
|
+
}
|
|
147
|
+
function framePlacement(element, matrix, location) {
|
|
148
|
+
const story = attribute(element, "ParentStory");
|
|
149
|
+
if (!story) return void 0;
|
|
150
|
+
const preference = framePreference(element, location);
|
|
151
|
+
return {
|
|
152
|
+
story,
|
|
153
|
+
center: frameCenter(element, matrix),
|
|
154
|
+
...preference ? { preference } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function spreadFrames(spread, location) {
|
|
158
|
+
const result = [];
|
|
159
|
+
const pending = [...spread.children].reverse().map((node) => ({
|
|
160
|
+
node,
|
|
161
|
+
matrix: IDENTITY
|
|
162
|
+
}));
|
|
163
|
+
while (pending.length > 0) {
|
|
164
|
+
const current = pending.pop();
|
|
165
|
+
if (!current || !isTag(current.node)) continue;
|
|
166
|
+
const name = localName(current.node.name);
|
|
167
|
+
if (name === "Page" || name === "Spread" || name === "MasterSpread") continue;
|
|
168
|
+
const matrix = name === "Group" || name === "TextFrame" ? compose(current.matrix, transform(current.node)) : current.matrix;
|
|
169
|
+
if (name === "TextFrame") {
|
|
170
|
+
const placement = framePlacement(current.node, matrix, `${location}#TextFrame[${result.length + 1}]`);
|
|
171
|
+
if (placement) result.push(placement);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (let index = current.node.children.length - 1; index >= 0; index -= 1) pending.push({
|
|
175
|
+
node: current.node.children[index],
|
|
176
|
+
matrix
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
function placeStories(element, pageElements, pages, report, framePreferences, location) {
|
|
182
|
+
const areas = pageElements.map(pageArea);
|
|
183
|
+
const frameStories = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const frame of spreadFrames(element, location)) {
|
|
185
|
+
frameStories.add(frame.story);
|
|
186
|
+
framePreferences.add(frame.story, frame.preference);
|
|
187
|
+
const pageIndex = frame.center ? areas.findIndex((area) => area !== void 0 && contains(area, frame.center)) : -1;
|
|
188
|
+
const target = pages[pageIndex];
|
|
189
|
+
if (target && !target.stories.includes(frame.story)) target.stories.push(frame.story);
|
|
190
|
+
else if (pageIndex < 0) report.add(report.unsupportedElements, "TextFrame off page", frame.story);
|
|
191
|
+
}
|
|
192
|
+
return frameStories;
|
|
193
|
+
}
|
|
194
|
+
function spreads(parts, kind, orderedNames, report, framePreferences) {
|
|
195
|
+
const result = [];
|
|
196
|
+
const frameStories = /* @__PURE__ */ new Set();
|
|
197
|
+
const byName = new Map(parts.map((part) => [part.name, part]));
|
|
198
|
+
const named = new Set(orderedNames);
|
|
199
|
+
const unlisted = parts.filter((part) => !named.has(part.name) && part.all.some((element) => localName(element.name) === kind && attribute(element, "Self"))).length;
|
|
200
|
+
if (unlisted > 0) report.add(report.unsupportedElements, "unlisted spread part", kind, unlisted);
|
|
201
|
+
for (const name of orderedNames) {
|
|
202
|
+
const part = byName.get(name);
|
|
203
|
+
if (!part) throw refusal(`designmap.xml ${kind} src ${name} has no matching ZIP entry`, `add the named ${kind} part to the package`);
|
|
204
|
+
for (const element of part.all.filter((candidate) => localName(candidate.name) === kind)) {
|
|
205
|
+
const id = attribute(element, "Self");
|
|
206
|
+
if (!id) continue;
|
|
207
|
+
const pageElements = elements(element, "Page");
|
|
208
|
+
const pages = pageElements.map(page);
|
|
209
|
+
for (const story of placeStories(element, pageElements, pages, report, framePreferences, name)) frameStories.add(story);
|
|
210
|
+
result.push({
|
|
211
|
+
id,
|
|
212
|
+
pages
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
values: result,
|
|
218
|
+
frameStories
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function spreadSources(document, kind) {
|
|
222
|
+
return document.all.filter((element) => localName(element.name) === kind).map((element) => attribute(element, "src")).filter((source) => source !== void 0);
|
|
223
|
+
}
|
|
224
|
+
function idmlModel(entries, limits = IDML_LIMITS) {
|
|
225
|
+
const parts = parseParts(entries, limits);
|
|
226
|
+
const designmap = parts.find((part) => part.name === "designmap.xml");
|
|
227
|
+
if (!designmap) throw refusal("the IDML designmap.xml entry is missing", "add the required designmap.xml entry");
|
|
228
|
+
const document = parts.flatMap((part) => part.all).find((element) => localName(element.name) === "Document");
|
|
229
|
+
const preference = parts.flatMap((part) => part.all).find((element) => localName(element.name) === "DocumentPreference");
|
|
230
|
+
if (!document) throw refusal("designmap.xml has no IDML Document element", "export a valid IDML design map");
|
|
231
|
+
if (!preference) throw refusal("IDML DocumentPreference is missing", "export document preferences with the package");
|
|
232
|
+
const margin = parts.flatMap((part) => part.all).find((element) => localName(element.name) === "MarginPreference");
|
|
233
|
+
const binding = attribute(preference, "PageBinding") ?? attribute(document, "PageBinding") ?? "LeftToRight";
|
|
234
|
+
if (binding !== "LeftToRight" && binding !== "RightToLeft") throw refusal(`IDML PageBinding ${binding} is not supported`, "use LeftToRight or RightToLeft page binding");
|
|
235
|
+
const report = new ReportCollector();
|
|
236
|
+
collectUnsupportedElements(parts, report);
|
|
237
|
+
const framePreferences = new FramePreferenceIndex();
|
|
238
|
+
const spreadResult = spreads(parts, "Spread", spreadSources(designmap, "Spread"), report, framePreferences);
|
|
239
|
+
const masterResult = spreads(parts, "MasterSpread", spreadSources(designmap, "MasterSpread"), report, new FramePreferenceIndex());
|
|
240
|
+
const masterStoryIds = [...masterResult.frameStories].filter((story) => !spreadResult.frameStories.has(story));
|
|
241
|
+
for (const story of masterStoryIds) report.add(report.unsupportedElements, "master story", story);
|
|
242
|
+
return {
|
|
243
|
+
id: attribute(document, "Self") ?? "IDML document",
|
|
244
|
+
preferences: {
|
|
245
|
+
pageWidth: numberAttribute(preference, "PageWidth"),
|
|
246
|
+
pageHeight: numberAttribute(preference, "PageHeight"),
|
|
247
|
+
facingPages: boolAttribute(preference, "FacingPages", true),
|
|
248
|
+
binding: binding === "RightToLeft" ? "right-to-left" : "left-to-right",
|
|
249
|
+
margin: {
|
|
250
|
+
top: numberAttribute(margin, "Top", 0),
|
|
251
|
+
bottom: numberAttribute(margin, "Bottom", 0),
|
|
252
|
+
left: numberAttribute(margin, "Left", 0),
|
|
253
|
+
right: numberAttribute(margin, "Right", 0)
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
spreads: spreadResult.values,
|
|
257
|
+
masterSpreads: masterResult.values,
|
|
258
|
+
stories: parseStories(parts, framePreferences, report),
|
|
259
|
+
bodyFramePreferences: framePreferences.values(),
|
|
260
|
+
masterStoryIds,
|
|
261
|
+
partsRead: parts.map((part) => part.name),
|
|
262
|
+
styles: parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []),
|
|
263
|
+
swatches: parts.flatMap((part) => part.name.endsWith("/Graphic.xml") || part.name === "Resources/Graphic.xml" ? parseSwatches(part.all) : []),
|
|
264
|
+
report: report.data()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
export { idmlModel };
|
package/dist/refusal.js
ADDED
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/report.d.ts
|
|
2
|
+
export interface IdmlFontUse {
|
|
3
|
+
appliedFont: string;
|
|
4
|
+
fontStyle?: string;
|
|
5
|
+
locations: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface IdmlReportData {
|
|
8
|
+
unsupportedElements: Record<string, number>;
|
|
9
|
+
unsupportedStyleProperties: Record<string, number>;
|
|
10
|
+
unsupportedOverrideProperties: Record<string, number>;
|
|
11
|
+
unsupportedSpecialCharacters: Record<string, number>;
|
|
12
|
+
unsupportedLanguages: Record<string, number>;
|
|
13
|
+
spotColors: Record<string, number>;
|
|
14
|
+
locations: Record<string, string[]>;
|
|
15
|
+
fonts: IdmlFontUse[];
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/report.ts
|
|
2
|
+
const BUCKET_NAMES = {
|
|
3
|
+
unsupportedElements: "element",
|
|
4
|
+
unsupportedStyleProperties: "style",
|
|
5
|
+
unsupportedOverrideProperties: "override",
|
|
6
|
+
unsupportedSpecialCharacters: "special",
|
|
7
|
+
unsupportedLanguages: "language",
|
|
8
|
+
spotColors: "spot"
|
|
9
|
+
};
|
|
10
|
+
var ReportCollector = class {
|
|
11
|
+
locations = /* @__PURE__ */ new Map();
|
|
12
|
+
fonts = /* @__PURE__ */ new Map();
|
|
13
|
+
buckets = Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, /* @__PURE__ */ new Map()]));
|
|
14
|
+
get unsupportedElements() {
|
|
15
|
+
return this.buckets.unsupportedElements;
|
|
16
|
+
}
|
|
17
|
+
get unsupportedStyleProperties() {
|
|
18
|
+
return this.buckets.unsupportedStyleProperties;
|
|
19
|
+
}
|
|
20
|
+
get unsupportedOverrideProperties() {
|
|
21
|
+
return this.buckets.unsupportedOverrideProperties;
|
|
22
|
+
}
|
|
23
|
+
get unsupportedSpecialCharacters() {
|
|
24
|
+
return this.buckets.unsupportedSpecialCharacters;
|
|
25
|
+
}
|
|
26
|
+
get unsupportedLanguages() {
|
|
27
|
+
return this.buckets.unsupportedLanguages;
|
|
28
|
+
}
|
|
29
|
+
get spotColors() {
|
|
30
|
+
return this.buckets.spotColors;
|
|
31
|
+
}
|
|
32
|
+
constructor(seed) {
|
|
33
|
+
for (const name of Object.keys(BUCKET_NAMES)) for (const [key, count] of Object.entries(seed?.[name] ?? {})) this.buckets[name].set(key, count);
|
|
34
|
+
for (const [key, locations] of Object.entries(seed?.locations ?? {})) this.locations.set(key, [...locations]);
|
|
35
|
+
for (const font of seed?.fonts ?? []) this.font(font.appliedFont, font.fontStyle, font.locations[0] ?? "Resources/Styles.xml");
|
|
36
|
+
}
|
|
37
|
+
add(bucket, name, location, count = 1) {
|
|
38
|
+
bucket.set(name, (bucket.get(name) ?? 0) + count);
|
|
39
|
+
const key = `${this.bucketName(bucket)}:${name}`;
|
|
40
|
+
const values = this.locations.get(key) ?? [];
|
|
41
|
+
if (values.length < 5 && !values.includes(location)) values.push(location);
|
|
42
|
+
this.locations.set(key, values);
|
|
43
|
+
}
|
|
44
|
+
addOnce(bucket, name, location) {
|
|
45
|
+
const key = `${this.bucketName(bucket)}:${name}`;
|
|
46
|
+
if (this.locations.get(key)?.includes(location)) return;
|
|
47
|
+
this.add(bucket, name, location);
|
|
48
|
+
}
|
|
49
|
+
font(appliedFont, fontStyle, location) {
|
|
50
|
+
if (!appliedFont) return;
|
|
51
|
+
const key = `${appliedFont}\u0000${fontStyle ?? ""}`;
|
|
52
|
+
const found = this.fonts.get(key) ?? {
|
|
53
|
+
appliedFont,
|
|
54
|
+
...fontStyle ? { fontStyle } : {},
|
|
55
|
+
locations: []
|
|
56
|
+
};
|
|
57
|
+
if (found.locations.length < 5 && !found.locations.includes(location)) found.locations.push(location);
|
|
58
|
+
this.fonts.set(key, found);
|
|
59
|
+
}
|
|
60
|
+
data() {
|
|
61
|
+
const record = (values) => Object.fromEntries([...values].sort(([a], [b]) => a.localeCompare(b)));
|
|
62
|
+
return {
|
|
63
|
+
...Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, record(this.buckets[name])])),
|
|
64
|
+
locations: Object.fromEntries([...this.locations].sort(([a], [b]) => a.localeCompare(b))),
|
|
65
|
+
fonts: [...this.fonts.values()]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
bucketName(bucket) {
|
|
69
|
+
const found = Object.keys(BUCKET_NAMES).find((name) => this.buckets[name] === bucket);
|
|
70
|
+
if (!found) throw new Error("Report bucket is not registered.");
|
|
71
|
+
return BUCKET_NAMES[found];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ReportCollector };
|