layoutbridge-spm 0.0.1 → 1.0.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/LICENSE +26 -0
  3. package/README.de.md +318 -0
  4. package/README.md +304 -4
  5. package/THIRD-PARTY-NOTICES.md +2878 -0
  6. package/dist/src/mcp/config.js +125 -0
  7. package/dist/src/mcp/dispatch.js +123 -0
  8. package/dist/src/mcp/license/free-tools.js +16 -0
  9. package/dist/src/mcp/license/gate.js +209 -0
  10. package/dist/src/mcp/license/index.js +96 -0
  11. package/dist/src/mcp/license/instance-name.js +56 -0
  12. package/dist/src/mcp/license/key-source.js +58 -0
  13. package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
  14. package/dist/src/mcp/license/placeholder-verifier.js +10 -0
  15. package/dist/src/mcp/license/polar-ids.js +63 -0
  16. package/dist/src/mcp/license/polar-verifier.js +265 -0
  17. package/dist/src/mcp/license/product-ids.js +72 -0
  18. package/dist/src/mcp/license/state-store.js +91 -0
  19. package/dist/src/mcp/license/types.js +11 -0
  20. package/dist/src/mcp/logging.js +270 -0
  21. package/dist/src/mcp/product-info.js +48 -0
  22. package/dist/src/mcp/server.js +250 -0
  23. package/dist/src/mcp/setup.js +608 -0
  24. package/dist/src/mcp/start.cjs +95 -0
  25. package/dist/src/mcp/tool-definition.js +2 -0
  26. package/dist/src/mcp/tools.js +2004 -0
  27. package/dist/src/mcp/typography-rules.js +70 -0
  28. package/dist/src/transport/com.js +250 -0
  29. package/dist/src/transport/osascript.js +171 -0
  30. package/dist/src/transport/queue.js +19 -0
  31. package/dist/src/transport/script-bundle.js +92 -0
  32. package/dist/src/transport/transport.js +30 -0
  33. package/indesign/commands/10-app.js +6 -0
  34. package/indesign/commands/20-documents.js +513 -0
  35. package/indesign/commands/22-document-setup.js +118 -0
  36. package/indesign/commands/23-save-package.js +255 -0
  37. package/indesign/commands/25-export.js +379 -0
  38. package/indesign/commands/26-export-image.js +197 -0
  39. package/indesign/commands/27-textdefaults.js +151 -0
  40. package/indesign/commands/28-selection.js +174 -0
  41. package/indesign/commands/30-pages.js +1032 -0
  42. package/indesign/commands/31-view.js +210 -0
  43. package/indesign/commands/32-sections.js +203 -0
  44. package/indesign/commands/35-parents.js +428 -0
  45. package/indesign/commands/37-layout.js +760 -0
  46. package/indesign/commands/40-frames.js +836 -0
  47. package/indesign/commands/41-shapes.js +505 -0
  48. package/indesign/commands/42-groups.js +117 -0
  49. package/indesign/commands/43-layers.js +291 -0
  50. package/indesign/commands/44-effects.js +274 -0
  51. package/indesign/commands/45-frame-options.js +247 -0
  52. package/indesign/commands/47-anchored.js +287 -0
  53. package/indesign/commands/50-modify.js +781 -0
  54. package/indesign/commands/52-align.js +139 -0
  55. package/indesign/commands/55-fit.js +106 -0
  56. package/indesign/commands/60-images.js +771 -0
  57. package/indesign/commands/62-embed.js +120 -0
  58. package/indesign/commands/69-placeholders.js +127 -0
  59. package/indesign/commands/70-text.js +567 -0
  60. package/indesign/commands/71-outlines.js +92 -0
  61. package/indesign/commands/72-place-text.js +366 -0
  62. package/indesign/commands/74-stories.js +116 -0
  63. package/indesign/commands/75-tables.js +676 -0
  64. package/indesign/commands/76-text-variables.js +260 -0
  65. package/indesign/commands/77-story-options.js +86 -0
  66. package/indesign/commands/78-frame-lines.js +304 -0
  67. package/indesign/commands/79-check-typography.js +303 -0
  68. package/indesign/commands/80-styles.js +1620 -0
  69. package/indesign/commands/81-composition.js +355 -0
  70. package/indesign/commands/82-object-styles.js +263 -0
  71. package/indesign/commands/83-indents-tabs.js +237 -0
  72. package/indesign/commands/84-import-styles.js +142 -0
  73. package/indesign/commands/85-swatches.js +112 -0
  74. package/indesign/commands/86-gradients.js +185 -0
  75. package/indesign/commands/87-replace-swatch.js +180 -0
  76. package/indesign/commands/88-replace-font.js +217 -0
  77. package/indesign/interpreter.js +758 -0
  78. package/package.json +55 -6
  79. package/rules/typography-rules.json +31 -0
@@ -0,0 +1,771 @@
1
+ /* Bereich F – Bilder und Verknuepfungen. */
2
+
3
+ /* Verknuepfungsstatus als Text. longForm: LINK_MISSING/LINK_OUT_OF_DATE (list_links), sonst MISSING/OUT_OF_DATE. */
4
+ function idcLinkStatusName(status, longForm) {
5
+ if (status === null || status === undefined) { return null; }
6
+ if (typeof LinkStatus !== 'undefined') {
7
+ if (IDC.enumIs(status, LinkStatus.NORMAL)) { return 'NORMAL'; }
8
+ if (IDC.enumIs(status, LinkStatus.LINK_MISSING)) { return longForm ? 'LINK_MISSING' : 'MISSING'; }
9
+ if (IDC.enumIs(status, LinkStatus.LINK_OUT_OF_DATE)) { return longForm ? 'LINK_OUT_OF_DATE' : 'OUT_OF_DATE'; }
10
+ if (IDC.enumIs(status, LinkStatus.LINK_EMBEDDED)) { return 'EMBEDDED'; }
11
+ }
12
+ var text = String(status);
13
+ if (!longForm) { text = text.replace(/^LINK_/, ''); }
14
+ return text;
15
+ }
16
+
17
+ function idcLinkOf(graphic) {
18
+ var link = null;
19
+ try {
20
+ link = graphic.itemLink;
21
+ if (link && link.isValid !== false) { return link; }
22
+ } catch (e) { link = null; }
23
+ return null;
24
+ }
25
+
26
+ /* Alle Grafiken eines Rahmens (auch in Gruppen) als Array. */
27
+ function idcGraphicsOf(item) {
28
+ var out = [];
29
+ var i, all;
30
+ try {
31
+ all = item.allGraphics;
32
+ for (i = 0; i < all.length; i++) { out.push(IDC.concrete(all[i])); }
33
+ if (out.length > 0) { return out; }
34
+ } catch (e) { /* weiter */ }
35
+ try {
36
+ all = item.graphics;
37
+ for (i = 0; i < all.length; i++) { out.push(IDC.concrete(all.item(i))); }
38
+ } catch (e2) { /* keine */ }
39
+ return out;
40
+ }
41
+
42
+ /* Rahmen fuer Grafiken: Rechteck, Oval, Polygon – keine Gruppe, kein Text, keine Linie. */
43
+ function idcGraphicFrameArg(doc, args, field) {
44
+ var found = idcFrameArg(doc, args, field);
45
+ var className = IDC.className(found.item);
46
+ if (className !== 'Rectangle' && className !== 'Oval' && className !== 'Polygon') {
47
+ IDC.fail('invalid_state', 'Object ' + args[field] + ' is a ' + IDC.itemType(found.item) + ' (' + className + '), not a graphic frame.',
48
+ { frame_id: args[field], type: IDC.itemType(found.item) });
49
+ }
50
+ return found;
51
+ }
52
+
53
+ /* ppi [x, y] -> { x, y, min } oder null. */
54
+ /*
55
+ * Mindestaufloesung fuer die Warnung (G-1): Parameter min_ppi, sonst 300 –
56
+ * der uebliche Offsetwert (doppelte 150-lpi-Rasterweite). Konfigurierbar
57
+ * ueber die Serverkonfiguration (E-54), die den Wert in die args legt.
58
+ */
59
+ function idcMinPpi(args) {
60
+ if (args && typeof args.min_ppi === 'number' && isFinite(args.min_ppi) && args.min_ppi > 0) {
61
+ return args.min_ppi;
62
+ }
63
+ return 300;
64
+ }
65
+
66
+ function idcPpi(value) {
67
+ var x, y;
68
+ try {
69
+ if (IDC.isArray(value) && value.length >= 2) {
70
+ x = IDC.round(Number(value[0]), 1);
71
+ y = IDC.round(Number(value[1]), 1);
72
+ if (x === null || y === null) { return null; }
73
+ return { x: x, y: y, min: Math.min(x, y) };
74
+ }
75
+ if (typeof value === 'number') { return { x: IDC.round(value, 1), y: IDC.round(value, 1), min: IDC.round(value, 1) }; }
76
+ } catch (e) { return null; }
77
+ return null;
78
+ }
79
+
80
+ /* Rueckfallkette, wenn die gewuenschte PDF-Box fehlt (Fund 1, Hefttest Sitzung 6). */
81
+ function idcPdfCropFallbacks(name) {
82
+ if (name === 'bleed') { return ['trim', 'crop', 'media']; }
83
+ if (name === 'trim') { return ['crop', 'media']; }
84
+ if (name === 'art' || name === 'content' || name === 'crop') { return ['crop', 'media']; }
85
+ return [];
86
+ }
87
+
88
+ function idcPdfCropFor(name) {
89
+ if (typeof PDFCrop === 'undefined') { return null; }
90
+ if (name === 'media') { return PDFCrop.CROP_MEDIA; }
91
+ if (name === 'crop') { return PDFCrop.CROP_PDF; }
92
+ if (name === 'trim') { return PDFCrop.CROP_TRIM; }
93
+ if (name === 'bleed') { return PDFCrop.CROP_BLEED; }
94
+ if (name === 'art') { return PDFCrop.CROP_ART; }
95
+ if (name === 'content') { return PDFCrop.CROP_CONTENT; }
96
+ return null;
97
+ }
98
+
99
+ function idcFitOptionFor(name) {
100
+ if (typeof FitOptions === 'undefined') { return null; }
101
+ if (name === 'fill_proportionally') { return FitOptions.FILL_PROPORTIONALLY; }
102
+ if (name === 'fit_proportionally') { return FitOptions.PROPORTIONALLY; }
103
+ if (name === 'fit_content_to_frame') { return FitOptions.CONTENT_TO_FRAME; }
104
+ if (name === 'center_content') { return FitOptions.CENTER_CONTENT; }
105
+ return null;
106
+ }
107
+
108
+ /* Deckt die Grafik den Rahmen bis auf 0,2 mm? */
109
+ function idcCoversFrame(frameBounds, graphicBounds) {
110
+ var t = 0.2;
111
+ if (!frameBounds || !graphicBounds) { return false; }
112
+ return graphicBounds.x <= frameBounds.x + t
113
+ && graphicBounds.y <= frameBounds.y + t
114
+ && graphicBounds.x + graphicBounds.width >= frameBounds.x + frameBounds.width - t
115
+ && graphicBounds.y + graphicBounds.height >= frameBounds.y + frameBounds.height - t;
116
+ }
117
+
118
+ /* Beschreibung einer Grafik: Datei, Status, ppi, Farbraum, Art, Begrenzungen. */
119
+ function idcDescribeGraphic(doc, frame, graphic, page) {
120
+ var link = idcLinkOf(graphic);
121
+ var entry = {
122
+ graphic_id: null,
123
+ link_id: null,
124
+ file_name: null,
125
+ file_path: null,
126
+ link_status: null,
127
+ effective_ppi: null,
128
+ effective_ppi_xy: null,
129
+ actual_ppi: null,
130
+ actual_ppi_xy: null,
131
+ color_space: null,
132
+ image_type: IDC.className(graphic),
133
+ is_pdf: false,
134
+ frame_bounds: null,
135
+ graphic_bounds: null,
136
+ hints: []
137
+ };
138
+ var ppi;
139
+ try { entry.graphic_id = Number(graphic.id); } catch (e0) { entry.graphic_id = null; }
140
+ if (link !== null) {
141
+ try { entry.link_id = Number(link.id); } catch (e1) { entry.link_id = null; }
142
+ try { entry.file_name = String(link.name); } catch (e2) { entry.file_name = null; }
143
+ try { entry.file_path = String(link.filePath); } catch (e3) { entry.file_path = null; }
144
+ try { entry.link_status = idcLinkStatusName(link.status, false); } catch (e4) { entry.link_status = null; }
145
+ } else {
146
+ entry.hints.push('err: no link object (pasted or embedded graphic without link)');
147
+ }
148
+ try {
149
+ if (graphic.imageTypeName !== undefined && graphic.imageTypeName !== null) { entry.image_type = String(graphic.imageTypeName); }
150
+ } catch (e8) { /* Klassenname bleibt */ }
151
+ /* Platzierte PDFs heissen "Adobe PDF" (imageTypeName) bzw. "PDF" (Klasse) – Live-Fund 04.09.2026. */
152
+ entry.is_pdf = /pdf/i.test(entry.image_type) || IDC.className(graphic) === 'PDF';
153
+ try {
154
+ ppi = idcPpi(graphic.effectivePpi);
155
+ if (ppi !== null) { entry.effective_ppi = ppi.min; entry.effective_ppi_xy = [ppi.x, ppi.y]; }
156
+ } catch (e5) { if (!entry.is_pdf) { entry.hints.push('err: effective ppi not available (' + String(e5.message ? e5.message : e5) + ')'); } }
157
+ try {
158
+ ppi = idcPpi(graphic.actualPpi);
159
+ if (ppi !== null) { entry.actual_ppi = ppi.min; entry.actual_ppi_xy = [ppi.x, ppi.y]; }
160
+ } catch (e6) { if (!entry.is_pdf) { entry.hints.push('err: actual ppi not available (' + String(e6.message ? e6.message : e6) + ')'); } }
161
+ try {
162
+ if (graphic.space !== undefined && graphic.space !== null) { entry.color_space = String(graphic.space).toUpperCase(); }
163
+ } catch (e7) { if (!entry.is_pdf) { entry.hints.push('err: colour space not available (' + String(e7.message ? e7.message : e7) + ')'); } }
164
+ if (entry.is_pdf && (entry.effective_ppi === null || entry.color_space === null)) {
165
+ entry.hints.push('err: InDesign reports neither ppi nor colour space for placed PDFs – check the file itself');
166
+ }
167
+ try { entry.frame_bounds = IDC.boundsOnPage(doc, frame, page); } catch (e9) { entry.frame_bounds = null; }
168
+ try { entry.graphic_bounds = IDC.boundsOnPage(doc, graphic, page); } catch (e10) { entry.graphic_bounds = null; }
169
+ return entry;
170
+ }
171
+
172
+ /* -------------------------------------------------------------- place_image */
173
+
174
+ IDC.register('place_image', { needs_document: true, read_only: false, ruler: 'page' },
175
+ function (ctx) {
176
+ var args = ctx.args;
177
+ var doc = ctx.doc;
178
+ var found = idcGraphicFrameArg(doc, args, 'frame_id');
179
+ var frame = found.item;
180
+ var path = args.image_path;
181
+ var fit = args.fit === undefined || args.fit === null ? null : args.fit;
182
+ var crop = args.pdf_crop === undefined || args.pdf_crop === null ? null : args.pdf_crop;
183
+ var pdfPage = args.pdf_page === undefined || args.pdf_page === null ? 1 : args.pdf_page;
184
+ var fallback = args.pdf_crop_fallback !== false;
185
+ var file, isPdf, prevCrop = null, prevPage = null, cropSet = false, pageSet = false;
186
+ var placed, graphic, removed = 0, i, graphics, fitOption, page, frameBounds, graphicBounds, fits, entry;
187
+ var cropUsed = crop, attempts, attempt, lastError = null, firstErrorMessage = '', k, minPpi;
188
+
189
+ if (typeof path !== 'string' || path === '') {
190
+ IDC.fail('invalid_argument', 'image_path is required: a real file path on the machine running InDesign.', null);
191
+ }
192
+ if (fit !== null && idcFitOptionFor(fit) === null && typeof FitOptions !== 'undefined') {
193
+ IDC.fail('invalid_argument', 'fit must be fill_proportionally, fit_proportionally, fit_content_to_frame or center_content.', { fit: fit });
194
+ }
195
+ if (crop !== null && idcPdfCropFor(crop) === null && typeof PDFCrop !== 'undefined') {
196
+ IDC.fail('invalid_argument', 'pdf_crop must be media, crop, trim, bleed, art or content.', { pdf_crop: crop });
197
+ }
198
+ if (typeof pdfPage !== 'number' || pdfPage < 1 || Math.floor(pdfPage) !== pdfPage) {
199
+ IDC.fail('invalid_argument', 'pdf_page must be a whole number of 1 or more.', { pdf_page: pdfPage });
200
+ }
201
+ file = IDC.fileFor(path);
202
+ if (!file.exists) {
203
+ IDC.fail('not_found', 'File not found: ' + path, { image_path: path });
204
+ }
205
+ if (IDC.fileSize(file) === 0) {
206
+ IDC.fail('io_error', 'File is empty (0 bytes) – a cloud placeholder that is not downloaded yet? ' + path, { image_path: path });
207
+ }
208
+ isPdf = /\.(pdf|ai)$/i.test(path);
209
+
210
+ /* Bestehende Grafik entfernen – erneutes Platzieren setzt Zuschnitt und Skalierung zurueck. */
211
+ graphics = idcGraphicsOf(frame);
212
+ for (i = graphics.length - 1; i >= 0; i--) {
213
+ try { graphics[i].remove(); removed += 1; } catch (er) { /* dann ersetzt place() */ }
214
+ }
215
+ if (removed > 0) { ctx.warn('The frame held ' + removed + ' graphic(s); they were replaced (crop and scale reset).'); }
216
+
217
+ try {
218
+ if (typeof ContentType !== 'undefined') { frame.contentType = ContentType.GRAPHIC_TYPE; }
219
+ } catch (ec) { /* Inhaltstyp bleibt */ }
220
+
221
+ /*
222
+ * Platzieren mit PDF-Importvorgabe nur fuer diesen Versuch. Fehlt der PDF
223
+ * die gewuenschte Box (z. B. keine TrimBox in einem QR-Code-PDF), wirft
224
+ * InDesign; dann Rueckfall auf die naechstgroessere Box mit Warnung
225
+ * (pdf_crop_fallback false schaltet das ab).
226
+ */
227
+ attempts = [crop];
228
+ if (isPdf && crop !== null && fallback) { attempts = attempts.concat(idcPdfCropFallbacks(crop)); }
229
+ placed = null;
230
+ for (k = 0; k < attempts.length; k++) {
231
+ attempt = attempts[k];
232
+ cropSet = false; pageSet = false;
233
+ try {
234
+ if (isPdf && typeof app.pdfPlacePreferences !== 'undefined') {
235
+ if (attempt !== null) {
236
+ prevCrop = app.pdfPlacePreferences.pdfCrop;
237
+ app.pdfPlacePreferences.pdfCrop = idcPdfCropFor(attempt);
238
+ cropSet = true;
239
+ }
240
+ prevPage = app.pdfPlacePreferences.pageNumber;
241
+ app.pdfPlacePreferences.pageNumber = pdfPage;
242
+ pageSet = true;
243
+ }
244
+ } catch (ep) {
245
+ ctx.warn('Could not set the PDF place preferences: ' + String(ep.message ? ep.message : ep));
246
+ }
247
+ try {
248
+ placed = frame.place(file);
249
+ lastError = null;
250
+ } catch (e) {
251
+ lastError = e;
252
+ placed = null;
253
+ } finally {
254
+ try { if (cropSet) { app.pdfPlacePreferences.pdfCrop = prevCrop; } } catch (er1) { ctx.warn('Could not restore the PDF crop preference.'); }
255
+ try { if (pageSet) { app.pdfPlacePreferences.pageNumber = prevPage; } } catch (er2) { ctx.warn('Could not restore the PDF page preference.'); }
256
+ }
257
+ if (lastError === null) {
258
+ cropUsed = attempt;
259
+ if (k > 0) {
260
+ ctx.warn('pdf_crop "' + crop + '" failed (InDesign: ' + firstErrorMessage + '); the PDF was placed with pdf_crop "' +
261
+ attempt + '" instead – the box is larger, check the position.');
262
+ }
263
+ break;
264
+ }
265
+ if (k === 0) { firstErrorMessage = String(lastError.message ? lastError.message : lastError); }
266
+ if (idcGraphicsOf(frame).length > 0) {
267
+ /* Trotz Fehler etwas platziert? Dann nicht nochmals. */
268
+ break;
269
+ }
270
+ }
271
+ if (lastError !== null) {
272
+ IDC.fail('io_error', 'InDesign could not place ' + path + ': ' + String(lastError.message ? lastError.message : lastError) +
273
+ (attempts.length > 1 ? ' (tried pdf_crop ' + attempts.join(', ') + ')' : ''), { image_path: path, frame_id: args.frame_id, pdf_crop_tried: attempts });
274
+ }
275
+ /*
276
+ * place() liefert in ExtendScript ein Array, das IDC.isArray nicht als
277
+ * solches erkennt (Live-Fund 04.09.2026). Deshalb zuerst die Grafik im
278
+ * Rahmen frisch lesen, place()-Rueckgabe nur als Rueckfall.
279
+ */
280
+ graphic = null;
281
+ graphics = idcGraphicsOf(frame);
282
+ if (graphics.length > 0) { graphic = graphics[0]; }
283
+ if (graphic === null) {
284
+ try {
285
+ if (placed && typeof placed.length === 'number' && placed.id === undefined && placed.length > 0) { graphic = IDC.concrete(placed[0]); }
286
+ else if (placed) { graphic = IDC.concrete(placed); }
287
+ } catch (eg) { graphic = null; }
288
+ }
289
+ if (graphic === null) {
290
+ IDC.fail('io_error', 'place() returned no graphic for ' + path, { image_path: path, frame_id: args.frame_id });
291
+ }
292
+
293
+ if (fit !== null) {
294
+ fitOption = idcFitOptionFor(fit);
295
+ try {
296
+ if (fitOption !== null) { frame.fit(fitOption); }
297
+ } catch (ef) {
298
+ ctx.warn('The graphic was placed but fit "' + fit + '" failed: ' + String(ef.message ? ef.message : ef));
299
+ }
300
+ }
301
+
302
+ page = idcPageOfItem(doc, frame);
303
+ entry = idcDescribeGraphic(doc, frame, graphic, page);
304
+ frameBounds = entry.frame_bounds;
305
+ graphicBounds = entry.graphic_bounds;
306
+ fits = idcCoversFrame(frameBounds, graphicBounds);
307
+ if (!fits) {
308
+ ctx.warn('The graphic does not cover the frame (tolerance 0.2 mm): frame ' + IDC.stringify(frameBounds) + ', graphic ' + IDC.stringify(graphicBounds) +
309
+ '. Use fit "fill_proportionally" or scale_image_in_frame.');
310
+ }
311
+ if (entry.link_status !== null && entry.link_status !== 'NORMAL' && entry.link_status !== 'EMBEDDED') {
312
+ ctx.warn('Link status after placing: ' + entry.link_status + '.');
313
+ }
314
+ minPpi = idcMinPpi(args);
315
+ if (entry.effective_ppi !== null && entry.effective_ppi < minPpi && !entry.is_pdf) {
316
+ ctx.warn('Effective resolution is ' + entry.effective_ppi + ' ppi – below ' + minPpi + ' ppi (min_ppi).');
317
+ }
318
+ var h;
319
+ for (h = 0; h < entry.hints.length; h++) { if (!entry.is_pdf || !/ppi|colour/.test(entry.hints[h])) { ctx.warn(entry.hints[h]); } }
320
+
321
+ ctx.changes = { modified_frame_ids: [Number(frame.id)], created_graphic_ids: [entry.graphic_id] };
322
+ return {
323
+ frame_id: Number(frame.id),
324
+ graphic_id: entry.graphic_id,
325
+ link_id: entry.link_id,
326
+ file_name: entry.file_name,
327
+ link_status: entry.link_status,
328
+ image_type: entry.image_type,
329
+ is_pdf: entry.is_pdf,
330
+ fit: fit,
331
+ pdf_crop: isPdf ? crop : null,
332
+ pdf_crop_used: isPdf ? cropUsed : null,
333
+ pdf_page: isPdf ? pdfPage : null,
334
+ frame_bounds_mm: frameBounds,
335
+ graphic_bounds_mm: graphicBounds,
336
+ fits_frame: fits,
337
+ effective_ppi: entry.effective_ppi,
338
+ color_space: entry.color_space,
339
+ graphics_replaced: removed,
340
+ page_id: page === null ? null : Number(page.id),
341
+ on_parent_page: found.on_parent
342
+ };
343
+ });
344
+
345
+ /* ----------------------------------------------------- scale_image_in_frame */
346
+
347
+ /* Ankerpunkt {x, y} aus einer Begrenzung. */
348
+ function idcAnchorPoint(bounds, anchor) {
349
+ var x = bounds.x + bounds.width / 2;
350
+ var y = bounds.y + bounds.height / 2;
351
+ if (/left/.test(anchor)) { x = bounds.x; }
352
+ if (/right/.test(anchor)) { x = bounds.x + bounds.width; }
353
+ if (/top/.test(anchor)) { y = bounds.y; }
354
+ if (/bottom/.test(anchor)) { y = bounds.y + bounds.height; }
355
+ return { x: x, y: y };
356
+ }
357
+
358
+ IDC.register('scale_image_in_frame', { needs_document: true, read_only: false, ruler: 'page' },
359
+ function (ctx) {
360
+ var args = ctx.args;
361
+ var doc = ctx.doc;
362
+ var found = idcGraphicFrameArg(doc, args, 'frame_id');
363
+ var frame = found.item;
364
+ var zoom = args.zoom_factor;
365
+ var anchor = args.anchor === undefined || args.anchor === null ? 'center' : args.anchor;
366
+ var anchors = { center: 1, top: 1, bottom: 1, left: 1, right: 1, top_left: 1, top_right: 1, bottom_left: 1, bottom_right: 1 };
367
+ var minPpi;
368
+ var graphics, graphic, page, frameBounds, before, pivot, after, rotation, dx, entry;
369
+
370
+ if (typeof zoom !== 'number' || !isFinite(zoom) || !(zoom > 0)) {
371
+ IDC.fail('invalid_argument', 'zoom_factor must be a number greater than 0 (1.2 = 20 % larger).', { zoom_factor: zoom });
372
+ }
373
+ if (typeof anchor !== 'string' || !IDC.hasOwn(anchors, anchor)) {
374
+ IDC.fail('invalid_argument', 'anchor must be center, top, bottom, left, right, top_left, top_right, bottom_left or bottom_right.', { anchor: anchor });
375
+ }
376
+ graphics = idcGraphicsOf(frame);
377
+ if (graphics.length === 0) {
378
+ IDC.fail('invalid_state', 'Frame ' + args.frame_id + ' holds no graphic.', { frame_id: args.frame_id });
379
+ }
380
+ graphic = graphics[0];
381
+ if (graphics.length > 1) { ctx.warn('The frame holds ' + graphics.length + ' graphics; only the first was scaled.'); }
382
+ rotation = idcRotationOf(graphic);
383
+ if (rotation !== null && rotation !== 0) { ctx.warn('The graphic is rotated by ' + rotation + ' degrees; scaling via bounds may shift it.'); }
384
+
385
+ page = idcPageOfItem(doc, frame);
386
+ frameBounds = IDC.boundsOnPage(doc, frame, page);
387
+ before = IDC.boundsOnPage(doc, graphic, page);
388
+ pivot = idcAnchorPoint(frameBounds, anchor);
389
+ after = {
390
+ x: pivot.x + (before.x - pivot.x) * zoom,
391
+ y: pivot.y + (before.y - pivot.y) * zoom,
392
+ width: before.width * zoom,
393
+ height: before.height * zoom
394
+ };
395
+ dx = page === null ? 0 : IDC.spreadOffsetOfPage(page);
396
+ try {
397
+ IDC.withRulerOrigin(doc, RulerOrigin.SPREAD_ORIGIN, function () {
398
+ graphic.geometricBounds = [after.y, after.x + dx, after.y + after.height, after.x + after.width + dx];
399
+ });
400
+ } catch (e) {
401
+ IDC.fail('script_error', 'InDesign could not scale the graphic in frame ' + args.frame_id + ': ' + String(e.message ? e.message : e),
402
+ { frame_id: args.frame_id, zoom_factor: zoom });
403
+ }
404
+
405
+ entry = idcDescribeGraphic(doc, frame, graphic, page);
406
+ minPpi = idcMinPpi(args);
407
+ if (entry.effective_ppi !== null && entry.effective_ppi < minPpi && !entry.is_pdf) {
408
+ ctx.warn('Effective resolution after zoom is ' + entry.effective_ppi + ' ppi – below ' + minPpi + ' ppi (min_ppi).');
409
+ }
410
+ if (!idcCoversFrame(entry.frame_bounds, entry.graphic_bounds)) {
411
+ ctx.warn('The graphic no longer covers the whole frame (tolerance 0.2 mm).');
412
+ }
413
+ ctx.changes = { modified_frame_ids: [Number(frame.id)] };
414
+ return {
415
+ frame_id: Number(frame.id),
416
+ graphic_id: entry.graphic_id,
417
+ zoom_factor: zoom,
418
+ anchor: anchor,
419
+ graphic_bounds_mm: entry.graphic_bounds,
420
+ frame_bounds_mm: entry.frame_bounds,
421
+ graphic_bounds_before_mm: before,
422
+ effective_ppi: entry.effective_ppi,
423
+ effective_ppi_xy: entry.effective_ppi_xy,
424
+ fits_frame: idcCoversFrame(entry.frame_bounds, entry.graphic_bounds)
425
+ };
426
+ });
427
+
428
+ /* -------------------------------------------------- position_image_in_frame */
429
+
430
+ /*
431
+ * Sitzung 24 (Befund aus dem Praxiseinsatz, E-79): Grafik im Rahmen verschieben,
432
+ * ohne sie zu skalieren und ohne den Rahmen anzufassen – an einem der neun
433
+ * Bezugspunkte ausrichten (Bezugspunkt der Grafik auf denselben des
434
+ * Rahmens) und/oder um offset_mm versetzen. Schreibweg wie
435
+ * scale_image_in_frame: Begrenzung der Grafik unter Bogen-Nullpunkt, gleiche
436
+ * Groesse.
437
+ */
438
+ IDC.register('position_image_in_frame', { needs_document: true, read_only: false, ruler: 'page' },
439
+ function (ctx) {
440
+ var args = ctx.args;
441
+ var doc = ctx.doc;
442
+ var found = idcGraphicFrameArg(doc, args, 'frame_id');
443
+ var frame = found.item;
444
+ var anchors = { center: 1, top: 1, bottom: 1, left: 1, right: 1, top_left: 1, top_right: 1, bottom_left: 1, bottom_right: 1 };
445
+ var align = args.align === undefined || args.align === null ? null : args.align;
446
+ var offset = null;
447
+ var graphics, graphic, page, frameBounds, before, target, frameAnchor, graphicAnchor, rotation, dxPage, entry, frameAfter;
448
+
449
+ if (align !== null && (typeof align !== 'string' || !IDC.hasOwn(anchors, align))) {
450
+ IDC.fail('invalid_argument', 'align must be center, top, bottom, left, right, top_left, top_right, bottom_left or bottom_right.', { align: align });
451
+ }
452
+ if (args.offset_mm !== undefined && args.offset_mm !== null) { offset = idcPointArg(args.offset_mm, 'offset_mm'); }
453
+ if (align === null && offset === null) {
454
+ IDC.fail('invalid_argument', 'Pass align, offset_mm or both.', null);
455
+ }
456
+ graphics = idcGraphicsOf(frame);
457
+ if (graphics.length === 0) {
458
+ IDC.fail('invalid_state', 'Frame ' + args.frame_id + ' holds no graphic.', { frame_id: args.frame_id });
459
+ }
460
+ graphic = graphics[0];
461
+ if (graphics.length > 1) { ctx.warn('The frame holds ' + graphics.length + ' graphics; only the first was positioned.'); }
462
+ rotation = idcRotationOf(graphic);
463
+ if (rotation !== null && rotation !== 0) { ctx.warn('The graphic is rotated by ' + rotation + ' degrees; alignment uses its bounding box.'); }
464
+
465
+ page = idcPageOfItem(doc, frame);
466
+ frameBounds = IDC.boundsOnPage(doc, frame, page);
467
+ before = IDC.boundsOnPage(doc, graphic, page);
468
+ target = { x: before.x, y: before.y };
469
+ if (align !== null) {
470
+ frameAnchor = idcAnchorPoint(frameBounds, align);
471
+ graphicAnchor = idcAnchorPoint(before, align);
472
+ target.x = before.x + (frameAnchor.x - graphicAnchor.x);
473
+ target.y = before.y + (frameAnchor.y - graphicAnchor.y);
474
+ }
475
+ if (offset !== null) {
476
+ target.x += offset.x;
477
+ target.y += offset.y;
478
+ }
479
+ dxPage = page === null ? 0 : IDC.spreadOffsetOfPage(page);
480
+ try {
481
+ IDC.withRulerOrigin(doc, RulerOrigin.SPREAD_ORIGIN, function () {
482
+ graphic.geometricBounds = [target.y, target.x + dxPage, target.y + before.height, target.x + before.width + dxPage];
483
+ });
484
+ } catch (e) {
485
+ IDC.fail('script_error', 'InDesign could not move the graphic in frame ' + args.frame_id + ': ' + String(e.message ? e.message : e),
486
+ { frame_id: args.frame_id });
487
+ }
488
+
489
+ entry = idcDescribeGraphic(doc, frame, graphic, page);
490
+ frameAfter = entry.frame_bounds;
491
+ if (!idcCoversFrame(entry.frame_bounds, entry.graphic_bounds)) {
492
+ ctx.warn('The graphic no longer covers the whole frame (tolerance 0.2 mm) – part of the frame shows no picture.');
493
+ }
494
+ ctx.changes = { modified_frame_ids: [Number(frame.id)] };
495
+ return {
496
+ frame_id: Number(frame.id),
497
+ graphic_id: entry.graphic_id,
498
+ align: align,
499
+ offset_mm: offset,
500
+ moved_by_mm: { x: IDC.round(target.x - before.x), y: IDC.round(target.y - before.y) },
501
+ graphic_bounds_mm: entry.graphic_bounds,
502
+ graphic_bounds_before_mm: before,
503
+ frame_bounds_mm: frameAfter,
504
+ coordinate_space: page === null ? 'spread' : 'page',
505
+ page_id: page === null ? null : Number(page.id),
506
+ fits_frame: idcCoversFrame(entry.frame_bounds, entry.graphic_bounds)
507
+ };
508
+ });
509
+
510
+ /* ------------------------------------------------------ get_image_link_info */
511
+
512
+ IDC.register('get_image_link_info', { needs_document: true, read_only: true, ruler: 'page' },
513
+ function (ctx) {
514
+ var args = ctx.args;
515
+ var doc = ctx.doc;
516
+ var found = idcFrameArg(doc, args, 'frame_id');
517
+ var frame = found.item;
518
+ var graphics = idcGraphicsOf(frame);
519
+ var page = idcPageOfItem(doc, frame);
520
+ var out = [];
521
+ var i, entry, h;
522
+
523
+ for (i = 0; i < graphics.length; i++) {
524
+ entry = idcDescribeGraphic(doc, frame, graphics[i], page);
525
+ for (h = 0; h < entry.hints.length; h++) { ctx.warn('Graphic ' + entry.graphic_id + ': ' + entry.hints[h]); }
526
+ out.push({
527
+ graphic_id: entry.graphic_id,
528
+ link_id: entry.link_id,
529
+ file_name: entry.file_name,
530
+ file_path: entry.file_path,
531
+ link_status: entry.link_status,
532
+ effective_ppi: entry.effective_ppi,
533
+ effective_ppi_xy: entry.effective_ppi_xy,
534
+ actual_ppi: entry.actual_ppi,
535
+ actual_ppi_xy: entry.actual_ppi_xy,
536
+ color_space: entry.color_space,
537
+ image_type: entry.image_type,
538
+ is_pdf: entry.is_pdf,
539
+ frame_bounds: entry.frame_bounds,
540
+ graphic_bounds: entry.graphic_bounds,
541
+ fits_frame: idcCoversFrame(entry.frame_bounds, entry.graphic_bounds),
542
+ err: entry.hints.length > 0 ? entry.hints.join('; ') : null
543
+ });
544
+ }
545
+ if (graphics.length === 0) { ctx.warn('Frame ' + args.frame_id + ' holds no graphic.'); }
546
+ return {
547
+ frame_id: Number(frame.id),
548
+ type: IDC.itemType(frame),
549
+ count: out.length,
550
+ graphics: out,
551
+ page_id: page === null ? null : Number(page.id),
552
+ coordinate_space: page === null ? 'spread' : 'page',
553
+ on_parent_page: found.on_parent
554
+ };
555
+ });
556
+
557
+ /* --------------------------------------------------------------- list_links */
558
+
559
+ /* Rahmen und Seite einer Verknuepfung. */
560
+ function idcLinkPlacement(doc, link) {
561
+ var out = { frame_id: null, page_id: null, page_name: '' };
562
+ var graphic = null, frame = null, landing;
563
+ try { graphic = link.parent; } catch (e0) { graphic = null; }
564
+ if (graphic === null || graphic === undefined) { return out; }
565
+ try { frame = graphic.parent; } catch (e1) { frame = null; }
566
+ try {
567
+ if (frame !== null && frame !== undefined && IDC.className(frame) !== 'Spread' && IDC.className(frame) !== 'MasterSpread') {
568
+ out.frame_id = Number(frame.id);
569
+ }
570
+ } catch (e2) { out.frame_id = null; }
571
+ try {
572
+ landing = IDC.landing(frame !== null && frame !== undefined ? frame : graphic);
573
+ out.page_id = landing.page_id;
574
+ out.page_name = landing.page_name;
575
+ } catch (e3) { /* bleibt leer */ }
576
+ return out;
577
+ }
578
+
579
+ function idcDescribeLink(doc, link) {
580
+ var placement = idcLinkPlacement(doc, link);
581
+ var entry = { link_id: null, file_name: null, file_path: null, status: null, page_id: placement.page_id, page_name: placement.page_name, frame_id: placement.frame_id };
582
+ try { entry.link_id = Number(link.id); } catch (e0) { entry.link_id = null; }
583
+ try { entry.file_name = String(link.name); } catch (e1) { entry.file_name = null; }
584
+ try { entry.file_path = String(link.filePath); } catch (e2) { entry.file_path = null; }
585
+ try { entry.status = idcLinkStatusName(link.status, true); } catch (e3) { entry.status = null; }
586
+ return entry;
587
+ }
588
+
589
+ IDC.register('list_links', { needs_document: true, read_only: true, ruler: 'page' },
590
+ function (ctx) {
591
+ var args = ctx.args;
592
+ var doc = ctx.doc;
593
+ var filter = args.path_contains === undefined || args.path_contains === null ? null : String(args.path_contains).toLowerCase();
594
+ var out = [];
595
+ var i, link, entry, haystack;
596
+
597
+ for (i = 0; i < doc.links.length; i++) {
598
+ link = doc.links.item(i);
599
+ entry = idcDescribeLink(doc, link);
600
+ if (filter !== null) {
601
+ haystack = String(entry.file_path === null ? '' : entry.file_path) + '\n' + String(entry.file_name === null ? '' : entry.file_name);
602
+ if (haystack.toLowerCase().indexOf(filter) < 0) { continue; }
603
+ }
604
+ out.push(entry);
605
+ }
606
+ return { links: out, count: out.length, total: Number(doc.links.length), path_contains: filter };
607
+ });
608
+
609
+ /* ------------------------------------------------------------- relink_links */
610
+
611
+ /*
612
+ * Gleicher Dateipfad? macOS: Gross-/Kleinschreibung egal, /private/var und
613
+ * /var sind derselbe Ort (Integrationssuite: temporaerer Ordner, 04.09.2026).
614
+ */
615
+ function idcSamePath(a, b) {
616
+ function norm(p) {
617
+ var t = String(p).toLowerCase();
618
+ if (t.indexOf('/private/') === 0) { t = t.substr(8); }
619
+ return t;
620
+ }
621
+ return norm(a) === norm(b);
622
+ }
623
+
624
+ function idcLinkById(doc, id) {
625
+ var i, link;
626
+ try {
627
+ link = doc.links.itemByID(Number(id));
628
+ if (link && link.isValid === true) { return link; }
629
+ } catch (e) { /* weiter */ }
630
+ for (i = 0; i < doc.links.length; i++) {
631
+ link = doc.links.item(i);
632
+ try { if (Number(link.id) === Number(id)) { return link; } } catch (e2) { /* weiter */ }
633
+ }
634
+ return null;
635
+ }
636
+
637
+ IDC.register('relink_links', { needs_document: true, read_only: false, ruler: 'none' },
638
+ function (ctx) {
639
+ var args = ctx.args;
640
+ var doc = ctx.doc;
641
+ var list = args.relink;
642
+ var results = [];
643
+ var relinked = 0, failed = 0;
644
+ var i, spec, link, file, oldPath, graphic, fresh, newId, statusAfter, entry;
645
+
646
+ if (!IDC.isArray(list) || list.length < 1 || list.length > 500) {
647
+ IDC.fail('invalid_argument', 'relink must be an array of 1 to 500 entries { link_id, new_path }.', null);
648
+ }
649
+ for (i = 0; i < list.length; i++) {
650
+ spec = list[i] || {};
651
+ entry = { link_id: spec.link_id === undefined ? null : spec.link_id, new_link_id: null, ok: false, status_after: null, reason: null, new_path: spec.new_path === undefined ? null : String(spec.new_path) };
652
+ if (typeof spec.link_id !== 'number' || typeof spec.new_path !== 'string' || spec.new_path === '') {
653
+ entry.reason = 'entry needs link_id (number) and new_path (string)';
654
+ failed += 1; results.push(entry); continue;
655
+ }
656
+ link = idcLinkById(doc, spec.link_id);
657
+ if (link === null) {
658
+ entry.reason = 'no link with link_id ' + spec.link_id;
659
+ failed += 1; results.push(entry); continue;
660
+ }
661
+ file = IDC.fileFor(spec.new_path);
662
+ if (!file.exists) {
663
+ entry.reason = 'file not found: ' + spec.new_path;
664
+ failed += 1; results.push(entry); continue;
665
+ }
666
+ if (IDC.fileSize(file) === 0) {
667
+ entry.reason = 'file is empty (0 bytes) – cloud placeholder not downloaded? ' + spec.new_path;
668
+ failed += 1; results.push(entry); continue;
669
+ }
670
+ oldPath = '';
671
+ try { oldPath = String(link.filePath); } catch (e0) { oldPath = ''; }
672
+ if (oldPath !== '' && idcSamePath(oldPath, IDC.fsPath(file))) {
673
+ entry.reason = 'new path equals the current path – use update_links';
674
+ failed += 1; results.push(entry); continue;
675
+ }
676
+ graphic = null;
677
+ try { graphic = link.parent; } catch (e1) { graphic = null; }
678
+ try {
679
+ link.relink(file);
680
+ } catch (e) {
681
+ entry.reason = 'InDesign refused: ' + String(e.message ? e.message : e);
682
+ failed += 1; results.push(entry); continue;
683
+ }
684
+ /* Nach relink() kann ein neues Link-Objekt entstehen – ueber die Grafik frisch lesen. */
685
+ fresh = null;
686
+ try { if (graphic !== null && graphic.isValid !== false) { fresh = graphic.itemLink; } } catch (e2) { fresh = null; }
687
+ if (fresh === null || fresh === undefined) { fresh = link; }
688
+ try {
689
+ if (fresh.isValid !== false) {
690
+ try { if (IDC.enumIs(fresh.status, LinkStatus.LINK_OUT_OF_DATE)) { fresh.update(); } } catch (eu) { /* Status bleibt */ }
691
+ statusAfter = idcLinkStatusName(fresh.status, true);
692
+ newId = Number(fresh.id);
693
+ } else { statusAfter = null; newId = null; }
694
+ } catch (e3) { statusAfter = null; newId = null; }
695
+ entry.ok = true;
696
+ entry.status_after = statusAfter;
697
+ entry.new_link_id = newId;
698
+ if (statusAfter !== null && statusAfter !== 'NORMAL') {
699
+ ctx.warn('Link ' + spec.link_id + ' was relinked but reads ' + statusAfter + '.');
700
+ }
701
+ relinked += 1;
702
+ results.push(entry);
703
+ }
704
+ if (relinked > 0) {
705
+ ctx.changes = { modified_link_ids: (function () {
706
+ var ids = [], seen = {}, k;
707
+ for (k = 0; k < results.length; k++) {
708
+ if (results[k].ok && !IDC.hasOwn(seen, String(results[k].link_id))) { seen[String(results[k].link_id)] = 1; ids.push(results[k].link_id); }
709
+ }
710
+ return ids;
711
+ })() };
712
+ }
713
+ return { results: results, relinked: relinked, failed: failed, considered: results.length };
714
+ });
715
+
716
+ /* ------------------------------------------------------------- update_links */
717
+
718
+ IDC.register('update_links', { needs_document: true, read_only: false, ruler: 'none' },
719
+ function (ctx) {
720
+ var args = ctx.args;
721
+ var doc = ctx.doc;
722
+ var ids = args.link_ids;
723
+ var results = [];
724
+ var updated = 0, failed = 0, nothing = 0;
725
+ var i, link, entry, placement, before, after, candidates = [];
726
+
727
+ if (ids !== undefined && ids !== null) {
728
+ if (!IDC.isArray(ids) || ids.length < 1 || ids.length > 500) {
729
+ IDC.fail('invalid_argument', 'link_ids must be an array of 1 to 500 link ids; leave it out to update every out-of-date link.', null);
730
+ }
731
+ for (i = 0; i < ids.length; i++) { candidates.push({ id: ids[i], link: idcLinkById(doc, ids[i]) }); }
732
+ } else {
733
+ for (i = 0; i < doc.links.length; i++) {
734
+ link = doc.links.item(i);
735
+ try { if (IDC.enumIs(link.status, LinkStatus.LINK_OUT_OF_DATE)) { candidates.push({ id: Number(link.id), link: link }); } } catch (e0) { /* ueberspringen */ }
736
+ }
737
+ }
738
+
739
+ for (i = 0; i < candidates.length; i++) {
740
+ link = candidates[i].link;
741
+ entry = { link_id: candidates[i].id, name: null, page_name: '', status_before: null, status_after: null, ok: false, reason: null };
742
+ if (link === null) {
743
+ entry.reason = 'no link with link_id ' + candidates[i].id;
744
+ failed += 1; results.push(entry); continue;
745
+ }
746
+ try { entry.name = String(link.name); } catch (e1) { entry.name = null; }
747
+ placement = idcLinkPlacement(doc, link);
748
+ entry.page_name = placement.page_name;
749
+ try { before = link.status; } catch (e2) { before = null; }
750
+ entry.status_before = idcLinkStatusName(before, true);
751
+ if (!IDC.enumIs(before, LinkStatus.LINK_OUT_OF_DATE)) {
752
+ entry.ok = true;
753
+ entry.status_after = entry.status_before;
754
+ entry.reason = 'nothing to do';
755
+ nothing += 1; results.push(entry); continue;
756
+ }
757
+ try {
758
+ link.update();
759
+ } catch (e) {
760
+ entry.reason = 'InDesign refused: ' + String(e.message ? e.message : e);
761
+ failed += 1; results.push(entry); continue;
762
+ }
763
+ try { after = link.status; } catch (e3) { after = null; }
764
+ entry.status_after = idcLinkStatusName(after, true);
765
+ entry.ok = true;
766
+ updated += 1;
767
+ results.push(entry);
768
+ }
769
+ if (updated > 0) { ctx.changes = { modified_link_ids: (function () { var out = [], k; for (k = 0; k < results.length; k++) { if (results[k].ok && results[k].reason === null) { out.push(results[k].link_id); } } return out; })() }; }
770
+ return { results: results, updated: updated, failed: failed, nothing_to_do: nothing, considered: results.length };
771
+ });