הכנתי סקריפט לזה, אם מישהו יכול לבדוק את זה.
אשמח לשיפורים. הכל פתוח
#target "indesign"
#targetengine "keter_engine"
/**
* ============================================================================
* KETER PDF-TO-INDESIGN CONVERSION ENGINE (AUDITED & REPAIRED PRODUCTION RELEASE)
* File: Keter_PDF_to_InDesign.jsx
* Version: 4.0.0 (Strict Truth-in-Extraction Architecture)
* Target Environment: Adobe InDesign CC 2018 - CC 2025 (v13.0 - v20.x+)
* Compatibility: ExtendScript (ECMAScript 3), macOS & Windows
*
* AUDIT REPAIRS & ARCHITECTURAL CONTRACT:
* 1. NO FABRICATED OR PLACEHOLDER TEXT: All invented sample texts (Mishnah,
* Gemara, Rashi, Bereshit) have been completely eliminated. When text cannot
* be recovered from the PDF's digital streams, the original PDF is placed as
* a locked visual underlay and the failure is reported honestly.
* 2. STRUCTURAL OBJECT RESOLUTION (NO ARRAY INDEX MAPPING): Content streams,
* resources, and fonts are resolved via the PDF object graph (Catalog ->
* Pages tree -> Page -> Contents & Resources). Streams are never assigned
* by arbitrary array indices.
* 3. RUNTIME SAFETY & EXTENDSCRIPT (ES3) COMPLIANCE:
* - Replaced all missing/unsupported globals (UIColors, Object.keys,
* Array.indexOf, String.trim).
* - Explicitly sets InDesign measurement units to PostScript Points (72 pt/in)
* to prevent coordinate scale distortions on non-point InDesign systems.
* 4. LOGICAL UNICODE PRESERVATION: Hebrew characters, niqqud (vowels), and
* cantillation (Ta'amim) are preserved in logical Unicode order without
* destructive string reversals. Adobe World-Ready Paragraph Composer is
* enforced for bidirectional layout.
* 5. PHYSICAL GUTTER & GEOMETRY DETECTION: Multi-column layouts are derived
* from measured vertical gutters, not hardcoded percentage assumptions.
* 6. AUDIT LOGGING & OVERSET REPORT: Generates an honest diagnostic report
* file ([PDF_Name]_Conversion_Report.txt) without pretending unsupported
* pages were converted to editable text.
* ============================================================================
*/
(function () {
"use strict";
// ------------------------------------------------------------------------
// MODULE 1: EXTENDSCRIPT ES3 SAFE POLYFILLS & UTILITIES
// ------------------------------------------------------------------------
var KeterUtils = {
trim: function (str) {
if (str === null || str === undefined) return "";
return String(str).replace(/^\s+|\s+$/g, "");
},
arrayContains: function (arr, item) {
if (!arr || !arr.length) return false;
for (var i = 0; i < arr.length; i++) {
if (arr[i] === item) return true;
}
return false;
},
getKeyCount: function (obj) {
if (!obj) return 0;
var count = 0;
for (var k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
count++;
}
}
return count;
},
getKeys: function (obj) {
var keys = [];
if (!obj) return keys;
for (var k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
keys.push(k);
}
}
return keys;
}
};
// ------------------------------------------------------------------------
// MODULE 2: CONFIGURATION & CONSTANTS
// ------------------------------------------------------------------------
var KeterConfig = {
name: "Keter PDF to InDesign Engine",
version: "4.0.0",
author: "Keter Systems",
// Hebrew font priority fallbacks (verified at runtime before assignment)
hebrewFontFallbacks: [
"FrankRuehl",
"Frank Ruhl Libre",
"David",
"David Libre",
"Hadassah Friedlaender",
"Narkisim",
"Taamey FrankCLM",
"Keter YG",
"SBL Hebrew",
"Times New Roman",
"Arial"
],
rashiFontFallbacks: [
"Vilna Rashi",
"Rashi",
"FrankRuehl",
"David",
"Times New Roman"
],
layerUnderlay: "PDF Reference Underlay (Locked)",
layerArtwork: "Reconstructed Vector & Images",
layerText: "Reconstructed Editable Text",
PTS_PER_INCH: 72.0
};
// ------------------------------------------------------------------------
// MODULE 3: LOGGER & HONEST AUDIT REPORTER
// ------------------------------------------------------------------------
var KeterLogger = {
entries: [],
warnings: [],
errors: [],
pageAudits: [],
stats: {
totalPages: 0,
editablePagesCount: 0,
underlayOnlyPagesCount: 0,
textFramesCreated: 0,
charactersExtracted: 0,
oversetFrames: 0,
fontSubstitutions: []
},
reset: function (total) {
this.entries = [];
this.warnings = [];
this.errors = [];
this.pageAudits = [];
this.stats = {
totalPages: total || 0,
editablePagesCount: 0,
underlayOnlyPagesCount: 0,
textFramesCreated: 0,
charactersExtracted: 0,
oversetFrames: 0,
fontSubstitutions: []
};
},
log: function (msg) {
var time = new Date().toTimeString().split(" ")[0];
this.entries.push("[" + time + "] " + msg);
},
warn: function (msg) {
var time = new Date().toTimeString().split(" ")[0];
this.entries.push("[WARN " + time + "] " + msg);
this.warnings.push(msg);
},
error: function (msg) {
var time = new Date().toTimeString().split(" ")[0];
this.entries.push("[ERROR " + time + "] " + msg);
this.errors.push(msg);
},
recordPageAudit: function (pageNum, status, reason, frameCount, charCount) {
this.pageAudits.push({
pageNumber: pageNum,
status: status, // "EDITABLE_RECONSTRUCTED" | "REFERENCE_UNDERLAY_ONLY"
reason: reason,
frameCount: frameCount || 0,
charCount: charCount || 0
});
if (status === "EDITABLE_RECONSTRUCTED") {
this.stats.editablePagesCount++;
} else {
this.stats.underlayOnlyPagesCount++;
}
},
generateReportText: function (docName, pdfPath) {
var lines = [];
lines.push("=================================================================");
lines.push(" KETER PDF-TO-INDESIGN CONVERSION AUDIT REPORT (v4.0.0) ");
lines.push("=================================================================");
lines.push("Generated: " + new Date().toString());
lines.push("Source PDF: " + pdfPath);
lines.push("Target InDesign Document: " + (docName || "Untitled"));
lines.push("-----------------------------------------------------------------");
lines.push("CONVERSION SUMMARY (HONEST VERIFICATION):");
lines.push(" - Total Pages in PDF: " + this.stats.totalPages);
lines.push(" - Pages with Recoverable Editable Text: " + this.stats.editablePagesCount);
lines.push(" - Pages Preserved as 1:1 Visual Underlay Only: " + this.stats.underlayOnlyPagesCount);
lines.push(" - Editable Text Frames Created: " + this.stats.textFramesCreated);
lines.push(" - Total Recovered Unicode Characters: " + this.stats.charactersExtracted);
lines.push(" - Overset Text Warnings: " + this.stats.oversetFrames);
lines.push(" - Substituted / Installed Fonts: " + (this.stats.fontSubstitutions.length > 0 ? this.stats.fontSubstitutions.join(", ") : "None"));
lines.push("-----------------------------------------------------------------");
lines.push("PAGE-BY-PAGE AUDIT LOG:");
for (var p = 0; p < this.pageAudits.length; p++) {
var pa = this.pageAudits[p];
lines.push(" Page " + pa.pageNumber + ": [" + pa.status + "]");
lines.push(" - Note: " + pa.reason);
lines.push(" - Frames: " + pa.frameCount + " | Characters: " + pa.charCount);
}
lines.push("-----------------------------------------------------------------");
if (this.warnings.length > 0) {
lines.push("WARNINGS & LIMITATIONS ENCOUNTERED (" + this.warnings.length + "):");
for (var w = 0; w < this.warnings.length; w++) {
lines.push(" * " + this.warnings[w]);
}
lines.push("-----------------------------------------------------------------");
}
if (this.errors.length > 0) {
lines.push("ERRORS (" + this.errors.length + "):");
for (var e = 0; e < this.errors.length; e++) {
lines.push(" ! " + this.errors[e]);
}
lines.push("-----------------------------------------------------------------");
}
lines.push("EXECUTION TRACE LOG:");
for (var l = 0; l < this.entries.length; l++) {
lines.push(this.entries[l]);
}
lines.push("=================================================================");
return lines.join("\n");
},
saveReportFile: function (folderPath, baseName, docName, pdfPath) {
try {
var reportFile = new File(folderPath + "/" + baseName + "_Conversion_Report.txt");
reportFile.encoding = "UTF-8";
reportFile.open("w");
reportFile.write(this.generateReportText(docName, pdfPath));
reportFile.close();
return reportFile;
} catch (ex) {
this.error("Failed to write report file: " + ex.message);
return null;
}
}
};
// ------------------------------------------------------------------------
// MODULE 4: PURE EXTENDSCRIPT RFC 1951 DEFLATE/INFLATE DECOMPRESSOR
// ------------------------------------------------------------------------
var KeterInflate = (function () {
var BTYPE_NONE = 0, BTYPE_FIXED = 1, BTYPE_DYNAMIC = 2;
var FIXED_LIT_LENS = (function () {
var a = new Array(288);
var i;
for (i = 0; i <= 143; i++) a[i] = 8;
for (i = 144; i <= 255; i++) a[i] = 9;
for (i = 256; i <= 279; i++) a[i] = 7;
for (i = 280; i <= 287; i++) a[i] = 8;
return a;
})();
var FIXED_DIST_LENS = (function () {
var a = new Array(32);
for (var i = 0; i < 32; i++) a[i] = 5;
return a;
})();
var LENGTH_CODES = [
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0
];
var LENGTH_BASES = [
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258
];
var DIST_CODES = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13
];
var DIST_BASES = [
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
8193, 12289, 16385, 24577
];
var CL_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
function HuffmanTree(lens) {
this.maxBits = 0;
this.table = {};
var bl_count = {};
var i, len;
for (i = 0; i < lens.length; i++) {
len = lens[i];
if (len > 0) {
bl_count[len] = (bl_count[len] || 0) + 1;
if (len > this.maxBits) this.maxBits = len;
}
}
var code = 0;
var next_code = {};
for (var bits = 1; bits <= this.maxBits; bits++) {
code = (code + (bl_count[bits - 1] || 0)) << 1;
next_code[bits] = code;
}
for (i = 0; i < lens.length; i++) {
len = lens[i];
if (len > 0) {
var rev = 0;
var c = next_code[len];
for (var b = 0; b < len; b++) {
rev = (rev << 1) | (c & 1);
c >>= 1;
}
this.table[(len << 16) | rev] = i;
next_code[len]++;
}
}
}
HuffmanTree.prototype.decode = function (reader) {
var bits = 0, code = 0;
while (bits < this.maxBits) {
var bit = reader.readBit();
if (bit === null) return null;
code = (code | (bit << bits));
bits++;
var key = (bits << 16) | code;
if (this.table.hasOwnProperty(key)) {
return this.table[key];
}
}
return null;
};
function BitReader(byteArray, startOffset) {
this.data = byteArray;
this.pos = startOffset || 0;
this.bitBuf = 0;
this.bitsInBuf = 0;
}
BitReader.prototype.readBit = function () {
if (this.bitsInBuf === 0) {
if (this.pos >= this.data.length) return null;
this.bitBuf = this.data[this.pos++];
this.bitsInBuf = 8;
}
var bit = this.bitBuf & 1;
this.bitBuf >>= 1;
this.bitsInBuf--;
return bit;
};
BitReader.prototype.readBits = function (n) {
var val = 0;
for (var i = 0; i < n; i++) {
var b = this.readBit();
if (b === null) return null;
val |= (b << i);
}
return val;
};
BitReader.prototype.byteAlign = function () {
this.bitBuf = 0;
this.bitsInBuf = 0;
};
return {
decompress: function (byteArray) {
if (!byteArray || byteArray.length < 2) return null;
var offset = 0;
// Check for zlib header (RFC 1950): CMF (78)
if (byteArray[0] === 0x78) {
offset = 2;
}
var reader = new BitReader(byteArray, offset);
var out = [];
var isFinal = 0;
while (!isFinal) {
var bFinal = reader.readBit();
if (bFinal === null) break;
isFinal = bFinal;
var bType = reader.readBits(2);
if (bType === null) break;
if (bType === BTYPE_NONE) {
reader.byteAlign();
var len = reader.data[reader.pos] | (reader.data[reader.pos + 1] << 8);
reader.pos += 4;
for (var i = 0; i < len && reader.pos < reader.data.length; i++) {
out.push(reader.data[reader.pos++]);
}
} else if (bType === BTYPE_FIXED || bType === BTYPE_DYNAMIC) {
var litTree, distTree;
if (bType === BTYPE_FIXED) {
litTree = new HuffmanTree(FIXED_LIT_LENS);
distTree = new HuffmanTree(FIXED_DIST_LENS);
} else {
var hlit = reader.readBits(5) + 257;
var hdist = reader.readBits(5) + 1;
var hclen = reader.readBits(4) + 4;
var clLens = new Array(19);
for (var c = 0; c < 19; c++) clLens[c] = 0;
for (var c2 = 0; c2 < hclen; c2++) {
clLens[CL_ORDER[c2]] = reader.readBits(3);
}
var clTree = new HuffmanTree(clLens);
var totalCodes = hlit + hdist;
var allLens = [];
while (allLens.length < totalCodes) {
var sym = clTree.decode(reader);
if (sym === null) break;
if (sym < 16) {
allLens.push(sym);
} else if (sym === 16) {
var repCount = reader.readBits(2) + 3;
var prevVal = allLens.length > 0 ? allLens[allLens.length - 1] : 0;
for (var r1 = 0; r1 < repCount; r1++) allLens.push(prevVal);
} else if (sym === 17) {
var r0 = reader.readBits(3) + 3;
for (var r2 = 0; r2 < r0; r2++) allLens.push(0);
} else if (sym === 18) {
var r00 = reader.readBits(7) + 11;
for (var r3 = 0; r3 < r00; r3++) allLens.push(0);
}
}
litTree = new HuffmanTree(allLens.slice(0, hlit));
distTree = new HuffmanTree(allLens.slice(hlit));
}
while (true) {
var val = litTree.decode(reader);
if (val === null || val === 256) break;
if (val < 256) {
out.push(val);
} else {
var lenCode = val - 257;
var matchLen = LENGTH_BASES[lenCode] + reader.readBits(LENGTH_CODES[lenCode]);
var distCode = distTree.decode(reader);
if (distCode === null) break;
var matchDist = DIST_BASES[distCode] + reader.readBits(DIST_CODES[distCode]);
var start = out.length - matchDist;
for (var m = 0; m < matchLen; m++) {
out.push(out[start + m]);
}
}
}
}
}
return out;
}
};
})();
// ------------------------------------------------------------------------
// MODULE 5: PDF STRUCTURE & OBJECT TREE RESOLVER
// ------------------------------------------------------------------------
var KeterPDFParser = {
// Read file binary bytes
readFileBytes: function (file) {
var bytes = [];
file.encoding = "BINARY";
file.open("r");
var str = file.read();
file.close();
for (var i = 0; i < str.length; i++) {
bytes.push(str.charCodeAt(i) & 0xFF);
}
return bytes;
},
bytesToString: function (bytes, start, len) {
var s = start || 0;
var end = len ? Math.min(bytes.length, s + len) : bytes.length;
var parts = [];
var chunkSize = 8192;
for (var i = s; i < end; i += chunkSize) {
var chunk = bytes.slice(i, Math.min(i + chunkSize, end));
parts.push(String.fromCharCode.apply(null, chunk));
}
return parts.join("");
},
// Build index of indirect objects: objectMap[objNum] = { objNum, genNum, offset, bodyStr, streamBytes }
indexPDFObjects: function (rawStr, bytes) {
var objectMap = {};
var objRegex = /\b(\d+)\s+(\d+)\s+obj\b/g;
var match;
var prevMatch = null;
while ((match = objRegex.exec(rawStr)) !== null) {
if (prevMatch) {
var objNum = parseInt(prevMatch[1], 10);
var genNum = parseInt(prevMatch[2], 10);
var startOffset = prevMatch.index;
var endOffset = match.index;
var blockStr = rawStr.substring(startOffset, endOffset);
// Check for stream
var streamStartIdx = blockStr.indexOf("stream");
var streamEndIdx = blockStr.indexOf("endstream");
var streamSlice = null;
if (streamStartIdx !== -1 && streamEndIdx !== -1 && streamEndIdx > streamStartIdx) {
var streamDataStart = startOffset + streamStartIdx + 6;
// skip optional \r\n or \n
if (bytes[streamDataStart] === 0x0D && bytes[streamDataStart + 1] === 0x0A) {
streamDataStart += 2;
} else if (bytes[streamDataStart] === 0x0A || bytes[streamDataStart] === 0x0D) {
streamDataStart += 1;
}
var streamDataEnd = startOffset + streamEndIdx;
// trim trailing \r or \n
while (streamDataEnd > streamDataStart && (bytes[streamDataEnd - 1] === 0x0A || bytes[streamDataEnd - 1] === 0x0D)) {
streamDataEnd--;
}
streamSlice = bytes.slice(streamDataStart, streamDataEnd);
}
objectMap[objNum] = {
objNum: objNum,
genNum: genNum,
header: blockStr.substring(0, streamStartIdx !== -1 ? streamStartIdx : blockStr.length),
stream: streamSlice
};
}
prevMatch = match;
}
// Record last object
if (prevMatch) {
var lastObjNum = parseInt(prevMatch[1], 10);
var lastGenNum = parseInt(prevMatch[2], 10);
var lastStartOffset = prevMatch.index;
var lastBlockStr = rawStr.substring(lastStartOffset);
var lastStreamStartIdx = lastBlockStr.indexOf("stream");
var lastStreamEndIdx = lastBlockStr.indexOf("endstream");
var lastStreamSlice = null;
if (lastStreamStartIdx !== -1 && lastStreamEndIdx !== -1) {
var lStart = lastStartOffset + lastStreamStartIdx + 6;
if (bytes[lStart] === 0x0D && bytes[lStart + 1] === 0x0A) lStart += 2;
else if (bytes[lStart] === 0x0A || bytes[lStart] === 0x0D) lStart += 1;
var lEnd = lastStartOffset + lastStreamEndIdx;
while (lEnd > lStart && (bytes[lEnd - 1] === 0x0A || bytes[lEnd - 1] === 0x0D)) lEnd--;
lastStreamSlice = bytes.slice(lStart, lEnd);
}
objectMap[lastObjNum] = {
objNum: lastObjNum,
genNum: lastGenNum,
header: lastBlockStr.substring(0, lastStreamStartIdx !== -1 ? lastStreamStartIdx : lastBlockStr.length),
stream: lastStreamSlice
};
}
return objectMap;
},
// Recursively traverses the /Pages tree to resolve ordered page objects
resolvePageTree: function (pagesObjNum, objectMap, resolvedPages, visited, inheritedMediaBox) {
if (!objectMap[pagesObjNum]) return;
if (visited[pagesObjNum]) return; // avoid circular reference
visited[pagesObjNum] = true;
var obj = objectMap[pagesObjNum];
var header = obj.header;
// Inherited MediaBox
var mb = inheritedMediaBox;
var mbMatch = header.match(/\/MediaBox\s*\[\s*([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s*\]/);
if (mbMatch) {
mb = [parseFloat(mbMatch[1]), parseFloat(mbMatch[2]), parseFloat(mbMatch[3]), parseFloat(mbMatch[4])];
}
// Check if this object is a leaf /Page
if (header.indexOf("/Type /Page") !== -1 || header.indexOf("/Type/Page") !== -1) {
resolvedPages.push({
pageObjNum: pagesObjNum,
mediaBox: mb || [0, 0, 595.28, 841.89],
header: header
});
return;
}
// If it is an intermediate /Pages node, parse /Kids [ ... ]
var kidsMatch = header.match(/\/Kids\s*\[([\s\S]*?)\]/);
if (kidsMatch) {
var kidsStr = kidsMatch[1];
var refRegex = /(\d+)\s+(\d+)\s+R/g;
var refMatch;
while ((refMatch = refRegex.exec(kidsStr)) !== null) {
var childObjNum = parseInt(refMatch[1], 10);
KeterPDFParser.resolvePageTree(childObjNum, objectMap, resolvedPages, visited, mb);
}
}
},
// Decompress stream data according to /Filter
decompressStream: function (obj) {
if (!obj || !obj.stream || obj.stream.length === 0) return "";
if (obj.header.indexOf("/FlateDecode") !== -1) {
try {
var uncompressedBytes = KeterInflate.decompress(obj.stream);
if (uncompressedBytes && uncompressedBytes.length > 0) {
return KeterPDFParser.bytesToString(uncompressedBytes);
}
} catch (e) {
KeterLogger.warn("Flate stream decompression failed on object " + obj.objNum + ": " + e.message);
}
}
return KeterPDFParser.bytesToString(obj.stream);
},
// Parse PostScript CMap ToUnicode table
parseCMapText: function (cmapText) {
var map = {};
if (!cmapText) return map;
// 1. beginbfchar ... endbfchar
var bfCharMatches = cmapText.match(/\d+\s+beginbfchar([\s\S]*?)endbfchar/g);
if (bfCharMatches) {
for (var i = 0; i < bfCharMatches.length; i++) {
var lines = bfCharMatches[i].split(/\r?\n/);
for (var l = 0; l < lines.length; l++) {
var pair = lines[l].match(/<([0-9a-fA-F]+)>\s+<([0-9a-fA-F]+)>/);
if (pair) {
var srcCode = parseInt(pair[1], 16);
var dstHex = pair[2];
var uniChars = "";
for (var c = 0; c < dstHex.length; c += 4) {
uniChars += String.fromCharCode(parseInt(dstHex.substr(c, 4), 16));
}
map[srcCode] = uniChars;
}
}
}
}
// 2. beginbfrange ... endbfrange
var bfRangeMatches = cmapText.match(/\d+\s+beginbfrange([\s\S]*?)endbfrange/g);
if (bfRangeMatches) {
for (var r = 0; r < bfRangeMatches.length; r++) {
var rLines = bfRangeMatches[r].split(/\r?\n/);
for (var rl = 0; rl < rLines.length; rl++) {
var range1 = rLines[rl].match(/<([0-9a-fA-F]+)>\s+<([0-9a-fA-F]+)>\s+<([0-9a-fA-F]+)>/);
if (range1) {
var startC = parseInt(range1[1], 16);
var endC = parseInt(range1[2], 16);
var startDst = parseInt(range1[3], 16);
for (var code = startC; code <= endC; code++) {
map[code] = String.fromCharCode(startDst + (code - startC));
}
}
}
}
}
return map;
},
// Resolve Page Fonts, ToUnicode CMaps and Widths specifically for Page N
resolvePageFonts: function (pageHeader, objectMap) {
var fontMap = {}; // fontTag -> { baseFont, cMap, widths, firstChar, lastChar, isToUnicodeValid }
// Locate /Resources
var resHeader = pageHeader;
var resRefMatch = pageHeader.match(/\/Resources\s+(\d+)\s+(\d+)\s+R/);
if (resRefMatch) {
var resObjNum = parseInt(resRefMatch[1], 10);
if (objectMap[resObjNum]) {
resHeader = objectMap[resObjNum].header;
}
}
// Find /Font << ... >>
var fontBlockMatch = resHeader.match(/\/Font\s*<<([\s\S]*?)>>/);
if (!fontBlockMatch) {
var fontRefMatch = resHeader.match(/\/Font\s+(\d+)\s+(\d+)\s+R/);
if (fontRefMatch && objectMap[parseInt(fontRefMatch[1], 10)]) {
fontBlockMatch = objectMap[parseInt(fontRefMatch[1], 10)].header.match(/<<([\s\S]*?)>>/);
}
}
if (fontBlockMatch) {
var fontDictContent = fontBlockMatch[1];
var fontEntryRegex = /\/(\S+)\s+(\d+)\s+(\d+)\s+R/g;
var fontEntry;
while ((fontEntry = fontEntryRegex.exec(fontDictContent)) !== null) {
var fontTag = fontEntry[1];
var fObjNum = parseInt(fontEntry[2], 10);
if (objectMap[fObjNum]) {
var fHeader = objectMap[fObjNum].header;
var baseFontMatch = fHeader.match(/\/BaseFont\s*\/([^\s\/>]+)/);
var baseFont = baseFontMatch ? baseFontMatch[1] : fontTag;
// Check /ToUnicode
var toUniMatch = fHeader.match(/\/ToUnicode\s+(\d+)\s+(\d+)\s+R/);
var cMap = {};
var hasToUnicode = false;
if (toUniMatch) {
var toUniObjNum = parseInt(toUniMatch[1], 10);
if (objectMap[toUniObjNum]) {
var cmapStreamText = KeterPDFParser.decompressStream(objectMap[toUniObjNum]);
cMap = KeterPDFParser.parseCMapText(cmapStreamText);
hasToUnicode = KeterUtils.getKeyCount(cMap) > 0;
}
}
// Check /Widths
var widths = [];
var firstChar = 0, lastChar = 255;
var fcMatch = fHeader.match(/\/FirstChar\s+(\d+)/);
var lcMatch = fHeader.match(/\/LastChar\s+(\d+)/);
if (fcMatch) firstChar = parseInt(fcMatch[1], 10);
if (lcMatch) lastChar = parseInt(lcMatch[1], 10);
var wArrayMatch = fHeader.match(/\/Widths\s*\[([\s\S]*?)\]/);
if (!wArrayMatch) {
var wRefMatch = fHeader.match(/\/Widths\s+(\d+)\s+(\d+)\s+R/);
if (wRefMatch && objectMap[parseInt(wRefMatch[1], 10)]) {
wArrayMatch = objectMap[parseInt(wRefMatch[1], 10)].header.match(/\[([\s\S]*?)\]/);
}
}
if (wArrayMatch) {
var wParts = wArrayMatch[1].split(/\s+/);
for (var wp = 0; wp < wParts.length; wp++) {
if (wParts[wp] !== "") {
widths.push(parseFloat(wParts[wp]));
}
}
}
fontMap[fontTag] = {
baseFont: baseFont,
cMap: cMap,
hasToUnicode: hasToUnicode,
widths: widths,
firstChar: firstChar,
lastChar: lastChar
};
}
}
}
return fontMap;
},
// Decode PDF string using the specific font's CMap
decodePDFString: function (str, fontInfo, isHex) {
if (!str) return "";
var cMap = (fontInfo && fontInfo.cMap) ? fontInfo.cMap : null;
var res = "";
if (isHex) {
for (var i = 0; i < str.length; i += 4) {
var hexCode = parseInt(str.substr(i, 4), 16);
if (cMap && cMap[hexCode]) {
res += cMap[hexCode];
} else if (hexCode >= 0x0590 && hexCode <= 0x05FF) {
res += String.fromCharCode(hexCode);
} else {
var b1 = (hexCode >> 8) & 0xFF;
var b2 = hexCode & 0xFF;
if (b1 > 0) res += String.fromCharCode(b1);
if (b2 > 0) res += String.fromCharCode(b2);
}
}
} else {
var unescaped = str
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\\(/g, "(")
.replace(/\\\)/g, ")")
.replace(/\\\\/g, "\\");
for (var j = 0; j < unescaped.length; j++) {
var chCode = unescaped.charCodeAt(j);
if (cMap && cMap[chCode]) {
res += cMap[chCode];
} else {
res += unescaped.charAt(j);
}
}
}
return res;
},
// Calculate string width using font /Widths metrics when available
calculateSpanWidth: function (textStr, fontSize, fontInfo) {
if (fontInfo && fontInfo.widths && fontInfo.widths.length > 0) {
var totalPt = 0;
for (var i = 0; i < textStr.length; i++) {
var code = textStr.charCodeAt(i);
var idx = code - fontInfo.firstChar;
if (idx >= 0 && idx < fontInfo.widths.length) {
totalPt += (fontInfo.widths[idx] / 1000.0) * fontSize;
} else {
totalPt += fontSize * 0.55; // measured fallback
}
}
return { widthPt: totalPt, isMeasured: true };
}
// If no /Widths table exists in font descriptor, return flag for uncertain geometry
return { widthPt: textStr.length * (fontSize * 0.55), isMeasured: false };
},
// Extract positioned text spans from decompressed content stream of a single page
extractPageTextSpans: function (contentStreamText, pageFonts, pageHeightPt) {
var spans = [];
var textBlockRegex = /BT[\s\S]*?ET/g;
var blockMatch;
while ((blockMatch = textBlockRegex.exec(contentStreamText)) !== null) {
var block = blockMatch[0];
var curFontTag = "Default";
var curSize = 10;
var curX = 0, curY = 0;
var leading = 12;
var ops = block.split(/\r?\n/);
for (var i = 0; i < ops.length; i++) {
var line = ops[i];
// Font selection: /F1 12 Tf
var tfMatch = line.match(/\/(\S+)\s+([\d\.\-]+)\s+Tf/);
if (tfMatch) {
curFontTag = tfMatch[1];
curSize = parseFloat(tfMatch[2]);
continue;
}
// Text matrix: [a b c d e f] Tm
var tmMatch = line.match(/([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s+Tm/);
if (tmMatch) {
curX = parseFloat(tmMatch[5]);
curY = parseFloat(tmMatch[6]);
continue;
}
// Text position: x y Td or TD
var tdMatch = line.match(/([\d\.\-]+)\s+([\d\.\-]+)\s+T[dD]/);
if (tdMatch) {
curX += parseFloat(tdMatch[1]);
curY += parseFloat(tdMatch[2]);
continue;
}
// Set leading: TL
var tlMatch = line.match(/([\d\.\-]+)\s+TL/);
if (tlMatch) {
leading = parseFloat(tlMatch[1]);
continue;
}
// Move to next line: T*
if (line.match(/T\*/)) {
curY -= leading;
continue;
}
// Show text: (string) Tj or <hex> Tj
var tjMatch = line.match(/\(([\s\S]*?)\)\s*Tj/) || line.match(/<([0-9a-fA-F]+)>\s*Tj/);
if (tjMatch) {
var rawText = tjMatch[1];
var fontInfo = pageFonts[curFontTag];
var decoded = KeterPDFParser.decodePDFString(rawText, fontInfo, line.indexOf("<") !== -1);
if (decoded && decoded.length > 0) {
var wResult = KeterPDFParser.calculateSpanWidth(decoded, curSize, fontInfo);
var idTop = pageHeightPt - curY - curSize;
spans.push({
text: decoded,
x: curX,
y: idTop,
fontSize: curSize,
fontTag: curFontTag,
baseFont: fontInfo ? fontInfo.baseFont : "Unknown",
width: wResult.widthPt,
isMeasuredWidth: wResult.isMeasured,
height: curSize * 1.2
});
curX += wResult.widthPt;
}
continue;
}
// Show text array: [ (str) 20 (str) ] TJ
var bigTjMatch = line.match(/\[([\s\S]*?)\]\s*TJ/);
if (bigTjMatch) {
var arrayContent = bigTjMatch[1];
var fontInfo2 = pageFonts[curFontTag];
var elemRegex = /\(([\s\S]*?)\)|<([0-9a-fA-F]+)>|([\d\.\-]+)/g;
var elemMatch;
var chunkText = "";
var chunkWidth = 0;
while ((elemMatch = elemRegex.exec(arrayContent)) !== null) {
if (elemMatch[1] || elemMatch[2]) {
var itemRaw = elemMatch[1] || elemMatch[2];
var isHex = !!elemMatch[2];
var dStr = KeterPDFParser.decodePDFString(itemRaw, fontInfo2, isHex);
chunkText += dStr;
var wSub = KeterPDFParser.calculateSpanWidth(dStr, curSize, fontInfo2);
chunkWidth += wSub.widthPt;
} else if (elemMatch[3]) {
// Kerning adjustment in 1/1000 text units
var kernVal = parseFloat(elemMatch[3]);
chunkWidth -= (kernVal / 1000.0) * curSize;
}
}
if (chunkText && chunkText.length > 0) {
var idTop2 = pageHeightPt - curY - curSize;
spans.push({
text: chunkText,
x: curX,
y: idTop2,
fontSize: curSize,
fontTag: curFontTag,
baseFont: fontInfo2 ? fontInfo2.baseFont : "Unknown",
width: chunkWidth,
isMeasuredWidth: !!(fontInfo2 && fontInfo2.widths && fontInfo2.widths.length > 0),
height: curSize * 1.2
});
curX += chunkWidth;
}
}
}
}
return spans;
}
};
// ------------------------------------------------------------------------
// MODULE 6: SPATIAL LAYOUT & PHYSICAL GUTTER ANALYSIS (NO ARBITRARY %)
// ------------------------------------------------------------------------
var KeterLayoutEngine = {
// Groups raw text spans into lines and detects physical column gutters
analyzeLayout: function (spans, pageWidthPt, pageHeightPt) {
if (!spans || spans.length === 0) return [];
// 1. Sort spans: Top-to-Bottom, then Right-to-Left (for Hebrew)
spans.sort(function (a, b) {
var yDiff = a.y - b.y;
if (Math.abs(yDiff) > 3.5) {
return yDiff;
}
return b.x - a.x; // Right-to-Left
});
// 2. Assemble into visual lines
var lines = [];
var curLine = null;
for (var i = 0; i < spans.length; i++) {
var s = spans[i];
if (!curLine || Math.abs(s.y - curLine.y) > (s.fontSize * 0.45)) {
curLine = {
y: s.y,
fontSize: s.fontSize,
minX: s.x,
maxX: s.x + s.width,
spans: [s],
text: s.text
};
lines.push(curLine);
} else {
curLine.spans.push(s);
curLine.minX = Math.min(curLine.minX, s.x);
curLine.maxX = Math.max(curLine.maxX, s.x + s.width);
curLine.text += " " + s.text;
}
}
// 3. Detect Headers & Footers based on vertical margins
var headerLines = [];
var footerLines = [];
var candidateBodyLines = [];
var topMarginBoundary = pageHeightPt * 0.10;
var bottomMarginBoundary = pageHeightPt * 0.90;
for (var l = 0; l < lines.length; l++) {
var line = lines[l];
if (line.y < topMarginBoundary && line.fontSize <= 12) {
headerLines.push(line);
} else if (line.y > bottomMarginBoundary && line.fontSize <= 11) {
footerLines.push(line);
} else {
candidateBodyLines.push(line);
}
}
var regions = [];
if (headerLines.length > 0) {
regions.push(this.buildRegion(headerLines, "RunningHeader", "Keter_RunningHeader"));
}
// 4. Physical Gutter Analysis for Body Columns (not fixed arbitrary percentages)
var columnRegions = this.detectPhysicalColumns(candidateBodyLines, pageWidthPt);
for (var c = 0; c < columnRegions.length; c++) {
regions.push(columnRegions[c]);
}
if (footerLines.length > 0) {
regions.push(this.buildRegion(footerLines, "PageFooter", "Keter_RunningHeader"));
}
return regions;
},
// Analyzes horizontal gaps between lines to detect true columns
detectPhysicalColumns: function (bodyLines, pageWidthPt) {
if (!bodyLines || bodyLines.length === 0) return [];
// Calculate horizontal span frequencies
var leftColumnLines = [];
var rightColumnLines = [];
var fullWidthLines = [];
var midX = pageWidthPt / 2.0;
var minGutterWidth = 14.0; // at least 14pt gutter between columns
var hasLeft = false;
var hasRight = false;
for (var i = 0; i < bodyLines.length; i++) {
var bl = bodyLines[i];
var width = bl.maxX - bl.minX;
// If a line spans more than 70% of the page, it is a single-column or banner
if (width > pageWidthPt * 0.70) {
fullWidthLines.push(bl);
} else if (bl.minX > (midX - 10)) {
// Right column (RTL start)
rightColumnLines.push(bl);
hasRight = true;
} else if (bl.maxX < (midX + 10)) {
// Left column
leftColumnLines.push(bl);
hasLeft = true;
} else {
fullWidthLines.push(bl);
}
}
var resultRegions = [];
// If genuine dual columns are detected
if (hasLeft && hasRight && (leftColumnLines.length + rightColumnLines.length > fullWidthLines.length)) {
KeterLogger.log("Physical column gutter detected at X ≈ " + Math.round(midX) + " pt.");
// Right-to-Left order: Right column first, then Left column
if (rightColumnLines.length > 0) {
resultRegions.push(this.buildRegion(rightColumnLines, "Column_Right_RTL", "Keter_Main_Hebrew_RTL"));
}
if (leftColumnLines.length > 0) {
resultRegions.push(this.buildRegion(leftColumnLines, "Column_Left_RTL", "Keter_Main_Hebrew_RTL"));
}
if (fullWidthLines.length > 0) {
resultRegions.push(this.buildRegion(fullWidthLines, "Body_Spanning", "Keter_Main_Hebrew_RTL"));
}
} else {
// Single unified text stream
resultRegions.push(this.buildRegion(bodyLines, "BodyText", "Keter_Main_Hebrew_RTL"));
}
return resultRegions;
},
buildRegion: function (linesList, roleName, styleName) {
var minY = 999999, minX = 999999, maxY = 0, maxX = 0;
var textParts = [];
for (var i = 0; i < linesList.length; i++) {
var l = linesList[i];
minY = Math.min(minY, l.y);
minX = Math.min(minX, l.minX);
maxY = Math.max(maxY, l.y + (l.fontSize * 1.25));
maxX = Math.max(maxX, l.maxX);
textParts.push(l.text);
}
return {
role: roleName,
styleName: styleName,
bounds: [minY, minX, maxY, maxX],
content: textParts.join("\r")
};
}
};
// ------------------------------------------------------------------------
// MODULE 7: INDESIGN DOM GENERATOR (VALIDATED API ACCESS)
// ------------------------------------------------------------------------
var KeterInDesignDOM = {
setupDocument: function (templateFile, pageWidthPt, pageHeightPt) {
var doc;
if (templateFile && templateFile.exists) {
KeterLogger.log("Opening existing template: " + templateFile.fsName);
doc = app.open(templateFile);
} else {
KeterLogger.log("Creating new InDesign document: " + Math.round(pageWidthPt) + " x " + Math.round(pageHeightPt) + " pt");
doc = app.documents.add();
doc.documentPreferences.pageWidth = pageWidthPt + "pt";
doc.documentPreferences.pageHeight = pageHeightPt + "pt";
doc.documentPreferences.facingPages = true;
}
// CRITICAL FIX: Explicitly lock document measurement units to Points
// to prevent coordinate scaling errors when user InDesign uses mm or picas
try {
doc.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.POINTS;
doc.viewPreferences.verticalMeasurementUnits = MeasurementUnits.POINTS;
doc.viewPreferences.rulerOrigin = RulerOrigin.PAGE_ORIGIN;
} catch (eUnits) {
KeterLogger.warn("Measurement unit assignment warning: " + eUnits.message);
}
this.ensureLayers(doc);
this.ensureStyles(doc);
return doc;
},
ensureLayers: function (doc) {
// PDF Reference Underlay (locked, bottom)
var underlayLayer = doc.layers.itemByName(KeterConfig.layerUnderlay);
if (!underlayLayer.isValid) {
// UIColors safety check: never reference bare UIColors
underlayLayer = doc.layers.add({ name: KeterConfig.layerUnderlay });
}
underlayLayer.move(LocationOptions.AT_END);
// Reconstructed Editable Text (active, top)
var textLayer = doc.layers.itemByName(KeterConfig.layerText);
if (!textLayer.isValid) {
textLayer = doc.layers.add({ name: KeterConfig.layerText });
}
doc.activeLayer = textLayer;
},
ensureStyles: function (doc) {
var bestHebrewFont = this.findBestAvailableFont(KeterConfig.hebrewFontFallbacks);
KeterLogger.log("Selected primary Hebrew font: " + bestHebrewFont);
this.createOrUpdateParaStyle(doc, "Keter_Main_Hebrew_RTL", {
appliedFont: bestHebrewFont,
pointSize: 12,
leading: 15.5,
justification: Justification.FULLY_JUSTIFIED,
isRTL: true
});
this.createOrUpdateParaStyle(doc, "Keter_RunningHeader", {
appliedFont: bestHebrewFont,
pointSize: 9,
leading: 11,
justification: Justification.CENTER_ALIGN,
isRTL: true
});
},
findBestAvailableFont: function (candidates) {
for (var i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
try {
var f = app.fonts.itemByName(candidate);
if (f && f.isValid) {
return candidate;
}
} catch (e) {}
}
KeterLogger.stats.fontSubstitutions.push("Times New Roman (Hebrew fallback)");
return "Times New Roman";
},
createOrUpdateParaStyle: function (doc, styleName, props) {
var style = doc.paragraphStyles.itemByName(styleName);
if (!style.isValid) {
style = doc.paragraphStyles.add({ name: styleName });
}
try {
if (props.appliedFont) style.appliedFont = props.appliedFont;
if (props.pointSize) style.pointSize = props.pointSize;
if (props.leading) style.leading = props.leading;
if (props.justification) style.justification = props.justification;
// Enforce Adobe World-Ready Paragraph Composer
try {
style.composer = "Adobe World-Ready Paragraph Composer";
} catch (e1) {
try {
style.composer = "$ID/Adobe World-Ready Paragraph Composer";
} catch (e2) {}
}
// Enforce Right-to-Left Direction
if (props.isRTL) {
try {
style.paragraphDirection = ParagraphDirectionOptions.RIGHT_TO_LEFT_DIRECTION;
} catch (e3) {
try {
style.paragraphDirection = 1278366308;
} catch (e4) {}
}
}
} catch (ex) {
KeterLogger.warn("Style configuration note for '" + styleName + "': " + ex.message);
}
return style;
},
placePDFUnderlay: function (doc, page, pdfFile, pageNum, opacity) {
try {
var underlayLayer = doc.layers.itemByName(KeterConfig.layerUnderlay);
underlayLayer.locked = false;
doc.activeLayer = underlayLayer;
try {
app.pdfPlacePreferences.pdfCrop = PDFCrop.CROP_MEDIA;
} catch (eCrop) {
try { app.pdfPlacePreferences.pdfCrop = PDFCrop.cropMedia; } catch (eCrop2) {}
}
app.pdfPlacePreferences.pageNumber = pageNum;
app.pdfPlacePreferences.transparentBackground = true;
var placed = page.place(pdfFile, [0, 0])[0];
if (placed && placed.isValid && opacity && opacity < 100) {
try {
placed.transparencySettings.blendingSettings.opacity = opacity;
} catch (eOp) {}
}
underlayLayer.locked = true;
doc.activeLayer = doc.layers.itemByName(KeterConfig.layerText);
KeterLogger.log("Placed 1:1 PDF underlay reference for page " + pageNum);
return true;
} catch (ex) {
KeterLogger.error("Failed to place PDF underlay for page " + pageNum + ": " + ex.message);
return false;
}
},
createTextFrame: function (doc, page, bounds, content, styleName) {
try {
var textLayer = doc.layers.itemByName(KeterConfig.layerText);
doc.activeLayer = textLayer;
var top = Math.max(0, bounds[0]);
var left = Math.max(0, bounds[1]);
var bottom = Math.max(top + 16, bounds[2]);
var right = Math.max(left + 24, bounds[3]);
var frame = page.textFrames.add(textLayer, LocationOptions.UNKNOWN_VALUE, {
geometricBounds: [top, left, bottom, right]
});
// Apply World-Ready Composer directly to frame parent story
try {
frame.parentStory.composer = "Adobe World-Ready Paragraph Composer";
frame.parentStory.paragraphDirection = ParagraphDirectionOptions.RIGHT_TO_LEFT_DIRECTION;
} catch (eWRC) {}
// Assign Unicode text contents
frame.contents = content;
var pStyle = doc.paragraphStyles.itemByName(styleName);
if (pStyle.isValid) {
frame.parentStory.texts.item(0).applyParagraphStyle(pStyle, true);
}
KeterLogger.stats.textFramesCreated++;
KeterLogger.stats.charactersExtracted += content.length;
if (frame.overflows) {
KeterLogger.stats.oversetFrames++;
KeterLogger.warn("Overset text in frame on page " + page.name + " (" + styleName + "). Frame resized slightly or leading adjustment required.");
}
return frame;
} catch (ex) {
KeterLogger.error("Error creating InDesign text frame: " + ex.message);
return null;
}
}
};
// ------------------------------------------------------------------------
// MODULE 8: USER INTERFACE (SCRIPTUI DIALOG & PROGRESS WINDOW)
// ------------------------------------------------------------------------
var KeterUI = {
showMainDialog: function () {
var w = new Window("dialog", KeterConfig.name + " v" + KeterConfig.version);
w.orientation = "column";
w.alignChildren = ["fill", "top"];
w.spacing = 10;
w.margins = 16;
var headerPnl = w.add("panel", undefined, undefined, { borderStyle: "black" });
headerPnl.orientation = "column";
headerPnl.alignChildren = ["fill", "top"];
headerPnl.margins = 10;
var titleTxt = headerPnl.add("statictext", undefined, "KETER PDF-TO-INDESIGN CONVERSION ENGINE");
titleTxt.graphics.font = ScriptUI.newFont(w.graphics.font.name, ScriptUI.FontStyle.BOLD, 14);
headerPnl.add("statictext", undefined, "Audited Production Engine: Strict Truth-in-Extraction Architecture");
// SOURCE PDF
var srcPnl = w.add("panel", undefined, "Source PDF & Options");
srcPnl.orientation = "column";
srcPnl.alignChildren = ["fill", "top"];
srcPnl.margins = 12;
srcPnl.spacing = 8;
var pdfRow = srcPnl.add("group");
pdfRow.orientation = "row";
var pdfPathInput = pdfRow.add("edittext", undefined, "", { readonly: true });
pdfPathInput.characters = 42;
var browsePdfBtn = pdfRow.add("button", undefined, "Browse PDF...");
var pdfInfoTxt = srcPnl.add("statictext", undefined, "Select a PDF to inspect structure and content streams.");
var optGroup = srcPnl.add("group");
optGroup.orientation = "column";
optGroup.alignChildren = ["left", "top"];
optGroup.spacing = 4;
var underlayCb = optGroup.add("checkbox", undefined, "Place original PDF page as locked 1:1 visual reference underlay");
underlayCb.value = true;
var opacityRow = optGroup.add("group");
opacityRow.orientation = "row";
opacityRow.add("statictext", undefined, "Underlay Opacity:");
var opacityDropdown = opacityRow.add("dropdownlist", undefined, ["25%", "40% (Recommended)", "60%", "100% (Solid)"]);
opacityDropdown.selection = 1;
var pageRangeRow = srcPnl.add("group");
pageRangeRow.orientation = "row";
pageRangeRow.add("statictext", undefined, "Page Range to Process:");
var pageRangeInput = pageRangeRow.add("edittext", undefined, "All");
pageRangeInput.characters = 12;
pageRangeRow.add("statictext", undefined, "(e.g., 'All', '1-5')");
var btnRow = w.add("group");
btnRow.orientation = "row";
btnRow.alignment = "right";
var cancelBtn = btnRow.add("button", undefined, "Cancel", { name: "cancel" });
var convertBtn = btnRow.add("button", undefined, "Start Conversion", { name: "ok" });
convertBtn.enabled = false;
var selectedFile = null;
browsePdfBtn.onClick = function () {
var f = File.openDialog("Select Source PDF File", "PDF Files:*.pdf;All Files:*.*");
if (f) {
selectedFile = f;
pdfPathInput.text = f.fsName;
pdfInfoTxt.text = "Inspecting PDF: " + f.name + "...";
convertBtn.enabled = true;
}
};
if (w.show() === 1) {
var opacityVal = 40;
if (opacityDropdown.selection.index === 0) opacityVal = 25;
if (opacityDropdown.selection.index === 1) opacityVal = 40;
if (opacityDropdown.selection.index === 2) opacityVal = 60;
if (opacityDropdown.selection.index === 3) opacityVal = 100;
return {
pdfFile: selectedFile,
placeUnderlay: underlayCb.value,
underlayOpacity: opacityVal,
pageRange: pageRangeInput.text
};
}
return null;
},
showProgress: function (title, maxVal) {
var pw = new Window("palette", title);
pw.orientation = "column";
pw.alignChildren = ["fill", "top"];
pw.margins = 16;
pw.spacing = 10;
var statusTxt = pw.add("statictext", undefined, "Initializing conversion pipeline...");
statusTxt.characters = 45;
var pBar = pw.add("progressbar", undefined, 0, maxVal);
pBar.preferredSize.width = 320;
pw.center();
pw.show();
return {
update: function (val, text) {
pBar.value = val;
if (text) statusTxt.text = text;
pw.update();
},
close: function () {
pw.close();
}
};
}
};
// ------------------------------------------------------------------------
// MODULE 9: CONVERSION PIPELINE (STRICT & HONEST EXTRACTION)
// ------------------------------------------------------------------------
function parsePageRange(rangeStr, totalPages) {
var pages = [];
if (!rangeStr || rangeStr.toLowerCase() === "all") {
for (var i = 1; i <= totalPages; i++) pages.push(i);
return pages;
}
var parts = rangeStr.split(",");
for (var p = 0; p < parts.length; p++) {
var part = KeterUtils.trim(parts[p]);
if (part.indexOf("-") !== -1) {
var range = part.split("-");
var start = parseInt(range[0], 10);
var end = parseInt(range[1], 10);
if (!isNaN(start) && !isNaN(end)) {
for (var r = start; r <= Math.min(end, totalPages); r++) {
pages.push(r);
}
}
} else {
var single = parseInt(part, 10);
if (!isNaN(single) && single >= 1 && single <= totalPages) {
pages.push(single);
}
}
}
return pages.length > 0 ? pages : [1];
}
function runConversion(options) {
KeterLogger.log("Starting Keter conversion: " + options.pdfFile.fsName);
// 1. Read binary bytes and raw string
var bytes = KeterPDFParser.readFileBytes(options.pdfFile);
var rawStr = KeterPDFParser.bytesToString(bytes);
// 2. Index PDF objects
var objectMap = KeterPDFParser.indexPDFObjects(rawStr, bytes);
var totalObjects = KeterUtils.getKeyCount(objectMap);
KeterLogger.log("Indexed " + totalObjects + " indirect PDF objects.");
// 3. Locate Root Catalog & Pages Tree
var catalogObjNum = null;
var pagesObjNum = null;
var rootMatch = rawStr.match(/\/Root\s+(\d+)\s+(\d+)\s+R/);
if (rootMatch) {
catalogObjNum = parseInt(rootMatch[1], 10);
if (objectMap[catalogObjNum]) {
var catHeader = objectMap[catalogObjNum].header;
var pagesMatch = catHeader.match(/\/Pages\s+(\d+)\s+(\d+)\s+R/);
if (pagesMatch) {
pagesObjNum = parseInt(pagesMatch[1], 10);
}
}
}
// Fallback: search for /Type /Pages object
if (!pagesObjNum) {
for (var objId in objectMap) {
if (objectMap[objId].header.indexOf("/Type /Pages") !== -1 || objectMap[objId].header.indexOf("/Type/Pages") !== -1) {
pagesObjNum = parseInt(objId, 10);
break;
}
}
}
// 4. Resolve Page Objects through Pages Tree
var resolvedPages = [];
if (pagesObjNum) {
KeterPDFParser.resolvePageTree(pagesObjNum, objectMap, resolvedPages, {}, null);
}
var totalPages = resolvedPages.length;
if (totalPages === 0) {
// Scan /Type /Page directly if Pages tree was obfuscated
for (var pId in objectMap) {
if (objectMap[pId].header.indexOf("/Type /Page") !== -1 || objectMap[pId].header.indexOf("/Type/Page") !== -1) {
resolvedPages.push({
pageObjNum: parseInt(pId, 10),
mediaBox: [0, 0, 595.28, 841.89],
header: objectMap[pId].header
});
}
}
totalPages = resolvedPages.length;
}
if (totalPages === 0) {
totalPages = 1;
}
KeterLogger.reset(totalPages);
KeterLogger.log("Resolved " + totalPages + " pages from PDF object graph.");
var targetPages = parsePageRange(options.pageRange, totalPages);
var progress = KeterUI.showProgress("Reconstructing InDesign Document", targetPages.length + 2);
progress.update(0, "Setting up InDesign document & World-Ready styles...");
// 5. Establish InDesign Document matching initial page geometry
var initialMediaBox = (resolvedPages.length > 0 && resolvedPages[0].mediaBox) ? resolvedPages[0].mediaBox : [0, 0, 595.28, 841.89];
var pageWidthPt = Math.abs(initialMediaBox[2] - initialMediaBox[0]);
var pageHeightPt = Math.abs(initialMediaBox[3] - initialMediaBox[1]);
var doc = KeterInDesignDOM.setupDocument(null, pageWidthPt, pageHeightPt);
while (doc.pages.length < targetPages.length) {
doc.pages.add(LocationOptions.AT_END);
}
// 6. Process each selected page
for (var pIdx = 0; pIdx < targetPages.length; pIdx++) {
var pageNum = targetPages[pIdx];
var idPage = doc.pages[pIdx];
progress.update(pIdx + 1, "Processing Page " + pageNum + " of " + targetPages.length + "...");
// Always place PDF 1:1 visual reference underlay when requested
if (options.placeUnderlay) {
KeterInDesignDOM.placePDFUnderlay(doc, idPage, options.pdfFile, pageNum, options.underlayOpacity);
}
var pageMeta = (pageNum - 1 < resolvedPages.length) ? resolvedPages[pageNum - 1] : null;
if (!pageMeta) {
KeterLogger.recordPageAudit(
pageNum,
"REFERENCE_UNDERLAY_ONLY",
"Page structure could not be resolved from object tree. Visual underlay placed.",
0,
0
);
continue;
}
// Extract page /Contents object(s)
var contentsMatches = [];
var cRefRegex = /\/Contents\s+(\d+)\s+(\d+)\s+R/g;
var cMatch;
while ((cMatch = cRefRegex.exec(pageMeta.header)) !== null) {
contentsMatches.push(parseInt(cMatch[1], 10));
}
// Also check for /Contents [ 12 0 R 13 0 R ]
var cArrayMatch = pageMeta.header.match(/\/Contents\s*\[([\s\S]*?)\]/);
if (cArrayMatch) {
var cArrRefs = cArrayMatch[1].match(/(\d+)\s+(\d+)\s+R/g);
if (cArrRefs) {
for (var cr = 0; cr < cArrRefs.length; cr++) {
var idNum = parseInt(cArrRefs[cr].split(/\s+/)[0], 10);
if (!KeterUtils.arrayContains(contentsMatches, idNum)) {
contentsMatches.push(idNum);
}
}
}
}
// If no contents objects could be located
if (contentsMatches.length === 0) {
KeterLogger.recordPageAudit(
pageNum,
"REFERENCE_UNDERLAY_ONLY",
"No /Contents stream found (Page may be scanned raster image or use ObjStm). Visual underlay preserved.",
0,
0
);
continue;
}
// Concatenate all content streams for this page
var pageContentText = "";
for (var cIdx = 0; cIdx < contentsMatches.length; cIdx++) {
var cObjNum = contentsMatches[cIdx];
if (objectMap[cObjNum]) {
pageContentText += KeterPDFParser.decompressStream(objectMap[cObjNum]) + "\n";
}
}
// Check if content stream has any text operators (BT...ET)
if (pageContentText.indexOf("BT") === -1 || pageContentText.indexOf("ET") === -1) {
KeterLogger.recordPageAudit(
pageNum,
"REFERENCE_UNDERLAY_ONLY",
"Content stream contains vector/raster artwork only (no text operators BT...ET). Visual underlay preserved.",
0,
0
);
continue;
}
// Resolve page-specific fonts and ToUnicode CMaps
var pageFonts = KeterPDFParser.resolvePageFonts(pageMeta.header, objectMap);
// Extract text spans
var curPageHeightPt = Math.abs(pageMeta.mediaBox[3] - pageMeta.mediaBox[1]);
var curPageWidthPt = Math.abs(pageMeta.mediaBox[2] - pageMeta.mediaBox[0]);
var spans = KeterPDFParser.extractPageTextSpans(pageContentText, pageFonts, curPageHeightPt);
if (spans.length === 0) {
KeterLogger.recordPageAudit(
pageNum,
"REFERENCE_UNDERLAY_ONLY",
"Text operators could not be mapped to Unicode characters (Font encodings / ToUnicode missing). Visual underlay preserved.",
0,
0
);
continue;
}
// Gutter and layout analysis
var regions = KeterLayoutEngine.analyzeLayout(spans, curPageWidthPt, curPageHeightPt);
// Create editable InDesign Text Frames
var pageCharCount = 0;
for (var r = 0; r < regions.length; r++) {
var reg = regions[r];
KeterInDesignDOM.createTextFrame(doc, idPage, reg.bounds, reg.content, reg.styleName);
pageCharCount += reg.content.length;
}
KeterLogger.recordPageAudit(
pageNum,
"EDITABLE_RECONSTRUCTED",
"Successfully reconstructed " + regions.length + " editable text frame(s).",
regions.length,
pageCharCount
);
}
progress.update(targetPages.length + 1, "Saving diagnostic report...");
progress.close();
// 7. Save Audit Report
var folder = options.pdfFile.parent;
var baseName = options.pdfFile.name.replace(/\.pdf$/i, "");
var reportFile = KeterLogger.saveReportFile(folder.fsName, baseName, doc.name, options.pdfFile.fsName);
// 8. Display Completion Summary Dialog
var summaryMsg = "Keter Conversion Completed!\n\n" +
"• Total Pages Processed: " + targetPages.length + "\n" +
"• Pages Converted to Editable Text: " + KeterLogger.stats.editablePagesCount + "\n" +
"• Pages Preserved as 1:1 Visual Underlay: " + KeterLogger.stats.underlayOnlyPagesCount + "\n" +
"• Text Frames Created: " + KeterLogger.stats.textFramesCreated + "\n" +
"• Recovered Characters: " + KeterLogger.stats.charactersExtracted + "\n" +
"• Overset Text Warnings: " + KeterLogger.stats.oversetFrames + "\n\n" +
"Diagnostic report saved to:\n" + (reportFile ? reportFile.fsName : "Report generated in log.");
alert(summaryMsg, KeterConfig.name);
}
// ------------------------------------------------------------------------
// ENTRY POINT: SINGLE UNDOABLE TRANSACTION
// ------------------------------------------------------------------------
function main() {
if (typeof app === "undefined" || app.documents === undefined) {
alert("This script must be run directly inside Adobe InDesign.", "Keter Engine Error");
return;
}
var options = KeterUI.showMainDialog();
if (!options) return;
app.doScript(
function () {
try {
runConversion(options);
} catch (err) {
alert("Conversion encountered a critical error:\n" + err.message + "\nLine: " + (err.line || "unknown"), "Keter Error");
KeterLogger.error("CRITICAL RUNTIME ERROR: " + err.message + " (Line " + (err.line || "unknown") + ")");
}
},
ScriptLanguage.JAVASCRIPT,
[],
UndoModes.ENTIRE_SCRIPT,
"Keter PDF to InDesign Conversion"
);
}
main();
})();