787 lines
28 KiB
JavaScript
787 lines
28 KiB
JavaScript
// 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();
|