paper-drift 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,161 @@
1
- # Temporary Holding Version
1
+ # paper-drift
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/Sooriya04/Paper-Drift/actions/workflows/ci.yml/badge.svg)](https://github.com/Sooriya04/Paper-Drift/actions/workflows/ci.yml)
4
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5
+ [![Node Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
6
+ [![WASM Powered](https://img.shields.io/badge/engine-WebAssembly-purple.svg)](https://webassembly.org/)
7
+
8
+ > **Zero-Python, zero-Chromium WebAssembly document parser, HTML renderer, and Markdown engine with GFM tables and math.**
9
+
10
+ Convert complex academic research papers and enterprise PDFs directly into clean, structured **Markdown** or standalone **HTML** with pixel-perfect visual fidelity—without Puppeteer, Python, Docker, or external servers. Slashes AI Agent token ingestion costs by 90%+.
11
+
12
+ ---
13
+
14
+ ## ⚡ Why paper-drift?
15
+
16
+ Most PDF conversion pipelines in JavaScript suffer from two major flaws:
17
+ 1. **The Heavyweight Trap**: Requiring Headless Chromium (Puppeteer / Playwright) or Python microservices (`unstructured`, `poppler`, `nougat`) that need 500MB+ Docker images, multi-second cold boots, and high cloud costs.
18
+ 2. **The "Text Soup" Trap**: Spatially naive parsers that dump unordered lines, break multi-column layouts, and butcher tabular grids.
19
+
20
+ **`paper-drift` solves this natively:**
21
+ * **Zero-Python & Zero-Chromium**: Runs compiled WebAssembly directly in any V8 runtime (Node.js, Bun, or modern browsers). Under 20 KB package footprint.
22
+ * **Direct Markdown Engine**: Converts directly into GitHub Flavored Markdown (GFM) with auto-detected `#`, `##` headings, list markers, and clean math.
23
+ * **Geometric Table Reconstruction**: Discovers column gutters and slices cells into standard pipe tables (`| col1 | col2 |`) with clean universal math exponents (`O(n² · d)`, `10¹⁹`).
24
+ * **Multi-Column Spatial Ordering**: Resolves 2-column papers via Recursive XY-Cut without column interweaving.
25
+ * **Dual-Layer HTML Mode**: High-DPI background raster rendering paired with a 1:1 selectable, searchable text layer.
26
+ * **100% Private & Instant**: Ingests pages in under 50ms locally. Zero data leaves your machine.
27
+
28
+ ---
29
+
30
+ ## 🚀 Quick Start (CLI)
31
+
32
+ ### Run with `npx` (Zero installation required)
33
+ ```bash
34
+ # Convert to Markdown (for Claude, LLMs, or AI Agents)
35
+ npx paper-drift paper.pdf output.md --format md
36
+
37
+ # Convert to Standalone HTML (for human viewing)
38
+ npx paper-drift paper.pdf output.html
39
+ ```
40
+
41
+ ### Install Globally
42
+ ```bash
43
+ npm install -g paper-drift
44
+ ```
45
+
46
+ ### Common Commands
47
+ ```bash
48
+ # Convert to clean Markdown
49
+ paper-drift research-paper.pdf paper.md --format md
50
+
51
+ # Convert specific pages to Markdown
52
+ paper-drift paper.pdf output.md --format md --pages 1-5,8
53
+
54
+ # Convert to pixel-perfect HTML at 2.0x Retina scale
55
+ paper-drift report.pdf output.html --scale 2.0
56
+
57
+ # Text-only HTML mode (skips background raster)
58
+ paper-drift book.pdf book.html --no-bg
59
+ ```
60
+
61
+ ### CLI Options
62
+
63
+ | Flag | Type | Default | Description |
64
+ | :--- | :--- | :--- | :--- |
65
+ | `<input.pdf>` | `string` | *(required)* | Path to the source PDF file |
66
+ | `[output]` | `string` | `<name>.<ext>` | Destination output file path |
67
+ | `--format <fmt>` | `string` | `html` | Output format: `markdown` (`md`) \| `html` \| `text` \| `json` |
68
+ | `--pages <range>` | `string` | `"all"` | Target page range (e.g. `1-10`, `2,4,6`) |
69
+ | `--scale <num>` | `number` | `2.0` | DPI multiplier for background raster rendering in HTML mode |
70
+ | `--no-bg` | `boolean` | `false` | Disables background rasterization (text-only HTML) |
71
+ | `-v, --version` | `flag` | — | Display current version |
72
+ | `-h, --help` | `flag` | — | Display CLI help menu |
73
+
74
+ ---
75
+
76
+ ## 💻 Programmatic API (Node.js / Bun)
77
+
78
+ Install in your project:
79
+ ```bash
80
+ npm install paper-drift
81
+ ```
82
+
83
+ ### Direct PDF to Markdown
84
+ ```javascript
85
+ import { convertPdfToMarkdown } from 'paper-drift';
86
+
87
+ const result = await convertPdfToMarkdown('research-paper.pdf', {
88
+ pageRange: '1-10'
89
+ });
90
+
91
+ console.log(`Converted ${result.pagesProcessed} pages in ${result.durationMs}ms`);
92
+ console.log(result.markdown);
93
+ ```
94
+
95
+ ### PDF to Standalone HTML
96
+ ```javascript
97
+ import { convertPdfToHtml } from 'paper-drift';
98
+ import fs from 'fs';
99
+
100
+ const result = await convertPdfToHtml('document.pdf', {
101
+ scale: 2.0,
102
+ withBg: true
103
+ });
104
+
105
+ fs.writeFileSync('output.html', result.html);
106
+ ```
107
+
108
+ ### Buffer Input Support
109
+ Both functions accept raw `Buffer` or `Uint8Array` inputs:
110
+ ```javascript
111
+ import { convertPdfToMarkdown } from 'paper-drift';
112
+
113
+ const pdfBuffer = await fetch('https://example.com/paper.pdf').then(r => r.arrayBuffer());
114
+ const { markdown } = await convertPdfToMarkdown(Buffer.from(pdfBuffer));
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 💸 AI Agent Token Savings
120
+
121
+ When feeding 10-page academic papers into LLMs (Claude, GPT-4, Cursor, Antigravity):
122
+
123
+ | Mode | Tokens Consumed | Cost / Ingestion | Reading Accuracy |
124
+ | :--- | :--- | :--- | :--- |
125
+ | **Multimodal Vision (Images)** | ~16,000 – 20,000 tokens | ~$0.15 – $0.40 | Heavy hallucinations on dense math |
126
+ | **Naive OCR / PDF Text Dump** | ~8,000 tokens | ~$0.08 | Tables destroyed into loose lines |
127
+ | **`paper-drift` Markdown** | **~1,200 – 1,800 tokens** | **~$0.01** | **Pixel-accurate reading order, GFM tables, math** |
128
+
129
+ > **Net Savings:** Over **90% token reduction** and **12x faster ingestion**.
130
+
131
+ ---
132
+
133
+ ## 🌐 Interactive Web Playground
134
+
135
+ `paper-drift` includes a local WebAssembly playground with live GFM table and KaTeX formula preview:
136
+
137
+ 1. Start the local server:
138
+ ```bash
139
+ npm start
140
+ ```
141
+ 2. Open your browser at **http://localhost:3000** and drag-and-drop any PDF.
142
+
143
+ ---
144
+
145
+ ## 🧪 Testing & Benchmarks
146
+
147
+ Run the complete test suite:
148
+ ```bash
149
+ npm test
150
+ ```
151
+
152
+ Benchmark throughput and layout fidelity across landmark ArXiv papers:
153
+ ```bash
154
+ npm run benchmark
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 📄 License
160
+
161
+ Apache-2.0 License. See [LICENSE](LICENSE) for details.
package/bin/cli.js ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file bin/cli.js
4
+ * @description Command-line interface executable for paper-drift.
5
+ * Enables zero-Chromium PDF to high-fidelity HTML, Markdown, and natural spatial text conversion straight from terminal.
6
+ * Part of paper-drift - https://github.com/Sooriya04/Paper-Drift
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { convertPdfToHtml, convertPdfToMarkdown } from '../src/index.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ // Read package metadata for version flag
18
+ const packageJson = JSON.parse(
19
+ fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8')
20
+ );
21
+
22
+ function printHelp() {
23
+ console.log(`
24
+ \x1b[1m\x1b[36mpaper-drift\x1b[0m v${packageJson.version}
25
+ High-fidelity, zero-Chromium PDF parser powered by WebAssembly & spatial layout decomposition.
26
+
27
+ \x1b[1mUSAGE:\x1b[0m
28
+ $ paper-drift <input.pdf> [output] [options]
29
+
30
+ \x1b[1mARGUMENTS:\x1b[0m
31
+ input.pdf Path to the PDF file to convert
32
+ output (Optional) Destination file path (default: <input>.<ext>)
33
+
34
+ \x1b[1mOPTIONS:\x1b[0m
35
+ --format <fmt> Output format: markdown (md) | html | text | json (default: html)
36
+ --scale <num> DPI scale for background raster rendering (default: 2.0)
37
+ --no-bg Text-only mode; disables background raster rendering
38
+ --pages <range> Target pages to convert (e.g. "1-5", "2,4,6", default: "all")
39
+ -v, --version Show version number
40
+ -h, --help Display this help message
41
+
42
+ \x1b[1mEXAMPLES:\x1b[0m
43
+ $ paper-drift document.pdf
44
+ $ paper-drift paper.pdf output.md --format md
45
+ $ paper-drift paper.pdf output.txt --format text
46
+ $ paper-drift paper.pdf output.html --scale 2.5
47
+ $ paper-drift manual.pdf manual.html --pages 1-10 --no-bg
48
+ `);
49
+ }
50
+
51
+ async function run() {
52
+ const args = process.argv.slice(2);
53
+
54
+ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
55
+ printHelp();
56
+ process.exit(0);
57
+ }
58
+
59
+ if (args.includes('-v') || args.includes('--version')) {
60
+ console.log(`paper-drift v${packageJson.version}`);
61
+ process.exit(0);
62
+ }
63
+
64
+ // Parse positional and flag arguments
65
+ let inputFile = null;
66
+ let outputFile = null;
67
+ let format = 'html';
68
+ let scale = 2.0;
69
+ let withBg = true;
70
+ let pageRange = 'all';
71
+
72
+ for (let i = 0; i < args.length; i++) {
73
+ const arg = args[i];
74
+ if (arg === '--format') {
75
+ format = (args[++i] || 'html').toLowerCase();
76
+ } else if (arg === '--scale') {
77
+ scale = parseFloat(args[++i]) || 2.0;
78
+ } else if (arg === '--no-bg') {
79
+ withBg = false;
80
+ } else if (arg === '--pages') {
81
+ pageRange = args[++i] || 'all';
82
+ } else if (!arg.startsWith('-')) {
83
+ if (!inputFile) {
84
+ inputFile = arg;
85
+ } else if (!outputFile) {
86
+ outputFile = arg;
87
+ }
88
+ }
89
+ }
90
+
91
+ if (!inputFile) {
92
+ console.error('\x1b[31mError: Missing required argument <input.pdf>\x1b[0m');
93
+ printHelp();
94
+ process.exit(1);
95
+ }
96
+
97
+ const resolvedInput = path.resolve(process.cwd(), inputFile);
98
+ if (!fs.existsSync(resolvedInput)) {
99
+ console.error(`\x1b[31mError: Input file does not exist: ${resolvedInput}\x1b[0m`);
100
+ process.exit(1);
101
+ }
102
+
103
+ const isMarkdown = format === 'md' || format === 'markdown';
104
+ const ext = isMarkdown ? 'md' : (format === 'text' ? 'txt' : (format === 'json' ? 'json' : 'html'));
105
+ if (!outputFile) {
106
+ const parsedPath = path.parse(resolvedInput);
107
+ outputFile = path.join(process.cwd(), `${parsedPath.name}.${ext}`);
108
+ } else {
109
+ outputFile = path.resolve(process.cwd(), outputFile);
110
+ }
111
+
112
+ console.log(`\x1b[36m⚡ paper-drift\x1b[0m v${packageJson.version}`);
113
+ console.log(`📄 Input: ${resolvedInput}`);
114
+ console.log(`🎯 Output: ${outputFile} (Format: ${format.toUpperCase()})`);
115
+ console.log(`⚙️ Config: Scale=${scale}x, Background=${withBg ? 'lossless' : 'disabled'}, Pages=${pageRange}`);
116
+ console.log('🔄 Running WebAssembly parser with spatial reading order decomposition...');
117
+
118
+ try {
119
+ let outputContent = '';
120
+ let durationMs = 0;
121
+ let pagesProcessed = 0;
122
+ let totalPages = 0;
123
+ let columnsOrTablesInfo = '';
124
+
125
+ if (isMarkdown) {
126
+ const result = await convertPdfToMarkdown(resolvedInput, {
127
+ pageRange,
128
+ onProgress: ({ current, total, pageNumber }) => {
129
+ process.stdout.write(` ⏳ Processing page ${pageNumber} (${current}/${total})...\r`);
130
+ }
131
+ });
132
+ outputContent = result.markdown;
133
+ durationMs = result.durationMs;
134
+ pagesProcessed = result.pagesProcessed;
135
+ totalPages = result.totalPages;
136
+ columnsOrTablesInfo = result.pages.map(p => `p${p.pageNumber}:${p.tablesCount}tables`).join(', ');
137
+ } else {
138
+ const result = await convertPdfToHtml(resolvedInput, {
139
+ scale,
140
+ withBg,
141
+ pageRange,
142
+ format,
143
+ onProgress: ({ current, total, pageNumber }) => {
144
+ process.stdout.write(` ⏳ Processing page ${pageNumber} (${current}/${total})...\r`);
145
+ }
146
+ });
147
+ durationMs = result.durationMs;
148
+ pagesProcessed = result.pagesProcessed;
149
+ totalPages = result.totalPages;
150
+ columnsOrTablesInfo = result.pages.map(p => `p${p.pageNumber}:${p.columnsDetected}col`).join(', ');
151
+
152
+ if (format === 'text') {
153
+ outputContent = result.text;
154
+ } else if (format === 'json') {
155
+ outputContent = JSON.stringify(result.pages, null, 2);
156
+ } else {
157
+ outputContent = result.html;
158
+ }
159
+ }
160
+
161
+ fs.writeFileSync(outputFile, outputContent, 'utf-8');
162
+ const stats = fs.statSync(outputFile);
163
+ const sizeKb = (stats.size / 1024).toFixed(1);
164
+
165
+ console.log(`\n\x1b[32m✔ Conversion complete in ${durationMs}ms!\x1b[0m`);
166
+ console.log(`📊 Converted ${pagesProcessed} of ${totalPages} pages (${sizeKb} KB) [${columnsOrTablesInfo}]`);
167
+ console.log(`✨ Open your file: \x1b[4mfile://${outputFile}\x1b[0m\n`);
168
+ } catch (err) {
169
+ console.error(`\n\x1b[31m❌ Conversion failed:\x1b[0m ${err.message}`);
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ run();
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "paper-drift",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Zero-Python, zero-Chromium WebAssembly document parser, HTML renderer, and Markdown engine with GFM tables and math",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "bin",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "bin": {
17
+ "paper-drift": "./bin/cli.js"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/Sooriya04/Paper-Drift.git"
22
+ },
23
+ "scripts": {
24
+ "start": "node playground/server.js",
25
+ "test": "node test/cli.test.js && node test/spatial.test.js && node test/math.test.js && node test/tables.test.js && node test/markdown.test.js",
26
+ "convert": "node bin/cli.js test/fixtures/sample.pdf output.html",
27
+ "generate:sample": "node test/scripts/generate-sample.js",
28
+ "benchmark": "node test/scripts/benchmark-papers.js run --limit 3 --pages 1-2",
29
+ "benchmark:20": "node test/scripts/benchmark-papers.js run --limit 20 --pages 1-2",
30
+ "benchmark:download": "node test/scripts/benchmark-papers.js download 20",
31
+ "benchmark:list": "node test/scripts/benchmark-papers.js list"
32
+ },
33
+ "keywords": [
34
+ "pdf",
35
+ "webassembly",
36
+ "wasm",
37
+ "pdf-to-html",
38
+ "rag",
39
+ "mupdf",
40
+ "document-parser"
41
+ ],
42
+ "author": "Sooriya",
43
+ "license": "Apache-2.0",
44
+ "dependencies": {
45
+ "mupdf": "^1.28.1",
46
+ "pdfjs-dist": "^3.11.174"
47
+ }
48
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @file src/converter.js
3
+ * @description Core WebAssembly PDF conversion engine using MuPDF WASM and recursive XY-Cut spatial ordering.
4
+ * Resolves multi-column reading orders dynamically while preserving pixel-perfect visual fidelity.
5
+ * Part of paper-drift - https://github.com/Sooriya04/Paper-Drift
6
+ */
7
+
8
+ import mupdf from 'mupdf';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { buildHtmlDocument } from './utils/html-template.js';
12
+ import { resolveReadingOrder } from './spatial/reading-order.js';
13
+
14
+ /**
15
+ * Parses page range string (e.g. "all", "1-3", "1,3,5") into sorted 0-indexed page indices.
16
+ * @param {string|number[]} range - Target page specification.
17
+ * @param {number} totalPages - Total page count in document.
18
+ * @returns {number[]} 0-indexed page numbers.
19
+ */
20
+ export function parsePageRange(range, totalPages) {
21
+ if (!range || range === 'all') {
22
+ return Array.from({ length: totalPages }, (_, i) => i);
23
+ }
24
+
25
+ if (Array.isArray(range)) {
26
+ return range.map(p => p - 1).filter(p => p >= 0 && p < totalPages);
27
+ }
28
+
29
+ const indices = new Set();
30
+ const parts = String(range).split(',');
31
+
32
+ for (const part of parts) {
33
+ const trimmed = part.trim();
34
+ if (trimmed.includes('-')) {
35
+ const [start, end] = trimmed.split('-').map(Number);
36
+ if (!isNaN(start) && !isNaN(end)) {
37
+ for (let p = Math.max(1, start); p <= Math.min(totalPages, end); p++) {
38
+ indices.add(p - 1);
39
+ }
40
+ }
41
+ } else {
42
+ const p = parseInt(trimmed, 10);
43
+ if (!isNaN(p) && p >= 1 && p <= totalPages) {
44
+ indices.add(p - 1);
45
+ }
46
+ }
47
+ }
48
+
49
+ return Array.from(indices).sort((a, b) => a - b);
50
+ }
51
+
52
+ function escapeHtml(str) {
53
+ return String(str)
54
+ .replace(/&/g, '&amp;')
55
+ .replace(/</g, '&lt;')
56
+ .replace(/>/g, '&gt;')
57
+ .replace(/"/g, '&quot;')
58
+ .replace(/'/g, '&#39;');
59
+ }
60
+
61
+ /**
62
+ * Converts a PDF buffer or file path into a standalone high-fidelity HTML document with spatial reading order.
63
+ *
64
+ * @param {string|Buffer|Uint8Array} input - File path or binary buffer of the PDF.
65
+ * @param {Object} [options={}] - Conversion options.
66
+ * @param {number} [options.scale=2.0] - DPI scale for background rendering (2.0 = Retina).
67
+ * @param {boolean} [options.withBg=true] - Whether to render lossless background image layer.
68
+ * @param {string|number[]} [options.pageRange='all'] - Page range to process.
69
+ * @param {string} [options.title] - Document title in HTML head.
70
+ * @param {string} [options.format='html'] - Target output format ('html', 'text', 'json').
71
+ * @param {function} [options.onProgress] - Optional progress callback `({ current, total, pageIndex }) => void`.
72
+ * @returns {Promise<{ html: string, text: string, pages: Array<Object>, totalPages: number, pagesProcessed: number, durationMs: number }>}
73
+ */
74
+ export async function convertPdfToHtml(input, options = {}) {
75
+ const startTime = Date.now();
76
+ const scale = typeof options.scale === 'number' ? options.scale : 2.0;
77
+ const withBg = options.withBg !== false;
78
+ const pageRangeStr = options.pageRange || 'all';
79
+
80
+ let pdfBuffer;
81
+ let docTitle = options.title || 'Converted Document';
82
+
83
+ if (typeof input === 'string') {
84
+ if (!fs.existsSync(input)) {
85
+ throw new Error(`File not found: ${input}`);
86
+ }
87
+ pdfBuffer = fs.readFileSync(input);
88
+ docTitle = options.title || path.basename(input, path.extname(input));
89
+ } else if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
90
+ pdfBuffer = input;
91
+ } else {
92
+ throw new TypeError('Input must be a valid file path string, Buffer, or Uint8Array.');
93
+ }
94
+
95
+ // Load PDF via WebAssembly (MuPDF binary)
96
+ const doc = mupdf.Document.openDocument(pdfBuffer, 'application/pdf');
97
+ const totalPages = doc.countPages();
98
+ const targetIndices = parsePageRange(pageRangeStr, totalPages);
99
+
100
+ if (targetIndices.length === 0) {
101
+ throw new Error(`No matching pages found for page range: "${pageRangeStr}" (Document has ${totalPages} pages).`);
102
+ }
103
+
104
+ const pagesHtml = [];
105
+ const processedPages = [];
106
+ const documentTextBlocks = [];
107
+
108
+ for (let step = 0; step < targetIndices.length; step++) {
109
+ const pageIndex = targetIndices[step];
110
+ const pageNumber = pageIndex + 1;
111
+
112
+ if (typeof options.onProgress === 'function') {
113
+ options.onProgress({
114
+ current: step + 1,
115
+ total: targetIndices.length,
116
+ pageNumber,
117
+ totalPages
118
+ });
119
+ }
120
+
121
+ const page = doc.loadPage(pageIndex);
122
+ const bounds = page.getBounds();
123
+ const widthPt = bounds[2] - bounds[0];
124
+ const heightPt = bounds[3] - bounds[1];
125
+
126
+ let bgImgTag = '';
127
+ if (withBg) {
128
+ const matrix = mupdf.Matrix.scale(scale, scale);
129
+ const pixmap = page.toPixmap(matrix, mupdf.ColorSpace.DeviceRGB, false, true);
130
+ const pngBase64 = Buffer.from(pixmap.asPNG()).toString('base64');
131
+ bgImgTag = ` <img src="data:image/png;base64,${pngBase64}" class="pdf-bg-layer" alt="Page ${pageNumber} Background" />\n`;
132
+ }
133
+
134
+ // Extract structured JSON and resolve spatial reading order
135
+ const structuredText = page.toStructuredText();
136
+ const pageJson = JSON.parse(structuredText.asJSON());
137
+ const spatialResult = resolveReadingOrder(pageJson, widthPt, heightPt);
138
+
139
+ // Build spatially ordered DOM text layer
140
+ let textLayerHtml = '';
141
+ for (const block of spatialResult.blocks) {
142
+ for (const line of (block.lines || [])) {
143
+ const fontSize = line.font?.size || line.bbox?.h || 12;
144
+ const fontFamily = line.font?.family ? `${line.font.family}, sans-serif` : 'sans-serif';
145
+ const fontWeight = line.font?.weight || 'normal';
146
+ const fontStyle = line.font?.style || 'normal';
147
+ const left = line.bbox?.x ?? line.x ?? 0;
148
+ const top = line.bbox?.y ?? (line.y - fontSize) ?? 0;
149
+ const height = line.bbox?.h ?? fontSize;
150
+
151
+ const escaped = escapeHtml(line.text || '');
152
+ textLayerHtml += ` <p class="pdf-line" style="position: absolute; left: ${left.toFixed(1)}pt; top: ${top.toFixed(1)}pt; font-size: ${fontSize.toFixed(1)}pt; font-family: ${fontFamily}; font-weight: ${fontWeight}; font-style: ${fontStyle}; height: ${height.toFixed(1)}pt; line-height: 1;">${escaped}</p>\n`;
153
+ }
154
+ }
155
+
156
+ const pageMarkup = ` <!-- Page ${pageNumber} -->
157
+ <article class="pdf-page" id="page-${pageNumber}" style="width: ${widthPt.toFixed(1)}pt; height: ${heightPt.toFixed(1)}pt;" data-page="${pageNumber}">
158
+ ${bgImgTag} <div class="pdf-text-layer">
159
+ ${textLayerHtml} </div>
160
+ </article>`;
161
+
162
+ pagesHtml.push(pageMarkup);
163
+
164
+ processedPages.push({
165
+ pageNumber,
166
+ width: widthPt,
167
+ height: heightPt,
168
+ columnsDetected: spatialResult.columnsDetected,
169
+ blocksCount: spatialResult.blocks.length,
170
+ text: spatialResult.text
171
+ });
172
+
173
+ if (spatialResult.text) {
174
+ documentTextBlocks.push(spatialResult.text);
175
+ }
176
+ }
177
+
178
+ const finalHtml = buildHtmlDocument({
179
+ title: docTitle,
180
+ pagesHtml,
181
+ options: { withBg }
182
+ });
183
+
184
+ const fullText = documentTextBlocks.join('\n\n--- Page Break ---\n\n');
185
+
186
+ return {
187
+ html: finalHtml,
188
+ text: fullText,
189
+ pages: processedPages,
190
+ totalPages,
191
+ pagesProcessed: targetIndices.length,
192
+ durationMs: Date.now() - startTime
193
+ };
194
+ }
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @file src/index.js
3
+ * @description Main library entrypoint for paper-drift.
4
+ * Exports programmatic APIs for direct Markdown conversion, PDF-to-HTML rendering,
5
+ * GFM table reconstruction, multi-column spatial reading order, and LaTeX math restoration.
6
+ * Part of paper-drift - https://github.com/Sooriya04/Paper-Drift
7
+ */
8
+
9
+ export { convertPdfToHtml, parsePageRange } from './converter.js';
10
+ export { convertPdfToMarkdown, pageToMarkdown } from './markdown/index.js';
11
+ export { reconstructTableFromLines, formatTableCell, foldHeaders } from './tables/index.js';
12
+ export { resolveReadingOrder, recursiveXyCut } from './spatial/reading-order.js';
13
+ export {
14
+ restoreLineMath,
15
+ restoreBlockMath,
16
+ formatInlineMath,
17
+ mapUnicodeToLatex,
18
+ normalizeMathIndices,
19
+ isMathFont,
20
+ containsMathSymbols,
21
+ ALL_MATH_SYMBOLS
22
+ } from './math/index.js';
23
+
24
+ import { convertPdfToHtml, parsePageRange } from './converter.js';
25
+ import { convertPdfToMarkdown, pageToMarkdown } from './markdown/index.js';
26
+ import { reconstructTableFromLines, formatTableCell, foldHeaders } from './tables/index.js';
27
+ import { resolveReadingOrder, recursiveXyCut } from './spatial/reading-order.js';
28
+ import {
29
+ restoreLineMath,
30
+ restoreBlockMath,
31
+ formatInlineMath,
32
+ mapUnicodeToLatex
33
+ } from './math/index.js';
34
+
35
+ export default {
36
+ convertPdfToHtml,
37
+ convertPdfToMarkdown,
38
+ pageToMarkdown,
39
+ reconstructTableFromLines,
40
+ formatTableCell,
41
+ foldHeaders,
42
+ parsePageRange,
43
+ resolveReadingOrder,
44
+ recursiveXyCut,
45
+ restoreLineMath,
46
+ restoreBlockMath,
47
+ formatInlineMath,
48
+ mapUnicodeToLatex
49
+ };