Guest User

Untitled

a guest
Feb 5th, 2026
5,777
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/env bun
  2.  
  3. /**
  4.  * Combined PDF Recovery Script
  5.  *
  6.  * Takes a corrupted base64-encoded PDF and produces the best recovered PDF.
  7.  * Combines: text substitutions + stream-by-stream l→1 correction search
  8.  *
  9.  * Usage: bun recover_pdf.ts <input.txt> <output.pdf>
  10.  */
  11.  
  12. import { readFileSync, writeFileSync, existsSync } from "fs";
  13. import { inflateSync } from "zlib";
  14. import { spawnSync } from "child_process";
  15.  
  16. // Check if a command exists
  17. function commandExists(cmd: string): boolean {
  18.   const result = spawnSync("which", [cmd], { encoding: "utf-8" });
  19.   return result.status === 0;
  20. }
  21.  
  22. // Run Ghostscript to re-distill PDF (makes it viewable)
  23. function runGhostscript(inputPath: string, outputPath: string): boolean {
  24.   if (!commandExists("gs")) {
  25.     console.error("\n⚠ Ghostscript (gs) not found. Install with: brew install ghostscript");
  26.     return false;
  27.   }
  28.  
  29.   const result = spawnSync("gs", [
  30.     "-sDEVICE=pdfwrite",
  31.     "-dCompatibilityLevel=1.4",
  32.     "-dNOPAUSE",
  33.     "-dQUIET",
  34.     "-dBATCH",
  35.     `-sOutputFile=${outputPath}`,
  36.     inputPath
  37.   ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
  38.  
  39.   return existsSync(outputPath);
  40. }
  41.  
  42. // Extract text from PDF using Ghostscript
  43. function extractText(pdfPath: string): string {
  44.   if (!commandExists("gs")) {
  45.     return "(gs not available for text extraction)";
  46.   }
  47.  
  48.   const result = spawnSync("gs", [
  49.     "-sDEVICE=txtwrite",
  50.     "-sOutputFile=-",
  51.     "-dQUIET",
  52.     "-dNOPAUSE",
  53.     "-dBATCH",
  54.     pdfPath
  55.   ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
  56.  
  57.   return result.stdout || "";
  58. }
  59.  
  60. // Known OCR error corrections (base64 level)
  61. const BASE64_SUBSTITUTIONS: [string, string][] = [
  62.   ["eGlw", "eG1w"],  // xipG → xmpG
  63.   ["TmFp", "TmFt"],  // Naie → Name
  64.   ["aW9k", "bW9k"],  // iode → mode
  65.   ["Q0lZ", "Q01Z"],  // CIYK → CMYK
  66.   ["UHJ1", "UHJl"],  // Pru → Pre (Predictor)
  67.   ["b2JT", "b2NT"],  // ProbSet → ProcSet
  68.   ["dElv", "dE1v"],  // LastIodified → LastModified
  69. ];
  70.  
  71. // Decoded content fixes (applied after base64 decode)
  72. const DECODED_FIXES: [RegExp, string][] = [
  73.   [/Naie\b/g, "Name"],
  74.   [/\biode\b/g, "mode"],
  75.   [/ProbSet/g, "ProcSet"],
  76.   [/xipG/g, "xmpG"],
  77.   [/>>\./g, ">>/"],
  78.   [/\]\./g, "]/"],
  79.   [/23\./g, "23/"],
  80.   [/\.Type\b/g, "/Type"],
  81.   [/\.Prop/g, "/Prop"],
  82.   [/\.Reso/g, "/Reso"],
  83.   [/\.Filter\b/g, "/Filter"],
  84.   [/\.Length\b/g, "/Length"],
  85.   [/\.Pages\b/g, "/Pages"],
  86.   [/\.Root\b/g, "/Root"],
  87.   [/\.Info\b/g, "/Info"],
  88.   [/\.Size\b/g, "/Size"],
  89.   [/%4338D8C/g, "54338D8C"],  // Hex ID fix
  90. ];
  91.  
  92. const L_BYTE = 'l'.charCodeAt(0);
  93. const ONE_BYTE = '1'.charCodeAt(0);
  94.  
  95. interface StreamLoc {
  96.   obj: number;
  97.   start: number;
  98.   end: number;
  99.   size: number;
  100. }
  101.  
  102. function pdfToB64(pdfPos: number): number {
  103.   return Math.floor((pdfPos * 4) / 3);
  104. }
  105.  
  106. function findObjStream(pdfStr: string, objNum: number): StreamLoc | null {
  107.   const pattern = new RegExp(objNum + '\\s+0\\s+obj[^>]*>>\\s*stream[\\r\\n]+');
  108.   const match = pdfStr.match(pattern);
  109.   if (!match) return null;
  110.  
  111.   const start = match.index! + match[0].length;
  112.   const end = pdfStr.indexOf('endstream', start);
  113.   if (end === -1) return null;
  114.   return { obj: objNum, start, end, size: end - start };
  115. }
  116.  
  117. function canDecompress(pdf: Buffer, stream: StreamLoc): boolean {
  118.   try {
  119.     inflateSync(pdf.subarray(stream.start, stream.end));
  120.     return true;
  121.   } catch {
  122.     return false;
  123.   }
  124. }
  125.  
  126. function findAllStreams(pdfStr: string): StreamLoc[] {
  127.   const streams: StreamLoc[] = [];
  128.   const pattern = /(\d+)\s+0\s+obj[^>]*>>\s*stream[\r\n]+/g;
  129.   let match;
  130.  
  131.   while ((match = pattern.exec(pdfStr)) !== null) {
  132.     const objNum = parseInt(match[1]);
  133.     const start = match.index + match[0].length;
  134.     const end = pdfStr.indexOf('endstream', start);
  135.     if (end !== -1) {
  136.       streams.push({ obj: objNum, start, end, size: end - start });
  137.     }
  138.   }
  139.  
  140.   return streams;
  141. }
  142.  
  143. async function main() {
  144.   const args = process.argv.slice(2);
  145.  
  146.   if (args.length < 2) {
  147.     console.log("Usage: bun recover_pdf.ts <input.txt> <output.pdf>");
  148.     console.log("  input.txt  - base64-encoded PDF file");
  149.     console.log("  output.pdf - recovered PDF output");
  150.     process.exit(1);
  151.   }
  152.  
  153.   const inputFile = args[0];
  154.   const outputFile = args[1];
  155.  
  156.   console.log("╔══════════════════════════════════════════════════════════════╗");
  157.   console.log("║       Combined PDF Recovery Script                           ║");
  158.   console.log("╚══════════════════════════════════════════════════════════════╝");
  159.  
  160.   // Step 1: Read original base64
  161.   console.log(`\n[1/4] Reading ${inputFile}...`);
  162.   let base64Str = readFileSync(inputFile, "utf-8").trim();
  163.   console.log(`  ${base64Str.length} characters`);
  164.  
  165.   // Step 2: Apply known text substitutions
  166.   console.log("\n[2/4] Applying known text corrections...");
  167.   for (const [from, to] of BASE64_SUBSTITUTIONS) {
  168.     const count = (base64Str.match(new RegExp(from, 'g')) || []).length;
  169.     if (count > 0) {
  170.       base64Str = base64Str.replace(new RegExp(from, 'g'), to);
  171.       console.log(`  ${from} → ${to}: ${count} fixes`);
  172.     }
  173.   }
  174.  
  175.   let base64Buf = Buffer.from(base64Str);
  176.   let pdf = Buffer.from(base64Buf.toString(), 'base64');
  177.   let pdfStr = pdf.toString('binary');
  178.  
  179.   // Apply decoded content fixes
  180.   console.log("\n  Applying decoded content fixes...");
  181.   for (const [pattern, replacement] of DECODED_FIXES) {
  182.     const matches = pdfStr.match(pattern);
  183.     if (matches && matches.length > 0) {
  184.       pdfStr = pdfStr.replace(pattern, replacement);
  185.       console.log(`    ${pattern.source}: ${matches.length} fixes`);
  186.     }
  187.   }
  188.   pdf = Buffer.from(pdfStr, 'binary');
  189.   base64Buf = Buffer.from(pdf.toString('base64'));
  190.  
  191.   // Step 3: Find all streams and check status
  192.   console.log("\n[3/4] Analyzing streams...");
  193.   const allStreams = findAllStreams(pdfStr);
  194.   let okCount = allStreams.filter(s => canDecompress(pdf, s)).length;
  195.   console.log(`  Found ${allStreams.length} streams, ${okCount} decompress OK`);
  196.  
  197.   // Page content streams (most important for visible content)
  198.   const pageContentObjs = [39, 40, 41, 42, 43, 44, 45, 46];
  199.  
  200.   console.log("\n  Page content streams:");
  201.   for (const objNum of pageContentObjs) {
  202.     const s = findObjStream(pdfStr, objNum);
  203.     if (s) {
  204.       const ok = canDecompress(pdf, s);
  205.       console.log(`    Object ${objNum}: ${s.size}b [${ok ? 'OK' : 'FAIL'}]`);
  206.     }
  207.   }
  208.  
  209.   // Step 4: Repair streams with l→1 corrections
  210.   console.log("\n[4/4] Repairing streams...");
  211.  
  212.   // Only attempt streams that are feasible to brute-force (< 3000 bytes)
  213.   const MAX_STREAM_SIZE = 3000;
  214.  
  215.   // Get all failed streams, prioritize page content, sort by size
  216.   const failedStreams = allStreams
  217.     .filter(s => !canDecompress(pdf, s) && s.size < MAX_STREAM_SIZE)
  218.     .sort((a, b) => {
  219.       // Page content first
  220.       const aIsPage = pageContentObjs.includes(a.obj);
  221.       const bIsPage = pageContentObjs.includes(b.obj);
  222.       if (aIsPage && !bIsPage) return -1;
  223.       if (!aIsPage && bIsPage) return 1;
  224.       // Then by size (smaller first)
  225.       return a.size - b.size;
  226.     });
  227.  
  228.   console.log(`  ${failedStreams.length} streams to attempt (< ${MAX_STREAM_SIZE}b)\n`);
  229.  
  230.   let totalFixed = 0;
  231.  
  232.   for (const stream of failedStreams) {
  233.     // Re-check in case earlier fixes affected this stream
  234.     pdf = Buffer.from(base64Buf.toString(), 'base64');
  235.     pdfStr = pdf.toString('binary');
  236.     const currentStream = findObjStream(pdfStr, stream.obj);
  237.     if (!currentStream || canDecompress(pdf, currentStream)) continue;
  238.  
  239.     const isPageContent = pageContentObjs.includes(stream.obj);
  240.     const label = isPageContent ? '[PAGE]' : '[other]';
  241.     process.stdout.write(`  Object ${stream.obj} ${label} (${currentStream.size}b): `);
  242.  
  243.     const padding = 200;
  244.     const b64Start = Math.max(0, pdfToB64(currentStream.start) - padding);
  245.     const b64End = Math.min(base64Buf.length, pdfToB64(currentStream.end) + padding);
  246.  
  247.     // Find all 'l' positions in this region
  248.     const lPos: number[] = [];
  249.     for (let i = b64Start; i < b64End; i++) {
  250.       if (base64Buf[i] === L_BYTE) lPos.push(i);
  251.     }
  252.  
  253.     let found = false;
  254.  
  255.     // Try single l→1
  256.     for (const p of lPos) {
  257.       base64Buf[p] = ONE_BYTE;
  258.       pdf = Buffer.from(base64Buf.toString(), 'base64');
  259.       const s = findObjStream(pdf.toString('binary'), stream.obj);
  260.       if (s && canDecompress(pdf, s)) {
  261.         console.log(`1x l→1`);
  262.         found = true;
  263.         totalFixed++;
  264.         break;
  265.       }
  266.       base64Buf[p] = L_BYTE;
  267.     }
  268.     if (found) continue;
  269.  
  270.     // Try double l→1
  271.     outer2: for (let i = 0; i < lPos.length; i++) {
  272.       for (let j = i + 1; j < lPos.length; j++) {
  273.         base64Buf[lPos[i]] = ONE_BYTE;
  274.         base64Buf[lPos[j]] = ONE_BYTE;
  275.         pdf = Buffer.from(base64Buf.toString(), 'base64');
  276.         const s = findObjStream(pdf.toString('binary'), stream.obj);
  277.         if (s && canDecompress(pdf, s)) {
  278.           console.log(`2x l→1`);
  279.           found = true;
  280.           totalFixed++;
  281.           break outer2;
  282.         }
  283.         base64Buf[lPos[i]] = L_BYTE;
  284.         base64Buf[lPos[j]] = L_BYTE;
  285.       }
  286.     }
  287.     if (found) continue;
  288.  
  289.     // Try triple l→1 (limit to 50 positions)
  290.     if (lPos.length <= 50) {
  291.       outer3: for (let i = 0; i < lPos.length; i++) {
  292.         for (let j = i + 1; j < lPos.length; j++) {
  293.           for (let k = j + 1; k < lPos.length; k++) {
  294.             base64Buf[lPos[i]] = ONE_BYTE;
  295.             base64Buf[lPos[j]] = ONE_BYTE;
  296.             base64Buf[lPos[k]] = ONE_BYTE;
  297.             pdf = Buffer.from(base64Buf.toString(), 'base64');
  298.             const s = findObjStream(pdf.toString('binary'), stream.obj);
  299.             if (s && canDecompress(pdf, s)) {
  300.               console.log(`3x l→1`);
  301.               found = true;
  302.               totalFixed++;
  303.               break outer3;
  304.             }
  305.             base64Buf[lPos[i]] = L_BYTE;
  306.             base64Buf[lPos[j]] = L_BYTE;
  307.             base64Buf[lPos[k]] = L_BYTE;
  308.           }
  309.         }
  310.       }
  311.     }
  312.     if (found) continue;
  313.  
  314.     // Try quad l→1 (limit to 40 positions)
  315.     if (lPos.length <= 40) {
  316.       outer4: for (let i = 0; i < lPos.length; i++) {
  317.         for (let j = i + 1; j < lPos.length; j++) {
  318.           for (let k = j + 1; k < lPos.length; k++) {
  319.             for (let m = k + 1; m < lPos.length; m++) {
  320.               base64Buf[lPos[i]] = ONE_BYTE;
  321.               base64Buf[lPos[j]] = ONE_BYTE;
  322.               base64Buf[lPos[k]] = ONE_BYTE;
  323.               base64Buf[lPos[m]] = ONE_BYTE;
  324.               pdf = Buffer.from(base64Buf.toString(), 'base64');
  325.               const s = findObjStream(pdf.toString('binary'), stream.obj);
  326.               if (s && canDecompress(pdf, s)) {
  327.                 console.log(`4x l→1`);
  328.                 found = true;
  329.                 totalFixed++;
  330.                 break outer4;
  331.               }
  332.               base64Buf[lPos[i]] = L_BYTE;
  333.               base64Buf[lPos[j]] = L_BYTE;
  334.               base64Buf[lPos[k]] = L_BYTE;
  335.               base64Buf[lPos[m]] = L_BYTE;
  336.             }
  337.           }
  338.         }
  339.       }
  340.     }
  341.  
  342.     if (!found) {
  343.       console.log(`no fix (${lPos.length} 'l' positions)`);
  344.     }
  345.   }
  346.  
  347.   // Final output
  348.   pdf = Buffer.from(base64Buf.toString(), 'base64');
  349.   pdfStr = pdf.toString('binary');
  350.  
  351.   console.log("\n╔══════════════════════════════════════════════════════════════╗");
  352.   console.log("║                       RESULTS                                ║");
  353.   console.log("╚══════════════════════════════════════════════════════════════╝");
  354.  
  355.   const finalStreams = findAllStreams(pdfStr);
  356.   okCount = finalStreams.filter(s => canDecompress(pdf, s)).length;
  357.   console.log(`\nTotal streams: ${okCount}/${finalStreams.length} OK`);
  358.   console.log(`Streams fixed: ${totalFixed}`);
  359.  
  360.   // Page content status
  361.   let pageOk = 0;
  362.   console.log("\nPage content streams:");
  363.   for (const objNum of pageContentObjs) {
  364.     const s = findObjStream(pdfStr, objNum);
  365.     if (s) {
  366.       const ok = canDecompress(pdf, s);
  367.       if (ok) pageOk++;
  368.       console.log(`  Object ${objNum}: ${ok ? '✓' : '✗'}`);
  369.     }
  370.   }
  371.   console.log(`\nPage content: ${pageOk}/${pageContentObjs.length} OK`);
  372.  
  373.   // Save raw repaired PDF
  374.   const rawFile = outputFile.replace(/\.pdf$/, "-raw.pdf");
  375.   writeFileSync(rawFile, pdf);
  376.   console.log(`\nSaved raw: ${rawFile}`);
  377.  
  378.   // Re-distill with Ghostscript to make it viewable
  379.   console.log("\n[5/5] Re-distilling with Ghostscript...");
  380.   if (runGhostscript(rawFile, outputFile)) {
  381.     console.log(`  ✓ Saved: ${outputFile}`);
  382.  
  383.     // Extract and show text
  384.     console.log("\n--- Extracted Text ---");
  385.     const text = extractText(outputFile);
  386.     const lines = text.split('\n').filter(l => l.trim()).slice(0, 25);
  387.     if (lines.length > 0) {
  388.       console.log(lines.join('\n'));
  389.     } else {
  390.       console.log("(no text extracted)");
  391.     }
  392.   } else {
  393.     console.log(`  ✗ Ghostscript re-distill failed`);
  394.     console.log(`  Raw PDF saved to: ${rawFile}`);
  395.     console.log("\n  To install Ghostscript: brew install ghostscript");
  396.   }
  397. }
  398.  
  399. main().catch(console.error);
  400.  
Add Comment
Please, Sign In to add comment