fix(pdf): flag scanned PDFs in pdf-to-text and serve text as UTF-8 (#603)

When a PDF has no text layer (scanned or image-only), pdf-to-text now
returns a 422 that points at the OCR tool instead of a silent empty file,
and text downloads carry charset=utf-8 so UTF-8 Arabic renders correctly
when the .txt is viewed inline.

Fixes #589
This commit is contained in:
SnapOtter
2026-07-21 16:26:43 +08:00
committed by GitHub
parent 43334324c4
commit 7d37f6e6f5
8 changed files with 109 additions and 19 deletions
+6 -2
View File
@@ -227,7 +227,7 @@ export async function fileRoutes(app: FastifyInstance): Promise<void> {
});
}
function getContentType(ext: string): string {
export function getContentType(ext: string): string {
const map: Record<string, string> = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
@@ -303,5 +303,9 @@ function getContentType(ext: string): string {
heic: "image/heic",
heif: "image/heif",
};
return map[ext] ?? "application/octet-stream";
const type = map[ext] ?? "application/octet-stream";
// Text payloads (extracted text, markdown, CSV, subtitles) are written UTF-8.
// Without an explicit charset a browser can sniff a legacy encoding and
// mojibake non-Latin scripts like Arabic when the file is viewed inline (#589).
return type.startsWith("text/") ? `${type}; charset=utf-8` : type;
}
+10
View File
@@ -3,6 +3,7 @@ import { join } from "node:path";
import { pdfTextPy } from "@snapotter/doc-engine";
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { InputValidationError } from "../../modality/contract.js";
import { createToolRoute } from "../tool-factory.js";
const settingsSchema = z.object({});
@@ -23,6 +24,15 @@ export function registerPdfToText(app: FastifyInstance) {
const outPath = join(ctx.scratchDir, `${base}.txt`);
ctx.report(10, "Extracting text");
const result = await pdfTextPy(inPath, outPath);
// A scanned or image-only PDF has no text layer, so extraction returns an
// empty file. Rather than hand back a silent 0-byte "success", tell the
// user to run OCR instead (#589).
if (!result.hasText) {
throw new InputValidationError(
"This PDF has no extractable text layer. It looks scanned or image-only, so use the PDF OCR tool to read its text.",
422,
);
}
ctx.report(90, "Done");
return {