// IDexport.jsx — runs inside Adobe InDesign // Exports the active document as idconvert_export.json // Upload the resulting JSON to IDconvert to generate an editable Word document. // Version: 1.0 #target indesign // ── btoa polyfill (ExtendScript / ES3 does not have btoa built-in) ─────────── var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; function btoa(input) { var output = ''; var i = 0; var len = input.length; while (i < len) { var b0 = input.charCodeAt(i++) & 0xff; var b1 = (i < len) ? (input.charCodeAt(i++) & 0xff) : 0; var b2 = (i < len) ? (input.charCodeAt(i++) & 0xff) : 0; output += _b64chars.charAt(b0 >> 2); output += _b64chars.charAt(((b0 & 3) << 4) | (b1 >> 4)); output += _b64chars.charAt(((b1 & 15) << 2) | (b2 >> 6)); output += _b64chars.charAt(b2 & 63); } var pad = len % 3; if (pad === 1) output = output.slice(0, -2) + '=='; else if (pad === 2) output = output.slice(0, -1) + '='; return output; } // ── JSON polyfill (ExtendScript / ES3 does not have JSON built-in) ──────────── var JSON = (function () { function stringify(val) { if (val === null) return 'null'; if (val === undefined) return 'null'; var t = typeof val; if (t === 'boolean') return val ? 'true' : 'false'; if (t === 'number') { if (!isFinite(val)) return 'null'; return String(val); } if (t === 'string') { return '"' + val .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') + '"'; } if (t === 'object') { if (val instanceof Array) { var items = []; for (var i = 0; i < val.length; i++) { items.push(stringify(val[i])); } return '[' + items.join(',') + ']'; } var pairs = []; for (var k in val) { if (val.hasOwnProperty(k)) { var v = stringify(val[k]); if (v !== undefined) { pairs.push('"' + k + '":' + v); } } } return '{' + pairs.join(',') + '}'; } return 'null'; } return { stringify: stringify }; }()); function exportDocument() { if (app.documents.length === 0) { alert('No document is open. Please open an InDesign document first.'); return; } var doc = app.activeDocument; if (!doc.saved) { alert('Please save your document before exporting.'); return; } // Force all scripting measurements to points for the duration of this export. // page.bounds and geometricBounds respect app.scriptPreferences.measurementUnit, // so without this the values would be in whatever the document ruler is set to // (inches, mm, picas, etc.) and page dimensions in Word would be wrong. var originalUnit = app.scriptPreferences.measurementUnit; app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS; var output; try { output = { version: '1.0', generator: 'IDexport', document: extractDocumentInfo(doc), styles: extractStyles(doc), colors: extractColors(doc), fonts_used: extractFonts(doc), pages: extractPages(doc) }; } finally { // Always restore the original unit even if an error occurs app.scriptPreferences.measurementUnit = originalUnit; } var exportPath = doc.filePath + '/idconvert_export.json'; var file = new File(exportPath); file.encoding = 'UTF-8'; file.open('w'); file.write(JSON.stringify(output)); file.close(); alert('IDexport complete.\nFile saved to:\n' + exportPath + '\n\nUpload idconvert_export.json to IDconvert to generate your Word document.'); } // ── Document info ───────────────────────────────────────────────────────────── function extractDocumentInfo(doc) { var page = doc.pages[0]; var bounds = page.bounds; // [top, left, bottom, right] return { name: doc.name.replace('.indd', ''), page_width_pt: bounds[3] - bounds[1], page_height_pt: bounds[2] - bounds[0], page_count: doc.pages.length, facing_pages: doc.documentPreferences.facingPages }; } // ── Styles ──────────────────────────────────────────────────────────────────── function extractStyles(doc) { var paragraphStyles = []; for (var i = 0; i < doc.paragraphStyles.length; i++) { var s = doc.paragraphStyles[i]; if (s.name === '[No Paragraph Style]' || s.name === '[Basic Paragraph]') continue; try { paragraphStyles.push({ name: s.name, font: safeGet(s, 'appliedFont') ? s.appliedFont.name : null, size_pt: safeGet(s, 'pointSize', null), leading_pt: safeGet(s, 'leading', null), space_before_pt: safeGet(s, 'spaceBefore', 0), space_after_pt: safeGet(s, 'spaceAfter', 0), alignment: alignmentToString(safeGet(s, 'justification', null)), color_hex: resolveColor(safeGet(s, 'fillColor', null)), bold: safeGet(s, 'fontStyle', '').toLowerCase().indexOf('bold') >= 0, italic: safeGet(s, 'fontStyle', '').toLowerCase().indexOf('italic') >= 0 }); } catch(e) {} } var characterStyles = []; for (var j = 0; j < doc.characterStyles.length; j++) { var cs = doc.characterStyles[j]; if (cs.name === '[No Character Style]') continue; try { characterStyles.push({ name: cs.name, font: safeGet(cs, 'appliedFont') ? cs.appliedFont.name : null, bold: safeGet(cs, 'fontStyle', '').toLowerCase().indexOf('bold') >= 0, italic: safeGet(cs, 'fontStyle', '').toLowerCase().indexOf('italic') >= 0, color_hex: resolveColor(safeGet(cs, 'fillColor', null)) }); } catch(e) {} } return { paragraph: paragraphStyles, character: characterStyles }; } // ── Colors ──────────────────────────────────────────────────────────────────── function extractColors(doc) { var colors = {}; for (var i = 0; i < doc.colors.length; i++) { var c = doc.colors[i]; try { colors[c.name] = colorToHex(c); } catch(e) {} } return colors; } // ── Fonts ───────────────────────────────────────────────────────────────────── function extractFonts(doc) { var fonts = []; var seen = {}; for (var i = 0; i < doc.fonts.length; i++) { var f = doc.fonts[i]; if (!seen[f.name]) { seen[f.name] = true; fonts.push({ name: f.name, postscript: safeGet(f, 'postscriptName', f.name) }); } } return fonts; } // ── Pages ───────────────────────────────────────────────────────────────────── function extractPages(doc) { var pages = []; for (var i = 0; i < doc.pages.length; i++) { var page = doc.pages[i]; var bounds = page.bounds; var pageWidthPt = bounds[3] - bounds[1]; var pageHeightPt = bounds[2] - bounds[0]; var layoutInfo = detectPageLayout(page, pageWidthPt); var bgData = hasVisualBackground(page) ? extractPageBackground(doc, page) : null; pages.push({ page_number: page.name, width_pt: pageWidthPt, height_pt: pageHeightPt, layout: layoutInfo.layout, column_gutter_pt: layoutInfo.column_gutter_pt || null, background_b64: bgData, items: extractPageItems(page, doc, pageWidthPt, layoutInfo) }); } return pages; } // Returns true if the page has any filled shapes/rules worth exporting as background. function hasVisualBackground(page) { var allItems = page.allPageItems; for (var i = 0; i < allItems.length; i++) { var item = allItems[i]; var typeName = item.constructor.name; if (typeName === 'GraphicLine') return true; if (typeName === 'Rectangle' || typeName === 'Oval' || typeName === 'Polygon') { var hasImage = item.graphics && item.graphics.length > 0; if (!hasImage) { try { var fn = item.fillColor.name; if (fn !== 'None' && fn !== '[None]') return true; } catch(e) {} } } } return false; } // Hides text and image frames, exports the page as JPEG (showing only background // shapes/rules/fills), then restores visibility. Returns base64-encoded JPEG or null. function extractPageBackground(doc, page) { // Collect items to hide: text frames and image-bearing graphic frames var hidden = []; var allItems = page.allPageItems; for (var i = 0; i < allItems.length; i++) { var item = allItems[i]; var typeName = item.constructor.name; var shouldHide = typeName === 'TextFrame'; if (!shouldHide && (typeName === 'Rectangle' || typeName === 'Oval' || typeName === 'GraphicFrame')) { shouldHide = !!(item.graphics && item.graphics.length > 0); } if (shouldHide) { try { hidden.push({ item: item, wasVisible: item.visible }); item.visible = false; } catch(e) {} } } var bgData = null; try { app.jpegExportPreferences.exportingSpread = false; app.jpegExportPreferences.jpegQuality = JPEGOptionsQuality.HIGH; app.jpegExportPreferences.jpegRenderingStyle = JPEGOptionsFormat.BASELINE_ENCODING; app.jpegExportPreferences.resolution = 150; app.jpegExportPreferences.pageRange = page.name; // InDesign appends a zero-padded page suffix to exported JPEG files // (e.g. idconvert_bg_10001.jpg) even when a single page is requested. // Use a base path and then search for the actual written file. var baseName = 'idconvert_bg_' + page.name; var tempBase = new File(Folder.temp + '/' + baseName + '.jpg'); doc.exportFile(ExportFormat.JPG, tempBase, false); // Find the file InDesign actually wrote — exact path or suffixed variant var actualFile = null; if (tempBase.exists) { actualFile = tempBase; } else { var matches = new Folder(Folder.temp).getFiles(baseName + '*.jpg'); if (matches && matches.length > 0) actualFile = matches[0]; } if (actualFile) { bgData = fileToBase64(actualFile); actualFile.remove(); } } catch(e) {} // Always restore visibility for (var j = 0; j < hidden.length; j++) { try { hidden[j].item.visible = hidden[j].wasVisible; } catch(e) {} } return bgData; } function extractPageItems(page, doc, pageWidthPt, layoutInfo) { var allItems = page.allPageItems; var groupMap = {}; // InDesign group id → [extracted items] var ungrouped = []; // items not inside an InDesign group for (var i = 0; i < allItems.length; i++) { var item = allItems[i]; // Skip Group container nodes — their children appear separately in allPageItems if (item.constructor.name === 'Group') continue; try { var extracted = extractItem(item, page, doc); if (!extracted) continue; var gid = getGroupId(item); if (gid) { if (!groupMap[gid]) groupMap[gid] = []; groupMap[gid].push(extracted); } else { ungrouped.push(extracted); } } catch(e) {} } // Convert InDesign groups → frame_group items var finalItems = []; for (var gid in groupMap) { var gItems = groupMap[gid]; if (gItems.length > 1) { var fg = buildFrameGroup(gItems); if (fg) { finalItems.push(fg); } else { for (var k = 0; k < gItems.length; k++) ungrouped.push(gItems[k]); } } else { ungrouped.push(gItems[0]); } } // Proximity grouping on remaining ungrouped items var proximityResult = detectProximityGroups(ungrouped); for (var p = 0; p < proximityResult.length; p++) finalItems.push(proximityResult[p]); // Sort by layout if (layoutInfo && layoutInfo.layout === 'two_column') { return sortTwoColumn(finalItems, layoutInfo.midpoint_pt, pageWidthPt); } finalItems.sort(function(a, b) { if (Math.abs(a.y_pt - b.y_pt) < 5) return a.x_pt - b.x_pt; return a.y_pt - b.y_pt; }); return finalItems; } // ── Layout detection ────────────────────────────────────────────────────────── function detectPageLayout(page, pageWidthPt) { var allItems = page.allPageItems; var leftCount = 0, rightCount = 0; var leftMaxRight = 0, rightMinLeft = pageWidthPt; var midpoint = pageWidthPt / 2; for (var i = 0; i < allItems.length; i++) { var item = allItems[i]; if (item.constructor.name !== 'TextFrame') continue; try { if (hasAutoPageNumber(item)) continue; } catch(e) {} var bounds = item.geometricBounds; var pageBounds = page.bounds; var x = bounds[1] - pageBounds[1]; var width = bounds[3] - bounds[1]; // Ignore full-width frames — they span both columns if (width > pageWidthPt * 0.75) continue; var xCenter = x + width / 2; if (xCenter < midpoint) { leftCount++; leftMaxRight = Math.max(leftMaxRight, x + width); } else { rightCount++; rightMinLeft = Math.min(rightMinLeft, x); } } if (leftCount > 0 && rightCount > 0) { var gutter = rightMinLeft - leftMaxRight; if (gutter >= 0) { return { layout: 'two_column', column_gutter_pt: gutter, midpoint_pt: midpoint }; } } return { layout: 'single', column_gutter_pt: null, midpoint_pt: midpoint }; } // ── Frame grouping ──────────────────────────────────────────────────────────── // Returns the InDesign Group id if item is a direct child of a Group, else null. function getGroupId(item) { try { var par = item.parent; if (par && par.constructor.name === 'Group') return par.id.toString(); } catch(e) {} return null; } // Builds a frame_group from 2+ co-located items, sorted left-to-right. function buildFrameGroup(items) { if (!items || items.length < 2) return null; items.sort(function(a, b) { return a.x_pt - b.x_pt; }); var minX = items[0].x_pt; var minY = items[0].y_pt; var maxBottom = items[0].y_pt + (items[0].height_pt || 0); var totalWidth = 0; var columns = []; for (var i = 0; i < items.length; i++) { var it = items[i]; minY = Math.min(minY, it.y_pt); maxBottom = Math.max(maxBottom, it.y_pt + (it.height_pt || 0)); totalWidth += (it.width_pt || 100); columns.push({ width_pt: it.width_pt || 100, items: [it] }); } return { type: 'frame_group', id: 'fg_' + items[0].id, x_pt: minX, y_pt: minY, width_pt: totalWidth, height_pt: maxBottom - minY, columns: columns }; } // Returns true if two items should be proximity-grouped into a side-by-side table. function areProximityGroup(a, b) { if (!a.width_pt || !b.width_pt || !a.height_pt || !b.height_pt) return false; // Must be horizontally distinct (side-by-side, not stacked) var aCx = a.x_pt + a.width_pt / 2; var bCx = b.x_pt + b.width_pt / 2; var minW = Math.min(a.width_pt, b.width_pt); if (Math.abs(aCx - bCx) < minW * 0.5) return false; // Y ranges must overlap by ≥ 60% of the smaller item's height var overlap = Math.min(a.y_pt + a.height_pt, b.y_pt + b.height_pt) - Math.max(a.y_pt, b.y_pt); if (overlap < Math.min(a.height_pt, b.height_pt) * 0.6) return false; // Horizontal gap must be ≤ 30pt var gap = Math.min( Math.abs((a.x_pt + a.width_pt) - b.x_pt), Math.abs((b.x_pt + b.width_pt) - a.x_pt) ); if (gap > 30) return false; return true; } // Groups ungrouped items by spatial proximity using union-find. function detectProximityGroups(items) { var n = items.length; if (n === 0) return []; var parent = []; for (var i = 0; i < n; i++) parent[i] = i; function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } function unite(x, y) { parent[find(x)] = find(y); } for (var i = 0; i < n; i++) { for (var j = i + 1; j < n; j++) { if (areProximityGroup(items[i], items[j])) unite(i, j); } } var groupsObj = {}; for (var i = 0; i < n; i++) { var root = find(i); if (!groupsObj[root]) groupsObj[root] = []; groupsObj[root].push(items[i]); } var result = []; for (var r in groupsObj) { var g = groupsObj[r]; if (g.length > 1) { var fg = buildFrameGroup(g); if (fg) { result.push(fg); } else { for (var k = 0; k < g.length; k++) result.push(g[k]); } } else { result.push(g[0]); } } return result; } // Sorts items for two-column flow: left column first, then right column. // A column_break marker is inserted at the boundary for the Word builder to use. function sortTwoColumn(items, midpointPt, pageWidthPt) { var leftCol = [], rightCol = []; for (var i = 0; i < items.length; i++) { var item = items[i]; var xCenter = item.x_pt + (item.width_pt || 0) / 2; var fullWidth = (item.width_pt || 0) > pageWidthPt * 0.75; if (fullWidth || xCenter <= midpointPt) { leftCol.push(item); } else { rightCol.push(item); } } leftCol.sort(function(a, b) { return a.y_pt - b.y_pt; }); rightCol.sort(function(a, b) { return a.y_pt - b.y_pt; }); if (rightCol.length === 0) return leftCol; // Insert a column_break marker so the Word builder knows where to break var marker = { type: 'column_break', id: 'col_break', x_pt: 0, y_pt: 0, width_pt: 0, height_pt: 0 }; return leftCol.concat([marker], rightCol); } function extractItem(item, page, doc) { var bounds = item.geometricBounds; // [top, left, bottom, right] in spread coords var pageBounds = page.bounds; // [top, left, bottom, right] of page var x = bounds[1] - pageBounds[1]; var y = bounds[0] - pageBounds[0]; var width = bounds[3] - bounds[1]; var height = bounds[2] - bounds[0]; var typeName = item.constructor.name; if (typeName === 'TextFrame') { return extractTextFrame(item, x, y, width, height); } if (typeName === 'Rectangle' || typeName === 'Oval' || typeName === 'GraphicFrame') { if (item.graphics && item.graphics.length > 0) { return extractImageFrame(item, x, y, width, height); } return extractShapeFrame(item, x, y, width, height); } return null; } // ── Text frames ─────────────────────────────────────────────────────────────── function extractTextFrame(frame, x, y, width, height) { if (hasAutoPageNumber(frame)) { return extractPageNumber(frame, x, y); } var paragraphs = []; try { // Use frame.texts[0].paragraphs — only the text visible in THIS frame, // not frame.parentStory.paragraphs which returns the entire threaded story. var frameText = frame.texts[0]; for (var i = 0; i < frameText.paragraphs.length; i++) { var para = frameText.paragraphs[i]; paragraphs.push(extractParagraph(para)); } } catch(e) {} var threadId = ''; var threadPos = 1; try { threadId = frame.parentStory.id.toString(); var prev = frame.previousTextFrame; while (prev !== null) { threadPos++; prev = prev.previousTextFrame; } } catch(e) {} return { type: 'text_frame', id: frame.id.toString(), thread_id: threadId, thread_position: threadPos, x_pt: x, y_pt: y, width_pt: width, height_pt: height, column_count: safeGet(frame.textFramePreferences, 'textColumnCount', 1), paragraphs: paragraphs }; } function extractParagraph(para) { var styleName = '[No Paragraph Style]'; try { styleName = para.appliedParagraphStyle.name; } catch(e) {} var runs = []; var characters = para.characters; for (var i = 0; i < characters.length; i++) { var run = buildRun(characters[i]); if (runs.length > 0 && runsMatch(runs[runs.length - 1], run)) { runs[runs.length - 1].text += run.text; } else { runs.push(run); } } return { style: styleName, text: para.contents, runs: runs }; } function buildRun(ch) { var styleName = null; try { if (ch.appliedCharacterStyle.name !== '[No Character Style]') { styleName = ch.appliedCharacterStyle.name; } } catch(e) {} var fontStyle = safeGet(ch, 'fontStyle', '').toLowerCase(); return { text: ch.contents, style: styleName, bold: fontStyle.indexOf('bold') >= 0, italic: fontStyle.indexOf('italic') >= 0, color_hex: resolveColor(safeGet(ch, 'fillColor', null)), font: safeGet(ch, 'appliedFont') ? ch.appliedFont.name : null, size_pt: safeGet(ch, 'pointSize', null) }; } function runsMatch(a, b) { return a.style === b.style && a.bold === b.bold && a.italic === b.italic && a.color_hex === b.color_hex && a.font === b.font && a.size_pt === b.size_pt; } // ── Images ──────────────────────────────────────────────────────────────────── function extractImageFrame(frame, x, y, width, height) { var imageData = null; var imageName = 'image'; try { var graphic = frame.graphics[0]; imageName = graphic.itemLink.name; var tempFile = new File(Folder.temp + '/idconvert_temp.jpg'); frame.exportFile(ExportFormat.JPG, tempFile, false); imageData = fileToBase64(tempFile); tempFile.remove(); } catch(e) {} return { type: 'image_frame', id: frame.id.toString(), x_pt: x, y_pt: y, width_pt: width, height_pt: height, image_name: imageName, image_data_b64: imageData, fit_mode: 'fill_proportionally' }; } // ── Shapes ──────────────────────────────────────────────────────────────────── function extractShapeFrame(frame, x, y, width, height) { var fillColor = null; try { if (frame.fillColor.name !== 'None' && frame.fillColor.name !== '[None]') { fillColor = colorToHex(frame.fillColor); } } catch(e) {} if (!fillColor) return null; return { type: 'shape', id: frame.id.toString(), x_pt: x, y_pt: y, width_pt: width, height_pt: height, fill_hex: fillColor }; } // ── Page numbers ────────────────────────────────────────────────────────────── function extractPageNumber(frame, x, y) { var run = null; try { run = frame.parentStory.characters[0]; } catch(e) {} return { type: 'page_number', id: frame.id.toString(), x_pt: x, y_pt: y, font: run ? (run.appliedFont ? run.appliedFont.name : null) : null, size_pt: run ? safeGet(run, 'pointSize', 9) : 9, color_hex: run ? resolveColor(safeGet(run, 'fillColor', null)) : '000000', alignment: run ? alignmentToString(safeGet(run, 'justification', null)) : 'center' }; } function hasAutoPageNumber(frame) { try { var characters = frame.parentStory.texts[0].characters; for (var i = 0; i < characters.length; i++) { if (characters[i].contents === SpecialCharacters.AUTO_PAGE_NUMBER) return true; } } catch(e) {} return false; } // ── Utilities ───────────────────────────────────────────────────────────────── function resolveColor(colorObj) { if (!colorObj) return null; try { if (colorObj.name === 'None' || colorObj.name === '[None]') return null; return colorToHex(colorObj); } catch(e) { return null; } } function colorToHex(colorObj) { try { var vals = colorObj.colorValue; if (colorObj.space === ColorSpace.CMYK) { var c = vals[0]/100, m = vals[1]/100, y = vals[2]/100, k = vals[3]/100; return toHex(Math.round(255*(1-c)*(1-k))) + toHex(Math.round(255*(1-m)*(1-k))) + toHex(Math.round(255*(1-y)*(1-k))); } if (colorObj.space === ColorSpace.RGB) { return toHex(vals[0]) + toHex(vals[1]) + toHex(vals[2]); } } catch(e) {} return '000000'; } function toHex(n) { var h = Math.max(0, Math.min(255, Math.round(n))).toString(16); return h.length === 1 ? '0' + h : h; } function alignmentToString(justification) { if (!justification) return 'left'; var map = { 1: 'left', 2: 'center', 3: 'right', 4: 'justify', 1514227313: 'left', 1514731619: 'center', 1514731618: 'right', 1514599026: 'justify' }; return map[justification] || 'left'; } function fileToBase64(file) { file.open('r'); file.encoding = 'BINARY'; var content = file.read(); file.close(); return btoa(content); } function safeGet(obj, prop, fallback) { try { var val = obj[prop]; return (val !== undefined && val !== null) ? val : fallback; } catch(e) { return (fallback !== undefined) ? fallback : null; } } // ── Run ─────────────────────────────────────────────────────────────────────── exportDocument();