pi-quiver 5.5.0 → 6.0.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/CHANGELOG.md +7 -0
- package/README.md +8 -9
- package/dist/bin/pi-quiver.js +121 -56
- package/extensions/doc_to_md.ts +1 -1
- package/lib/doc-to-md-bundle.ts +46 -25
- package/lib/doc-to-md-core.ts +75 -38
- package/lib/doc-to-md-handle.ts +14 -3
- package/lib/doc-to-md-options.ts +3 -5
- package/package.json +1 -1
- package/scripts/doc_to_md.py +272 -88
package/lib/doc-to-md-core.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type ChildProcess, spawn } from "node:child_process";
|
|
|
12
12
|
import { homedir, tmpdir } from "node:os";
|
|
13
13
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
-
import { type Bundle, abortBundle, commitBundle, openBundle, publishSheetImages, publishStaged,
|
|
15
|
+
import { type Bundle, abortBundle, commitBundle, openBundle, publishSheetCsvs, publishSheetImages, publishStaged, rewriteLinks, tempBundleRoot, validateImageLinks } from "./doc-to-md-bundle.ts";
|
|
16
16
|
import { type Engine, type HandleData, type InfoData, type Tier, formatHandle, formatInfoHandle, scanOutline, type SheetInfo, type TocEntry } from "./doc-to-md-handle.ts";
|
|
17
17
|
import { type DocToMdOptions, type InputType, TUNABLE_DEFAULTS, classifyInput, sanitizeStem } from "./doc-to-md-options.ts";
|
|
18
18
|
|
|
@@ -28,6 +28,7 @@ export const VENV_DIR_NAME = "doc-to-md-venv-v2";
|
|
|
28
28
|
export const LEGACY_VENV_DIR_NAME = "pymupdf-venv";
|
|
29
29
|
const STDERR_CAP = 1_000_000;
|
|
30
30
|
export const OUTPUT_MAX_BYTES = 20_000_000;
|
|
31
|
+
export const EXCEL_PDF_FILTER = 'pdf:calc_pdf_Export:{"SinglePageSheets":{"type":"boolean","value":"true"}}';
|
|
31
32
|
|
|
32
33
|
export interface BackendConfig { pymupdfVersion: string; warmTimeoutMs: number; }
|
|
33
34
|
|
|
@@ -59,12 +60,12 @@ export function uvChildArgs(cfg: BackendConfig, script: string, mode: string): s
|
|
|
59
60
|
|
|
60
61
|
export function pythonChildArgs(script: string, mode: string): string[] { return [script, mode]; }
|
|
61
62
|
|
|
62
|
-
export function soffArgs(src: string, profileDir: string, outDir: string): string[] {
|
|
63
|
+
export function soffArgs(src: string, profileDir: string, outDir: string, filter = "pdf"): string[] {
|
|
63
64
|
return [
|
|
64
65
|
"--headless", "--invisible", "--nocrashreport", "--nodefault", "--nofirststartwizard",
|
|
65
66
|
"--nolockcheck", "--nologo", "--norestore", "--quickstart=no",
|
|
66
67
|
`-env:UserInstallation=${pathToFileURL(profileDir).href}`,
|
|
67
|
-
"--convert-to",
|
|
68
|
+
"--convert-to", filter, "--outdir", outDir, src,
|
|
68
69
|
];
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -367,37 +368,37 @@ export function resetBackendCacheForTests(): void {
|
|
|
367
368
|
backendPromise = null;
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
export
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
): Promise<{ pdfPath: string; cleanup: () => void }> {
|
|
371
|
+
export type OfficeResult =
|
|
372
|
+
| { ok: true; pdfPath: string; cleanup: () => void }
|
|
373
|
+
| { ok: false; kind: "missing" | "timeout" | "exit" | "no-pdf"; code: number | null; timedOut: boolean; stderr: string };
|
|
374
|
+
|
|
375
|
+
export async function tryConvertOffice(sofficeTimeoutMs: number, src: string, signal?: AbortSignal, run: RunFn = runCapped, filter = "pdf"): Promise<OfficeResult> {
|
|
376
376
|
const profileDir = mkdtempSync(join(tmpdir(), "pi-doc-soffice-prof-"));
|
|
377
377
|
const outDir = mkdtempSync(join(tmpdir(), "pi-doc-soffice-out-"));
|
|
378
|
-
const cleanup = () => {
|
|
379
|
-
for (const d of [profileDir, outDir]) {
|
|
380
|
-
try { rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
381
|
-
}
|
|
382
|
-
};
|
|
378
|
+
const cleanup = () => { for (const d of [profileDir, outDir]) try { rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } };
|
|
383
379
|
try {
|
|
384
380
|
const env = { ...process.env, SAL_USE_VCLPLUGIN: "svp", OOO_DISABLE_RECOVERY: "1", SAL_NO_MOUSEGRABS: "1" };
|
|
385
|
-
const r = await run("soffice", soffArgs(src, profileDir, outDir), { timeoutMs: sofficeTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env, signal });
|
|
381
|
+
const r = await run("soffice", soffArgs(src, profileDir, outDir, filter), { timeoutMs: sofficeTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env, signal });
|
|
386
382
|
if (signal?.aborted) throw new Error("aborted");
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (r.
|
|
391
|
-
const
|
|
392
|
-
const pdfPath = join(outDir, `${base}.pdf`);
|
|
383
|
+
const fail = (kind: "missing" | "timeout" | "exit" | "no-pdf"): OfficeResult => { cleanup(); return { ok: false, kind, code: r.code, timedOut: r.timedOut, stderr: r.stderr.slice(0, 500) }; };
|
|
384
|
+
if (r.code === null && !r.timedOut) return fail("missing");
|
|
385
|
+
if (r.timedOut) return fail("timeout");
|
|
386
|
+
if (r.code !== 0) return fail("exit");
|
|
387
|
+
const pdfPath = join(outDir, `${basename(src).replace(/\.[^.]+$/, "")}.pdf`);
|
|
393
388
|
const st = statSync(pdfPath, { throwIfNoEntry: false });
|
|
394
|
-
if (!st || !st.isFile() || st.size === 0)
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
return { pdfPath, cleanup };
|
|
389
|
+
if (!st || !st.isFile() || st.size === 0) return fail("no-pdf");
|
|
390
|
+
return { ok: true, pdfPath, cleanup };
|
|
398
391
|
} catch (e) { cleanup(); throw e; }
|
|
399
392
|
}
|
|
400
393
|
|
|
394
|
+
export async function convertOffice(sofficeTimeoutMs: number, src: string, signal?: AbortSignal, run: RunFn = runCapped): Promise<{ pdfPath: string; cleanup: () => void }> {
|
|
395
|
+
const r = await tryConvertOffice(sofficeTimeoutMs, src, signal, run);
|
|
396
|
+
if (r.ok) return r;
|
|
397
|
+
if (r.kind === "missing") throw new Error("LibreOffice (soffice) is required to convert .docx/.pptx but was not found on PATH. Install LibreOffice or convert the file to PDF first.");
|
|
398
|
+
if (r.kind === "no-pdf") throw new Error("LibreOffice (soffice) ran but produced no usable PDF for this file. Ensure LibreOffice can open the document, or convert it to PDF manually first.");
|
|
399
|
+
throw new Error(`soffice failed (code=${r.code} timedOut=${r.timedOut}): ${r.stderr}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
401
402
|
|
|
402
403
|
// --- Conversion tiers and bundle orchestration ---
|
|
403
404
|
|
|
@@ -405,13 +406,14 @@ export const DEGRADED_TEXT = "PyMuPDF text extraction - layout/tables not preser
|
|
|
405
406
|
export const DEGRADED_UNPDF = "unpdf text extraction - structure not preserved";
|
|
406
407
|
export const EXCEL_REMEDY = "Remedy: install uv, or pip install openpyxl xlrd pillow";
|
|
407
408
|
|
|
408
|
-
export type Mode = "info" | "pdf-primary" | "pdf-fallback" | "xlsx" | "pdf-text";
|
|
409
|
-
export interface TierJson { markdown?: string; pages?: number[]; pageCount?: number; emptyPages?: number[]; failedPages?: { page: number; error: string }[]; notes?: string[]; images?: { sheetIndex: number; file: string }[]; metadata?: Record<string, string>; toc?: [number, string, number][]; sheets?: SheetInfo[]; }
|
|
409
|
+
export type Mode = "info" | "pdf-primary" | "pdf-fallback" | "xlsx" | "pdf-text" | "render-pages";
|
|
410
|
+
export interface TierJson { markdown?: string; pages?: number[]; pageCount?: number; emptyPages?: number[]; failedPages?: { page: number; error: string }[]; notes?: string[]; images?: { sheetIndex: number; file: string }[]; metadata?: Record<string, string>; toc?: [number, string, number][]; sheets?: SheetInfo[]; renderPages?: number[]; sheetCount?: number; ok?: boolean; reason?: string; rendered?: { idx: number; file: string; dpi: number }[]; failed?: { idx: number; reason: string }[]; }
|
|
410
411
|
export type TierResult = { ok: true; json: TierJson } | { ok: false; reason: string; detail?: string } | { ok: false; userError: string; pageCount?: number };
|
|
411
412
|
|
|
412
413
|
export interface PipelineSeams {
|
|
413
414
|
backend: (cfg: BackendConfig) => Promise<Backend>;
|
|
414
415
|
runTier: (mode: Mode, childOptions: Record<string, unknown>, bundle: Pick<Bundle, "stagingDir">, signal: AbortSignal | undefined, timeoutMs: number, backend: Backend) => Promise<TierResult>;
|
|
416
|
+
office: typeof tryConvertOffice;
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
export function resolveUnpdfWorker(candidates: string[], exists: (path: string) => boolean): string {
|
|
@@ -463,8 +465,19 @@ function detailSuffix(r: Extract<TierResult, { ok: false; reason: string }>): st
|
|
|
463
465
|
return r.detail ? ` (${r.detail})` : "";
|
|
464
466
|
}
|
|
465
467
|
|
|
468
|
+
export function reconcileRenderMarkers(md: string, renderPages: number[], fmt: string, sourceMap: Map<string, string>, reason: (idx: number) => string): string {
|
|
469
|
+
for (const idx of renderPages) {
|
|
470
|
+
const file = `s${idx}.${fmt}`;
|
|
471
|
+
const ok = sourceMap.has(file);
|
|
472
|
+
md = md.replace(`<!--rv:${idx}-->`, ok ? `Rendered view: ` : `Rendered view: unavailable (${reason(idx)})`);
|
|
473
|
+
md = md.replace(`<!--rvs:${idx}-->`, ok ? "yes" : "no");
|
|
474
|
+
}
|
|
475
|
+
if (/<!--rvs?:\d+-->/.test(md)) throw new Error("internal: unresolved render marker");
|
|
476
|
+
return md;
|
|
477
|
+
}
|
|
478
|
+
|
|
466
479
|
export async function convertDocument(o: DocToMdOptions, signal?: AbortSignal, seams?: Partial<PipelineSeams>): Promise<ConvertOutcome> {
|
|
467
|
-
const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, ...seams };
|
|
480
|
+
const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, office: tryConvertOffice, ...seams };
|
|
468
481
|
const inputPath = resolve(o.path);
|
|
469
482
|
const st = statSync(inputPath, { throwIfNoEntry: false });
|
|
470
483
|
if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
|
|
@@ -479,17 +492,40 @@ export async function convertDocument(o: DocToMdOptions, signal?: AbortSignal, s
|
|
|
479
492
|
try {
|
|
480
493
|
let pdfPath = inputPath;
|
|
481
494
|
if (type === "docx" || type === "pptx") { office = await convertOffice(o.sofficeTimeoutMs, inputPath, signal); pdfPath = office.pdfPath; }
|
|
482
|
-
const base = { path: pdfPath, pages: o.pages, stagingDir: b.stagingDir,
|
|
495
|
+
const base = { path: pdfPath, pages: o.pages, stagingDir: b.stagingDir, sheetsStagingDir: b.sheetsStagingDir, imageDpi: o.imageDpi, imageFormat: o.imageFormat, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion };
|
|
483
496
|
let tier: Tier, engine: Engine, json: TierJson, degraded: string | null = null, fallbackReason: string | null = null;
|
|
497
|
+
let notes: string[] = [];
|
|
484
498
|
if (isExcel) {
|
|
485
499
|
const r = await s.runTier("xlsx", base, b, signal, o.excelTimeoutMs, backend);
|
|
486
500
|
if (!r.ok) {
|
|
487
501
|
if ("userError" in r) throw new Error(r.userError);
|
|
488
|
-
const remedy = r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes" ? ". Remedy: raise excelTimeoutMs
|
|
502
|
+
const remedy = r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes" ? ". Remedy: raise excelTimeoutMs" : "";
|
|
489
503
|
throw new Error(`Excel conversion failed: ${r.reason}${detailSuffix(r)}${remedy}`);
|
|
490
504
|
}
|
|
491
|
-
publishSheetImages(b);
|
|
492
|
-
tier = "excel"; engine = type === "xls" ? "xlrd" : "openpyxl"; json = r.json;
|
|
505
|
+
publishSheetImages(b); publishSheetCsvs(b);
|
|
506
|
+
tier = "excel"; engine = type === "xls" ? "xlrd" : "openpyxl"; json = r.json; notes = [...(json.notes ?? [])];
|
|
507
|
+
const renderPages = json.renderPages ?? [];
|
|
508
|
+
let skip: string | null = null;
|
|
509
|
+
const perSheet = new Map<number, string>();
|
|
510
|
+
if (renderPages.length) {
|
|
511
|
+
const off = await s.office(o.sofficeTimeoutMs, inputPath, signal, runCapped, EXCEL_PDF_FILTER);
|
|
512
|
+
if (!off.ok) skip = off.kind === "missing" ? "LibreOffice not found" : off.kind === "timeout" ? `soffice failed: timeout after ${o.sofficeTimeoutMs}ms` : off.kind === "exit" ? `soffice failed: exit ${off.code}` : "soffice produced no PDF";
|
|
513
|
+
else {
|
|
514
|
+
try {
|
|
515
|
+
const rp = await s.runTier("render-pages", { path: off.pdfPath, sheetIndices: renderPages, expectedPages: json.sheetCount, imageDpi: o.imageDpi, imageFormat: o.imageFormat, stagingDir: b.stagingDir, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion }, b, signal, o.fallbackTimeoutMs, backend);
|
|
516
|
+
if (!rp.ok) skip = `render failed: ${"userError" in rp ? rp.userError : rp.reason}`;
|
|
517
|
+
else if (rp.json.ok === false) skip = rp.json.reason ?? "render failed";
|
|
518
|
+
else {
|
|
519
|
+
publishSheetImages(b);
|
|
520
|
+
for (const f of rp.json.failed ?? []) perSheet.set(f.idx, f.reason);
|
|
521
|
+
for (const d of rp.json.rendered ?? []) if (d.dpi < o.imageDpi) notes.push(`Rendered view s${d.idx}: rendered at ${d.dpi} dpi`);
|
|
522
|
+
}
|
|
523
|
+
} finally { off.cleanup(); }
|
|
524
|
+
}
|
|
525
|
+
if (skip) notes.push(`Rendered views skipped: ${skip}`);
|
|
526
|
+
else if (perSheet.size) notes.push(`Rendered views: ${perSheet.size} of ${renderPages.length} unavailable`);
|
|
527
|
+
}
|
|
528
|
+
json = { ...json, markdown: reconcileRenderMarkers(json.markdown ?? "", renderPages, o.imageFormat, b.sourceMap, (idx) => perSheet.get(idx) ?? skip ?? "render failed") };
|
|
493
529
|
} else if (backend.kind === "none") {
|
|
494
530
|
const r = await s.runTier("pdf-text", base, b, signal, o.primaryTimeoutMs, backend);
|
|
495
531
|
if (!r.ok) throw new Error("userError" in r ? r.userError : `Conversion failed: unpdf ${r.reason}${detailSuffix(r)}`);
|
|
@@ -508,25 +544,26 @@ export async function convertDocument(o: DocToMdOptions, signal?: AbortSignal, s
|
|
|
508
544
|
tier = "fallback"; engine = "pymupdf-text"; json = f.json; degraded = DEGRADED_TEXT; fallbackReason = `primary ${p.reason}`;
|
|
509
545
|
}
|
|
510
546
|
}
|
|
511
|
-
|
|
512
|
-
|
|
547
|
+
if (!isExcel) notes = json.notes ?? [];
|
|
548
|
+
const body = rewriteLinks(json.markdown ?? "", b.sourceMap);
|
|
549
|
+
validateImageLinks(body, b.manifest, b.csvManifest);
|
|
513
550
|
const head: string[] = [];
|
|
514
551
|
if (degraded) head.push(`Degraded: ${degraded}`);
|
|
515
552
|
if (fallbackReason) head.push(`Fallback-Reason: ${fallbackReason}`);
|
|
516
553
|
if (json.failedPages?.length) head.push(`Failed pages: ${json.failedPages.map((f) => `${f.page} (${f.error})`).join("; ")}`);
|
|
517
554
|
if (json.emptyPages?.length) head.push(`Empty pages: ${json.emptyPages.join(", ")}`);
|
|
518
|
-
for (const n of
|
|
555
|
+
for (const n of notes) head.push(`Notes: ${n}`);
|
|
519
556
|
const markdown = (head.length ? `${head.join("\n")}\n\n` : "") + body;
|
|
520
557
|
commitBundle(b, markdown);
|
|
521
558
|
const outline = scanOutline(markdown, o.outlineMaxEntries);
|
|
522
|
-
const details: DocToMdDetails = { path: inputPath, backend: backend.kind, pymupdfVersion: o.pymupdfVersion, inputType: type, file: b.mdPath, outputDir: b.root, savedTo: b.mdPath, imagesDir: b.imagesDir, type, engine, tier, pageCount: json.pageCount ?? null, pages: o.pages, imageCount: b.manifest.size, bytes: Buffer.byteLength(markdown, "utf8"), lines: markdown.split("\n").length, degraded, fallbackReason, failedPages: (json.failedPages ?? []).map((f) => f.page), emptyPages: json.emptyPages ?? [], notes
|
|
559
|
+
const details: DocToMdDetails = { path: inputPath, backend: backend.kind, pymupdfVersion: o.pymupdfVersion, inputType: type, file: b.mdPath, outputDir: b.root, savedTo: b.mdPath, imagesDir: b.imagesDir, sheetsDir: b.csvManifest.size ? b.sheetsDir : null, type, engine, tier, pageCount: json.pageCount ?? null, pages: o.pages, imageCount: b.manifest.size, bytes: Buffer.byteLength(markdown, "utf8"), lines: markdown.split("\n").length, degraded, fallbackReason, failedPages: (json.failedPages ?? []).map((f) => f.page), emptyPages: json.emptyPages ?? [], notes, outline: outline.entries, outlineTotal: outline.total };
|
|
523
560
|
return { output: formatHandle(details), details };
|
|
524
561
|
} catch (e) { abortBundle(b); throw e; }
|
|
525
562
|
finally { office?.cleanup(); }
|
|
526
563
|
}
|
|
527
564
|
|
|
528
565
|
export async function inspectDocument(o: DocToMdOptions, signal?: AbortSignal, seams?: Partial<PipelineSeams>): Promise<{ output: string; details: InfoData }> {
|
|
529
|
-
const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, ...seams };
|
|
566
|
+
const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, office: tryConvertOffice, ...seams };
|
|
530
567
|
const inputPath = resolve(o.path);
|
|
531
568
|
const st = statSync(inputPath, { throwIfNoEntry: false });
|
|
532
569
|
if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
|
|
@@ -541,7 +578,7 @@ export async function inspectDocument(o: DocToMdOptions, signal?: AbortSignal, s
|
|
|
541
578
|
const r = await s.runTier("info", { path, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion }, { stagingDir: "" }, signal, isExcel ? o.excelTimeoutMs : o.fallbackTimeoutMs, backend);
|
|
542
579
|
if (!r.ok) {
|
|
543
580
|
if ("userError" in r) throw new Error(r.userError);
|
|
544
|
-
if (isExcel && (r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes")) throw new Error(`Excel inspection failed: ${r.reason}. Remedy: raise excelTimeoutMs
|
|
581
|
+
if (isExcel && (r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes")) throw new Error(`Excel inspection failed: ${r.reason}. Remedy: raise excelTimeoutMs${detailSuffix(r)}`);
|
|
545
582
|
throw new Error(`Inspection failed: ${r.reason}${detailSuffix(r)}`);
|
|
546
583
|
}
|
|
547
584
|
const toc: TocEntry[] = (r.json.toc ?? []).map(([level, title, page]) => ({ level, title, page }));
|
package/lib/doc-to-md-handle.ts
CHANGED
|
@@ -6,10 +6,20 @@ export type BackendKind = "uv" | "python" | "venv" | "none";
|
|
|
6
6
|
|
|
7
7
|
export interface OutlineEntry { line: number; level: number; title: string; }
|
|
8
8
|
export interface TocEntry { level: number; title: string; page: number; }
|
|
9
|
-
export interface SheetInfo {
|
|
9
|
+
export interface SheetInfo {
|
|
10
|
+
index: number;
|
|
11
|
+
name: string;
|
|
12
|
+
kind: "worksheet" | "chartsheet";
|
|
13
|
+
hidden: boolean;
|
|
14
|
+
rows: number | null; cols: number | null;
|
|
15
|
+
hiddenRows: number; hiddenCols: number;
|
|
16
|
+
charts: number; images: number;
|
|
17
|
+
rendered: boolean;
|
|
18
|
+
csv: string | null;
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
export interface HandleData {
|
|
12
|
-
savedTo: string; imagesDir: string | null; type: InputType; engine: Engine; tier: Tier;
|
|
22
|
+
savedTo: string; imagesDir: string | null; sheetsDir: string | null; type: InputType; engine: Engine; tier: Tier;
|
|
13
23
|
pageCount: number | null; pages: number[] | null; imageCount: number; bytes: number; lines: number;
|
|
14
24
|
degraded: string | null; fallbackReason: string | null; failedPages: number[]; emptyPages: number[];
|
|
15
25
|
notes: string[]; outline: OutlineEntry[]; outlineTotal: number;
|
|
@@ -73,6 +83,7 @@ function outlineLines(entries: OutlineEntry[], total: number): string[] {
|
|
|
73
83
|
export function formatHandle(h: HandleData): string {
|
|
74
84
|
const lines = [`Saved-To: ${h.savedTo}`];
|
|
75
85
|
if (h.imagesDir && h.imageCount > 0) lines.push(`Images-Dir: ${h.imagesDir}`);
|
|
86
|
+
if (h.sheetsDir) lines.push(`Sheets-Dir: ${h.sheetsDir}`);
|
|
76
87
|
lines.push(`Type: ${h.type} Engine: ${h.engine} Tier: ${h.tier}`);
|
|
77
88
|
lines.push(`Page-Count: ${h.pageCount ?? "?"} Pages: ${h.pages ? compactRanges(h.pages) : "all"} Images: ${h.imageCount} Size: ${formatSize(h.bytes)} / ${h.lines} lines`);
|
|
78
89
|
if (h.degraded) lines.push(`Degraded: ${h.degraded}`);
|
|
@@ -90,7 +101,7 @@ export function formatInfoHandle(i: InfoData, max: number): string {
|
|
|
90
101
|
if (i.sheets) {
|
|
91
102
|
const lines = [`Type: ${i.type} Sheets: ${i.sheetsTotal}`];
|
|
92
103
|
for (const s of i.sheets.slice(0, max)) {
|
|
93
|
-
const dims =
|
|
104
|
+
const dims = `${s.kind} rows=${s.rows ?? "-"} cols=${s.cols ?? "-"} charts=${s.charts} images=${s.images}`;
|
|
94
105
|
const hidden = s.hiddenRows || s.hiddenCols ? ` hiddenRows=${s.hiddenRows} hiddenCols=${s.hiddenCols}` : "";
|
|
95
106
|
lines.push(` ${trunc(s.name, TITLE_MAX)} ${s.hidden ? "hidden " : ""}${dims}${hidden}`);
|
|
96
107
|
}
|
package/lib/doc-to-md-options.ts
CHANGED
|
@@ -16,7 +16,6 @@ export interface Tunables {
|
|
|
16
16
|
pymupdfVersion: string;
|
|
17
17
|
imageDpi: number;
|
|
18
18
|
imageFormat: ImageFormat;
|
|
19
|
-
maxCellsPerSheet: number;
|
|
20
19
|
maxOutputBytes: number;
|
|
21
20
|
outlineMaxEntries: number;
|
|
22
21
|
}
|
|
@@ -63,14 +62,13 @@ export const DOC_TO_MD_OPTIONS: readonly OptionDescriptor[] = [
|
|
|
63
62
|
{ key: "outputDir", flag: "--output-dir", type: "string", default: null, settable: false, help: "Bundle root for <stem>.md + images/; default a per-call temp dir" },
|
|
64
63
|
{ key: "overwrite", flag: "--overwrite", type: "bool", default: false, settable: false, help: "Replace an existing completed <stem>.md bundle" },
|
|
65
64
|
{ key: "primaryTimeoutMs", flag: "--primary-timeout", type: "int", default: 60000, settable: true, env: "PI_DOC_TO_MD_CONVERT_TIMEOUT_MS", help: "pymupdf4llm tier; also the unpdf tier" },
|
|
66
|
-
{ key: "fallbackTimeoutMs", flag: "--fallback-timeout", type: "int", default: 30000, settable: true, help: "PyMuPDF get_text tier; also info on PDF" },
|
|
67
|
-
{ key: "sofficeTimeoutMs", flag: "--soffice-timeout", type: "int", default: 120000, settable: true, env: "PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", help: "DOCX/PPTX -> PDF via LibreOffice" },
|
|
65
|
+
{ key: "fallbackTimeoutMs", flag: "--fallback-timeout", type: "int", default: 30000, settable: true, help: "PyMuPDF get_text tier; also info on PDF and Excel rendered views" },
|
|
66
|
+
{ key: "sofficeTimeoutMs", flag: "--soffice-timeout", type: "int", default: 120000, settable: true, env: "PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", help: "DOCX/PPTX -> PDF via LibreOffice; also Excel rendered views" },
|
|
68
67
|
{ key: "excelTimeoutMs", flag: "--excel-timeout", type: "int", default: 60000, settable: true, help: "Excel child (both openpyxl loads); also info on Excel" },
|
|
69
68
|
{ key: "warmTimeoutMs", flag: "--warm-timeout", type: "int", default: 120000, settable: true, env: "PI_DOC_TO_MD_WARM_TIMEOUT_MS", help: "Absolute backend discovery/bootstrap deadline (first call per process)" },
|
|
70
69
|
{ key: "pymupdfVersion", flag: "--pymupdf-version", type: "version", default: "1.27.2.3", settable: true, env: "PI_DOC_TO_MD_PYMUPDF_VERSION", help: "pymupdf4llm pin (>= 1.27.0)" },
|
|
71
|
-
{ key: "imageDpi", flag: "--image-dpi", type: "int", default: 150, settable: true, help: "Render DPI for page images" },
|
|
70
|
+
{ key: "imageDpi", flag: "--image-dpi", type: "int", default: 150, settable: true, help: "Render DPI for page images and Excel rendered views" },
|
|
72
71
|
{ key: "imageFormat", flag: "--image-format", type: "enum", default: "png", settable: true, enumValues: ["png", "jpg"], help: "Rendered image format (embedded images keep their native extension)" },
|
|
73
|
-
{ key: "maxCellsPerSheet", flag: "--max-cells-per-sheet", type: "int", default: 50000, settable: true, help: "rows x cols budget per worksheet" },
|
|
74
72
|
{ key: "maxOutputBytes", flag: "--max-output-bytes", type: "int", default: 20000000, settable: true, help: "Child stdout cap in bytes" },
|
|
75
73
|
{ key: "outlineMaxEntries", flag: "--outline-max-entries", type: "int", default: 40, settable: true, help: "Heading outline / TOC / sheet inventory cap in the handle" },
|
|
76
74
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quiver",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|