layoutbridge-light 0.0.0-stage → 1.0.1

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +26 -0
  3. package/README.de.md +367 -0
  4. package/README.md +353 -2
  5. package/THIRD-PARTY-NOTICES.md +2878 -0
  6. package/dist/src/mcp/catalog.js +131 -0
  7. package/dist/src/mcp/config.js +211 -0
  8. package/dist/src/mcp/content-credentials.js +65 -0
  9. package/dist/src/mcp/dispatch.js +171 -0
  10. package/dist/src/mcp/epub-check.js +331 -0
  11. package/dist/src/mcp/epub-orientation.js +132 -0
  12. package/dist/src/mcp/license/free-tools.js +16 -0
  13. package/dist/src/mcp/license/gate.js +210 -0
  14. package/dist/src/mcp/license/index.js +107 -0
  15. package/dist/src/mcp/license/instance-name.js +57 -0
  16. package/dist/src/mcp/license/key-source.js +59 -0
  17. package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
  18. package/dist/src/mcp/license/placeholder-verifier.js +10 -0
  19. package/dist/src/mcp/license/polar-ids.js +100 -0
  20. package/dist/src/mcp/license/polar-verifier.js +285 -0
  21. package/dist/src/mcp/license/product-ids.js +73 -0
  22. package/dist/src/mcp/license/state-store.js +91 -0
  23. package/dist/src/mcp/license/types.js +11 -0
  24. package/dist/src/mcp/logging.js +271 -0
  25. package/dist/src/mcp/pdf-postprocess.js +798 -0
  26. package/dist/src/mcp/product-info.js +54 -0
  27. package/dist/src/mcp/product.js +56 -0
  28. package/dist/src/mcp/server.js +253 -0
  29. package/dist/src/mcp/setup.js +680 -0
  30. package/dist/src/mcp/start.cjs +117 -0
  31. package/dist/src/mcp/target-folder.js +25 -0
  32. package/dist/src/mcp/tool-definition.js +2 -0
  33. package/dist/src/mcp/tools.js +2680 -0
  34. package/dist/src/mcp/typography-rules.js +70 -0
  35. package/dist/src/transport/com.js +436 -0
  36. package/dist/src/transport/osascript.js +171 -0
  37. package/dist/src/transport/queue.js +19 -0
  38. package/dist/src/transport/script-bundle.js +92 -0
  39. package/dist/src/transport/transport.js +52 -0
  40. package/indesign/commands/10-app.js +6 -0
  41. package/indesign/commands/20-documents.js +517 -0
  42. package/indesign/commands/22-document-setup.js +204 -0
  43. package/indesign/commands/23-save-package.js +258 -0
  44. package/indesign/commands/25-export.js +695 -0
  45. package/indesign/commands/26-export-image.js +202 -0
  46. package/indesign/commands/27-textdefaults.js +151 -0
  47. package/indesign/commands/28-selection.js +174 -0
  48. package/indesign/commands/30-pages.js +1032 -0
  49. package/indesign/commands/31-view.js +310 -0
  50. package/indesign/commands/32-sections.js +203 -0
  51. package/indesign/commands/35-parents.js +448 -0
  52. package/indesign/commands/37-layout.js +837 -0
  53. package/indesign/commands/40-frames.js +886 -0
  54. package/indesign/commands/41-shapes.js +505 -0
  55. package/indesign/commands/42-groups.js +117 -0
  56. package/indesign/commands/43-layers.js +310 -0
  57. package/indesign/commands/44-effects.js +274 -0
  58. package/indesign/commands/45-frame-options.js +247 -0
  59. package/indesign/commands/47-anchored.js +473 -0
  60. package/indesign/commands/50-modify.js +803 -0
  61. package/indesign/commands/52-align.js +139 -0
  62. package/indesign/commands/55-fit.js +106 -0
  63. package/indesign/commands/60-images.js +1154 -0
  64. package/indesign/commands/62-embed.js +120 -0
  65. package/indesign/commands/69-placeholders.js +132 -0
  66. package/indesign/commands/70-text.js +710 -0
  67. package/indesign/commands/71-outlines.js +92 -0
  68. package/indesign/commands/72-place-text.js +476 -0
  69. package/indesign/commands/73-story-lock.js +157 -0
  70. package/indesign/commands/74-stories.js +121 -0
  71. package/indesign/commands/75-tables.js +702 -0
  72. package/indesign/commands/76-text-variables.js +260 -0
  73. package/indesign/commands/77-story-options.js +86 -0
  74. package/indesign/commands/78-frame-lines.js +511 -0
  75. package/indesign/commands/79-check-typography.js +480 -0
  76. package/indesign/commands/80-styles.js +1960 -0
  77. package/indesign/commands/81-composition.js +355 -0
  78. package/indesign/commands/82-object-styles.js +263 -0
  79. package/indesign/commands/83-drop-caps.js +87 -0
  80. package/indesign/commands/83-indents-tabs.js +264 -0
  81. package/indesign/commands/84-import-styles.js +142 -0
  82. package/indesign/commands/85-swatches.js +112 -0
  83. package/indesign/commands/86-gradients.js +185 -0
  84. package/indesign/commands/87-replace-swatch.js +180 -0
  85. package/indesign/commands/88-replace-font.js +212 -0
  86. package/indesign/commands/89-toc-index.js +1002 -0
  87. package/indesign/commands/90-print-on-demand.js +495 -0
  88. package/indesign/commands/91-epub.js +629 -0
  89. package/indesign/commands/92-accessibility.js +1131 -0
  90. package/indesign/interpreter.js +829 -0
  91. package/package.json +41 -4
  92. package/rules/typography-rules.json +32 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Sitzung 60 (A-45 Punkt 1, E-102): Pruefung einer fertigen EPUB-Datei ohne
3
+ * InDesign – fuer check_epub mit path. Liest das ZIP selbst (zentrales
4
+ * Verzeichnis, Methoden stored/deflate ueber node:zlib), dann
5
+ * META-INF/container.xml, das OPF-Paket und die Verweise der
6
+ * XHTML-Dateien. Grundlage: EPUB-OCF (mimetype als erster Eintrag,
7
+ * unkomprimiert), EPUB-Paketdokument (dc:title, dc:language, dc:identifier,
8
+ * nav bzw. NCX) und die Funde an InDesign-21.6-Exporten vom 23.10.2026
9
+ * (docs/EPUB-ANFORDERUNGEN.md). Ersetzt EPUBCheck nicht.
10
+ */
11
+ import { existsSync, readFileSync, statSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { posix } from 'node:path';
14
+ import { inflateRawSync } from 'node:zlib';
15
+ import { transportError } from '../transport/transport.js';
16
+ const MIMETYPE = 'application/epub+zip';
17
+ /** Liest das zentrale Verzeichnis eines ZIP-Archivs. Wirft bei kaputtem Archiv. */
18
+ export function readZipEntries(buffer) {
19
+ const minEnd = Math.max(0, buffer.length - 65_557);
20
+ let end = -1;
21
+ for (let i = buffer.length - 22; i >= minEnd; i--) {
22
+ if (buffer.readUInt32LE(i) === 0x06054b50) {
23
+ end = i;
24
+ break;
25
+ }
26
+ }
27
+ if (end < 0)
28
+ throw new Error('no ZIP end-of-central-directory record');
29
+ const count = buffer.readUInt16LE(end + 10);
30
+ let offset = buffer.readUInt32LE(end + 16);
31
+ if (count === 0xffff || offset === 0xffffffff)
32
+ throw new Error('ZIP64 archives are not supported');
33
+ const entries = [];
34
+ for (let n = 0; n < count; n++) {
35
+ if (offset + 46 > buffer.length || buffer.readUInt32LE(offset) !== 0x02014b50)
36
+ throw new Error('broken central directory');
37
+ const method = buffer.readUInt16LE(offset + 10);
38
+ const compressedSize = buffer.readUInt32LE(offset + 20);
39
+ const size = buffer.readUInt32LE(offset + 24);
40
+ const nameLength = buffer.readUInt16LE(offset + 28);
41
+ const extraLength = buffer.readUInt16LE(offset + 30);
42
+ const commentLength = buffer.readUInt16LE(offset + 32);
43
+ const localOffset = buffer.readUInt32LE(offset + 42);
44
+ const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLength);
45
+ entries.push({
46
+ name, method, compressedSize, size, localOffset,
47
+ crc: buffer.readUInt32LE(offset + 16), time: buffer.readUInt16LE(offset + 12), date: buffer.readUInt16LE(offset + 14),
48
+ });
49
+ offset += 46 + nameLength + extraLength + commentLength;
50
+ }
51
+ return entries;
52
+ }
53
+ function localHeader(buffer, entry) {
54
+ const at = entry.localOffset;
55
+ if (at + 30 > buffer.length || buffer.readUInt32LE(at) !== 0x04034b50)
56
+ throw new Error('broken local header for ' + entry.name);
57
+ const nameLength = buffer.readUInt16LE(at + 26);
58
+ const extraLength = buffer.readUInt16LE(at + 28);
59
+ return { dataStart: at + 30 + nameLength + extraLength, extraLength, name: buffer.toString('utf8', at + 30, at + 30 + nameLength) };
60
+ }
61
+ /** Die gespeicherten (ggf. komprimierten) Bytes eines Eintrags, unverändert. */
62
+ export function rawZipEntry(buffer, entry) {
63
+ const { dataStart } = localHeader(buffer, entry);
64
+ return Buffer.from(buffer.subarray(dataStart, dataStart + entry.compressedSize));
65
+ }
66
+ /** Inhalt eines Eintrags (stored oder deflate). */
67
+ export function readZipEntry(buffer, entry) {
68
+ const { dataStart } = localHeader(buffer, entry);
69
+ const data = buffer.subarray(dataStart, dataStart + entry.compressedSize);
70
+ if (entry.method === 0)
71
+ return Buffer.from(data);
72
+ if (entry.method === 8)
73
+ return inflateRawSync(data);
74
+ throw new Error('unsupported compression method ' + entry.method + ' in ' + entry.name);
75
+ }
76
+ function attr(tag, name) {
77
+ const match = new RegExp('\\s' + name.replace(':', '\\:') + '\\s*=\\s*("([^"]*)"|\'([^\']*)\')', 'i').exec(tag);
78
+ if (match === null)
79
+ return null;
80
+ return match[2] ?? match[3] ?? '';
81
+ }
82
+ function decodeXml(text) {
83
+ return text
84
+ .replace(/&lt;/g, '<')
85
+ .replace(/&gt;/g, '>')
86
+ .replace(/&quot;/g, '"')
87
+ .replace(/&apos;/g, "'")
88
+ .replace(/&amp;/g, '&');
89
+ }
90
+ function elementTexts(xml, qname) {
91
+ const out = [];
92
+ const re = new RegExp('<' + qname + '(\\s[^>]*)?>([\\s\\S]*?)</' + qname + '>', 'gi');
93
+ for (const match of xml.matchAll(re))
94
+ out.push(decodeXml((match[2] ?? '').trim()));
95
+ return out;
96
+ }
97
+ /** Verweis relativ zu einer Datei im Archiv aufloesen (ohne Fragment, dekodiert). */
98
+ function resolveRef(fromFile, ref) {
99
+ if (/^[a-z][a-z0-9+.-]*:/i.test(ref) || ref.startsWith('#') || ref.startsWith('//'))
100
+ return null;
101
+ const withoutFragment = ref.split('#')[0].split('?')[0];
102
+ if (withoutFragment === '')
103
+ return null;
104
+ let decoded = withoutFragment;
105
+ try {
106
+ decoded = decodeURIComponent(withoutFragment);
107
+ }
108
+ catch {
109
+ /* bleibt roh */
110
+ }
111
+ return posix.normalize(posix.join(posix.dirname(fromFile), decoded));
112
+ }
113
+ function expandHome(path) {
114
+ if (path === '~')
115
+ return homedir();
116
+ if (path.startsWith('~/'))
117
+ return homedir() + path.slice(1);
118
+ return path;
119
+ }
120
+ /** Prueft eine EPUB-Datei. Ergebnis im Umschlag der Werkzeuge (ok, warnings, …). */
121
+ export function checkEpubFile(path, options = {}) {
122
+ const file = expandHome(path);
123
+ if (!existsSync(file))
124
+ return transportError('not_found', 'The file does not exist: ' + path, { code: 'file_not_found', path });
125
+ let buffer;
126
+ try {
127
+ if (!statSync(file).isFile())
128
+ return transportError('invalid_argument', 'path is not a file: ' + path, { path });
129
+ buffer = readFileSync(file);
130
+ }
131
+ catch (error) {
132
+ return transportError('io_error', 'Cannot read the file: ' + String(error), { path });
133
+ }
134
+ const max = options.maxFindings ?? 200;
135
+ const findings = [];
136
+ let total = 0;
137
+ let errors = 0;
138
+ const checksRun = [];
139
+ const add = (check, severity, fileName, hint) => {
140
+ total += 1;
141
+ if (severity === 'error')
142
+ errors += 1;
143
+ if (findings.length < max)
144
+ findings.push({ check, severity, file: fileName, hint });
145
+ };
146
+ const result = (extra) => {
147
+ const warnings = [];
148
+ if (total > findings.length)
149
+ warnings.push(total + ' findings, only the first ' + findings.length + ' returned (max_findings).');
150
+ return {
151
+ ok: true,
152
+ warnings,
153
+ passed: errors === 0,
154
+ checked: 'file',
155
+ path,
156
+ bytes: buffer.length,
157
+ checks_run: checksRun,
158
+ errors,
159
+ findings,
160
+ findings_total: total,
161
+ findings_truncated: total > findings.length,
162
+ ...extra,
163
+ };
164
+ };
165
+ /* 1. ZIP-Aufbau */
166
+ checksRun.push('zip_structure');
167
+ let entries;
168
+ try {
169
+ entries = readZipEntries(buffer);
170
+ }
171
+ catch (error) {
172
+ add('zip_structure', 'error', null, 'Not a readable ZIP archive: ' + (error instanceof Error ? error.message : String(error)) + '.');
173
+ return result({});
174
+ }
175
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
176
+ const read = (name) => {
177
+ const entry = byName.get(name);
178
+ if (entry === undefined)
179
+ return null;
180
+ try {
181
+ return readZipEntry(buffer, entry).toString('utf8');
182
+ }
183
+ catch (error) {
184
+ add('zip_structure', 'error', name, 'Cannot read this entry: ' + (error instanceof Error ? error.message : String(error)) + '.');
185
+ return null;
186
+ }
187
+ };
188
+ const first = entries.slice().sort((a, b) => a.localOffset - b.localOffset)[0];
189
+ if (first === undefined || first.name !== 'mimetype') {
190
+ add('zip_structure', 'error', first?.name ?? null, 'The first entry of the archive must be "mimetype".');
191
+ }
192
+ else {
193
+ if (first.method !== 0)
194
+ add('zip_structure', 'error', 'mimetype', 'The mimetype entry must be stored uncompressed.');
195
+ try {
196
+ if (localHeader(buffer, first).extraLength !== 0)
197
+ add('zip_structure', 'warning', 'mimetype', 'The mimetype entry carries an extra field; EPUB readers expect none.');
198
+ }
199
+ catch {
200
+ add('zip_structure', 'error', 'mimetype', 'Broken local header of the mimetype entry.');
201
+ }
202
+ const mime = read('mimetype');
203
+ if (mime !== null && mime !== MIMETYPE)
204
+ add('zip_structure', 'error', 'mimetype', 'The mimetype entry must contain exactly "' + MIMETYPE + '".');
205
+ }
206
+ /* 2. container.xml */
207
+ checksRun.push('container');
208
+ const container = read('META-INF/container.xml');
209
+ if (container === null) {
210
+ add('container', 'error', 'META-INF/container.xml', 'META-INF/container.xml is missing.');
211
+ return result({ file_count: entries.length });
212
+ }
213
+ const rootTag = /<rootfile\b[^>]*>/i.exec(container)?.[0] ?? '';
214
+ const opfPath = attr(rootTag, 'full-path');
215
+ if (opfPath === null || opfPath === '') {
216
+ add('container', 'error', 'META-INF/container.xml', 'No rootfile full-path in container.xml.');
217
+ return result({ file_count: entries.length });
218
+ }
219
+ const opf = read(opfPath);
220
+ if (opf === null) {
221
+ add('container', 'error', opfPath, 'The package document named in container.xml is missing.');
222
+ return result({ file_count: entries.length });
223
+ }
224
+ /* 3. Metadaten */
225
+ checksRun.push('opf_metadata');
226
+ const packageTag = /<package\b[^>]*>/i.exec(opf)?.[0] ?? '';
227
+ const version = attr(packageTag, 'version');
228
+ const uniqueId = attr(packageTag, 'unique-identifier');
229
+ const titles = elementTexts(opf, 'dc:title');
230
+ const languages = elementTexts(opf, 'dc:language');
231
+ const creators = elementTexts(opf, 'dc:creator');
232
+ const identifierTags = [...opf.matchAll(/<dc:identifier(\s[^>]*)?>([\s\S]*?)<\/dc:identifier>/gi)];
233
+ const identifiers = identifierTags.map((match) => decodeXml((match[2] ?? '').trim()));
234
+ if (version === null)
235
+ add('opf_metadata', 'error', opfPath, 'The package element has no version.');
236
+ if (titles.length === 0 || titles.every((t) => t === ''))
237
+ add('opf_metadata', 'error', opfPath, 'dc:title is missing.');
238
+ if (languages.length === 0 || languages.every((t) => t === ''))
239
+ add('opf_metadata', 'error', opfPath, 'dc:language is missing.');
240
+ if (identifiers.length === 0)
241
+ add('opf_metadata', 'error', opfPath, 'dc:identifier is missing.');
242
+ else if (uniqueId !== null && !identifierTags.some((match) => attr(match[0], 'id') === uniqueId)) {
243
+ add('opf_metadata', 'error', opfPath, 'unique-identifier "' + uniqueId + '" names no dc:identifier.');
244
+ }
245
+ if (creators.length === 0)
246
+ add('opf_metadata', 'warning', opfPath, 'No dc:creator (author).');
247
+ /* 4. Manifest und Spine */
248
+ checksRun.push('manifest_files');
249
+ const opfDir = posix.dirname(opfPath);
250
+ const manifest = new Map();
251
+ for (const match of opf.matchAll(/<item\b[^>]*>/gi)) {
252
+ const tag = match[0];
253
+ const id = attr(tag, 'id') ?? '';
254
+ const href = decodeXml(attr(tag, 'href') ?? '');
255
+ const full = resolveRef(opfPath, href) ?? posix.join(opfDir, href);
256
+ manifest.set(id, { href, full, mediaType: attr(tag, 'media-type') ?? '', properties: attr(tag, 'properties') ?? '' });
257
+ if (href === '' || !byName.has(full))
258
+ add('manifest_files', 'error', full, 'The manifest names a file that is not in the archive.');
259
+ }
260
+ for (const match of opf.matchAll(/<itemref\b[^>]*>/gi)) {
261
+ const idref = attr(match[0], 'idref') ?? '';
262
+ if (!manifest.has(idref))
263
+ add('manifest_files', 'error', opfPath, 'The spine refers to "' + idref + '", which is not in the manifest.');
264
+ }
265
+ /* 5. Navigation */
266
+ checksRun.push('navigation');
267
+ const nav = [...manifest.values()].find((item) => item.properties.split(/\s+/).includes('nav'));
268
+ const spineTag = /<spine\b[^>]*>/i.exec(opf)?.[0] ?? '';
269
+ const ncxId = attr(spineTag, 'toc');
270
+ const ncx = ncxId === null ? undefined : manifest.get(ncxId);
271
+ const isEpub3 = version !== null && version.startsWith('3');
272
+ if (isEpub3 && nav === undefined)
273
+ add('navigation', 'error', opfPath, 'EPUB 3 needs a navigation document (manifest item with properties "nav").');
274
+ if (!isEpub3 && ncx === undefined)
275
+ add('navigation', 'error', opfPath, 'EPUB 2 needs an NCX table of contents (spine toc).');
276
+ let navEntries = null;
277
+ if (nav !== undefined) {
278
+ const navText = read(nav.full);
279
+ if (navText !== null) {
280
+ const tocNav = /<nav\b[^>]*epub:type\s*=\s*"[^"]*\btoc\b[^"]*"[^>]*>([\s\S]*?)<\/nav>/i.exec(navText);
281
+ navEntries = tocNav === null ? 0 : [...(tocNav[1] ?? '').matchAll(/<a\b/gi)].length;
282
+ if (tocNav === null)
283
+ add('navigation', 'error', nav.full, 'The navigation document has no toc nav.');
284
+ }
285
+ }
286
+ /* 6. Verweise in den Inhaltsdateien */
287
+ checksRun.push('references');
288
+ for (const item of manifest.values()) {
289
+ if (!/xhtml|html/i.test(item.mediaType) || !byName.has(item.full))
290
+ continue;
291
+ const text = read(item.full);
292
+ if (text === null)
293
+ continue;
294
+ for (const match of text.matchAll(/\s(src|href)\s*=\s*"([^"]*)"/gi)) {
295
+ const target = resolveRef(item.full, decodeXml(match[2] ?? ''));
296
+ if (target === null)
297
+ continue;
298
+ if (!byName.has(target))
299
+ add('references', 'error', item.full, '"' + (match[2] ?? '') + '" points to a file that is not in the archive.');
300
+ }
301
+ }
302
+ /* 7. Cover und Layout */
303
+ checksRun.push('cover');
304
+ const coverItem = [...manifest.values()].find((item) => item.properties.split(/\s+/).includes('cover-image'));
305
+ const coverMeta = /<meta\b[^>]*name\s*=\s*"cover"[^>]*>/i.exec(opf);
306
+ const coverMetaId = coverMeta === null ? null : attr(coverMeta[0], 'content');
307
+ const coverByMeta = coverMetaId === null ? undefined : manifest.get(coverMetaId);
308
+ const cover = coverItem ?? coverByMeta;
309
+ if (cover === undefined)
310
+ add('cover', 'warning', opfPath, 'No cover image inside the EPUB (manifest property "cover-image" or meta name="cover").');
311
+ const prepaginated = /<meta\b[^>]*property\s*=\s*"rendition:layout"[^>]*>\s*pre-paginated\s*</i.test(opf);
312
+ const fonts = [...manifest.values()].filter((item) => /font|opentype|truetype|woff/i.test(item.mediaType)).map((item) => item.full);
313
+ return result({
314
+ epub_version: version,
315
+ layout: prepaginated ? 'fixed_layout' : 'reflowable',
316
+ package_document: opfPath,
317
+ title: titles[0] ?? null,
318
+ language: languages[0] ?? null,
319
+ creators,
320
+ identifiers,
321
+ file_count: entries.length,
322
+ manifest_items: manifest.size,
323
+ nav_document: nav?.full ?? null,
324
+ nav_entries: navEntries,
325
+ ncx: ncx?.full ?? null,
326
+ cover_image: cover?.full ?? null,
327
+ fonts,
328
+ fonts_obfuscated: byName.has('META-INF/encryption.xml'),
329
+ });
330
+ }
331
+ //# sourceMappingURL=epub-check.js.map
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A-62 (Sitzung 60, Nachtrag, E-104): Ausrichtung eines fixed-layout-EPUB
3
+ * nach dem Export setzen. InDesign 21.6 schreibt `rendition:orientation
4
+ * landscape` auch bei Hochformatseiten; Kindle Previewer 4 sperrt das Buch
5
+ * dann auf Querformat (Befund 23.10.2026). Das DOM bietet dafür keine
6
+ * Einstellung (Probe 23.10.2026). Deshalb wird im Paketdokument (OPF) der
7
+ * Wert ersetzt und das Archiv neu geschrieben: mimetype zuerst und
8
+ * unkomprimiert, alle anderen Einträge byte-gleich übernommen, nur das OPF
9
+ * neu komprimiert. Die Datei wird über eine Zwischendatei ersetzt.
10
+ */
11
+ import { readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
12
+ import { deflateRawSync } from 'node:zlib';
13
+ import { rawZipEntry, readZipEntries, readZipEntry } from './epub-check.js';
14
+ const CRC_TABLE = (() => {
15
+ const table = new Uint32Array(256);
16
+ for (let n = 0; n < 256; n++) {
17
+ let c = n;
18
+ for (let k = 0; k < 8; k++)
19
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
20
+ table[n] = c >>> 0;
21
+ }
22
+ return table;
23
+ })();
24
+ export function crc32(data) {
25
+ let c = 0xffffffff;
26
+ for (let i = 0; i < data.length; i++)
27
+ c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
28
+ return (c ^ 0xffffffff) >>> 0;
29
+ }
30
+ /** Schreibt ein ZIP ohne Datendeskriptoren und ohne Zusatzfelder (UTF-8-Namen). */
31
+ export function writeZip(entries) {
32
+ const parts = [];
33
+ const central = [];
34
+ let offset = 0;
35
+ for (const entry of entries) {
36
+ const name = Buffer.from(entry.name, 'utf8');
37
+ const local = Buffer.alloc(30);
38
+ local.writeUInt32LE(0x04034b50, 0);
39
+ local.writeUInt16LE(20, 4);
40
+ local.writeUInt16LE(0x0800, 6);
41
+ local.writeUInt16LE(entry.method, 8);
42
+ local.writeUInt16LE(entry.time, 10);
43
+ local.writeUInt16LE(entry.date, 12);
44
+ local.writeUInt32LE(entry.crc, 14);
45
+ local.writeUInt32LE(entry.data.length, 18);
46
+ local.writeUInt32LE(entry.size, 22);
47
+ local.writeUInt16LE(name.length, 26);
48
+ parts.push(local, name, entry.data);
49
+ const cd = Buffer.alloc(46);
50
+ cd.writeUInt32LE(0x02014b50, 0);
51
+ cd.writeUInt16LE(20, 4);
52
+ cd.writeUInt16LE(20, 6);
53
+ cd.writeUInt16LE(0x0800, 8);
54
+ cd.writeUInt16LE(entry.method, 10);
55
+ cd.writeUInt16LE(entry.time, 12);
56
+ cd.writeUInt16LE(entry.date, 14);
57
+ cd.writeUInt32LE(entry.crc, 16);
58
+ cd.writeUInt32LE(entry.data.length, 20);
59
+ cd.writeUInt32LE(entry.size, 24);
60
+ cd.writeUInt16LE(name.length, 28);
61
+ cd.writeUInt32LE(offset, 42);
62
+ central.push(cd, name);
63
+ offset += 30 + name.length + entry.data.length;
64
+ }
65
+ const cdBuffer = Buffer.concat(central);
66
+ const end = Buffer.alloc(22);
67
+ end.writeUInt32LE(0x06054b50, 0);
68
+ end.writeUInt16LE(entries.length, 8);
69
+ end.writeUInt16LE(entries.length, 10);
70
+ end.writeUInt32LE(cdBuffer.length, 12);
71
+ end.writeUInt32LE(offset, 16);
72
+ return Buffer.concat([...parts, cdBuffer, end]);
73
+ }
74
+ /** Setzt rendition:orientation im OPF-Text; fehlt die Angabe, kommt sie hinter rendition:layout. */
75
+ export function setOrientationInOpf(opf, value) {
76
+ const re = /(<meta\b[^>]*property\s*=\s*"rendition:orientation"[^>]*>)\s*([^<]*?)\s*(<\/meta>)/i;
77
+ const found = re.exec(opf);
78
+ if (found !== null) {
79
+ return { opf: opf.replace(re, '$1' + value + '$3'), previous: found[2] ?? '' };
80
+ }
81
+ const layout = /(<meta\b[^>]*property\s*=\s*"rendition:layout"[^>]*>[^<]*<\/meta>)/i;
82
+ if (layout.test(opf)) {
83
+ return { opf: opf.replace(layout, '$1\n\t\t<meta property="rendition:orientation">' + value + '</meta>'), previous: null };
84
+ }
85
+ return { opf: opf.replace(/<\/metadata>/i, '\t<meta property="rendition:orientation">' + value + '</meta>\n\t</metadata>'), previous: null };
86
+ }
87
+ /** Ausrichtung in einer EPUB-Datei setzen. Wirft bei unlesbarem Archiv oder fehlendem OPF. */
88
+ export function setEpubOrientation(path, value) {
89
+ const buffer = readFileSync(path);
90
+ const entries = readZipEntries(buffer).sort((a, b) => a.localOffset - b.localOffset);
91
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
92
+ const container = byName.get('META-INF/container.xml');
93
+ if (container === undefined)
94
+ throw new Error('META-INF/container.xml is missing');
95
+ const containerText = readZipEntry(buffer, container).toString('utf8');
96
+ const opfPath = /full-path\s*=\s*"([^"]+)"/i.exec(containerText)?.[1];
97
+ const opfEntry = opfPath === undefined ? undefined : byName.get(opfPath);
98
+ if (opfEntry === undefined)
99
+ throw new Error('package document not found');
100
+ const before = readZipEntry(buffer, opfEntry).toString('utf8');
101
+ const { opf, previous } = setOrientationInOpf(before, value);
102
+ if (opf === before)
103
+ return { value, previous, changed: false, bytes: buffer.length };
104
+ const mime = byName.get('mimetype');
105
+ const ordered = mime === undefined ? entries : [mime, ...entries.filter((entry) => entry !== mime)];
106
+ const out = ordered.map((entry) => {
107
+ if (entry === opfEntry) {
108
+ const raw = Buffer.from(opf, 'utf8');
109
+ return { name: entry.name, method: 8, crc: crc32(raw), size: raw.length, data: deflateRawSync(raw), time: entry.time, date: entry.date };
110
+ }
111
+ if (entry === mime) {
112
+ const raw = readZipEntry(buffer, entry);
113
+ return { name: entry.name, method: 0, crc: crc32(raw), size: raw.length, data: raw, time: entry.time, date: entry.date };
114
+ }
115
+ return { name: entry.name, method: entry.method, crc: entry.crc, size: entry.size, data: rawZipEntry(buffer, entry), time: entry.time, date: entry.date };
116
+ });
117
+ const result = writeZip(out);
118
+ const temp = path + '.tmp-orientation';
119
+ writeFileSync(temp, result);
120
+ try {
121
+ renameSync(temp, path);
122
+ }
123
+ catch (error) {
124
+ try {
125
+ unlinkSync(temp);
126
+ }
127
+ catch { /* bleibt liegen */ }
128
+ throw error;
129
+ }
130
+ return { value, previous, changed: true, bytes: result.length };
131
+ }
132
+ //# sourceMappingURL=epub-orientation.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Freemium-Schalter (A-6 Nr. 4, E-60). Vorgabe: alles gesperrt.
3
+ *
4
+ * Absichtlich Konstanten im Code und kein Konfigurationsschluessel – was in
5
+ * config.json steht, koennte jeder Kunde aendern. Freigabe einzelner
6
+ * Werkzeuge: Namen in FREE_TOOLS eintragen; alle lesend gemeldeten Werkzeuge
7
+ * (readOnly) auf einmal: FREE_READ_ONLY_TOOLS auf true.
8
+ */
9
+ export const FREE_READ_ONLY_TOOLS = false;
10
+ export const FREE_TOOLS = new Set([]);
11
+ export function isFreeTool(name, readOnly) {
12
+ if (FREE_TOOLS.has(name))
13
+ return true;
14
+ return FREE_READ_ONLY_TOOLS && readOnly;
15
+ }
16
+ //# sourceMappingURL=free-tools.js.map