"""Behavioral checks for OCR quality gates and non-destructive PDF operations. Uses the existing pypdf, ReportLab and Pillow dependencies. Browser, OCR-model and Office integration checks run separately against real installed engines. """ from __future__ import annotations import hashlib import json import sys import tempfile import unittest from pathlib import Path from types import SimpleNamespace from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from PIL import Image from pypdf import PdfReader, PdfWriter from pypdf.generic import ArrayObject, DictionaryObject, NameObject, TextStringObject from reportlab.pdfgen import canvas import _pdf_common as common import create_design_pdf import edit_pdf import ocr_text import render_pdf class PDFWorkflows(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory(prefix="pdf-regression-") self.root = Path(self.directory.name).resolve() self.root_patch = patch.object(common, "PDF_OUTPUT_ROOT", self.root) self.root_patch.start() self.source = self.root / "source.pdf" c = canvas.Canvas(str(self.source), pagesize=(600, 800)) c.drawString(30, 750, "Original content 123") form = c.acroForm form.textfield(name="name", x=30, y=650, width=200, height=25, maxlen=40) form.checkbox(name="agree", x=30, y=600, checked=False) form.radio(name="mode", value="A", x=30, y=550, selected=True) form.radio(name="mode", value="B", x=80, y=550, selected=False) form.choice(name="country", value="CN", options=["CN", "US"], x=30, y=480, width=150, height=30) image = Image.new("RGB", (24, 16), "#8a3a2a") c.drawInlineImage(image, 350, 500, width=96, height=64) c.showPage() c.save() self.original_hash = hashlib.sha256(self.source.read_bytes()).hexdigest() def tearDown(self): self.assertEqual(hashlib.sha256(self.source.read_bytes()).hexdigest(), self.original_hash) self.root_patch.stop() self.directory.cleanup() def args(self, operation, **values): return SimpleNamespace(operation=operation, input=str(self.source), output=str(self.root / "result.pdf"), overwrite=False, data=None, data_file=None, pages=None, **values) def widgets(self, document: PdfReader | PdfWriter) -> list[DictionaryObject]: annotations = document.pages[0]["/Annots"] assert isinstance(annotations, ArrayObject) widgets = [] for reference in annotations: widget = reference.get_object() assert isinstance(widget, DictionaryObject) widgets.append(widget) return widgets def test_form_inventory_and_roundtrip_all_supported_types(self): fields = edit_pdf.execute(self.args("form-info", offset=0, limit=50)) infos = {field["id"]: field for field in fields["fields"]} self.assertEqual(infos["agree"]["type"], "checkbox") self.assertEqual(set(infos["agree"]["states"]), {"/Off", "/Yes"}) self.assertEqual(set(infos["mode"]["states"]), {"/A", "/B"}) self.assertFalse(fields["has_xfa"]) args = self.args("form-fill") args.data = json.dumps({"name": "Alice 123", "agree": True, "mode": "B", "country": "US"}) result = edit_pdf.execute(args) updated = PdfReader(result["path"]) fields = updated.get_fields() assert fields is not None self.assertEqual(fields["name"]["/V"], "Alice 123") self.assertEqual(fields["agree"]["/V"], "/Yes") self.assertEqual(fields["mode"]["/V"], "/B") self.assertEqual(fields["country"]["/V"], "US") widgets = self.widgets(updated) checkbox = next(widget for widget in widgets if widget.get("/T") == "agree") self.assertEqual(checkbox["/AS"], "/Yes") radio = [widget for widget in widgets if widget.get("/Parent")] self.assertEqual(sorted(str(widget["/AS"]) for widget in radio), ["/B", "/Off"]) self.assertIn("Original content 123", updated.pages[0].extract_text()) def test_checkbox_false_is_off(self): args = self.args("form-fill") args.data = '{"agree":false}' result = edit_pdf.execute(args) fields = PdfReader(result["path"]).get_fields() assert fields is not None self.assertEqual(fields["agree"]["/V"], "/Off") def test_invalid_form_input_does_not_publish(self): for data in [{"missing":"x"}, {"agree":"false"}, {"mode":True}, {"mode":"Off"}, {"country":"ZZ"}, {"country":["CN","US"]}, {"name":"x"*41}]: with self.subTest(data=data): args = self.args("form-fill") args.data = json.dumps(data) with self.assertRaises(ValueError): edit_pdf.execute(args) self.assertFalse(Path(args.output).exists()) def test_form_inventory_handles_indirect_appearance(self): writer = PdfWriter(clone_from=self.source) for widget in self.widgets(writer): if "/AP" in widget: widget[NameObject("/AP")] = writer._add_object(widget["/AP"]) alternative = self.root / "indirect.pdf" writer.write(alternative) args = self.args("form-info", offset=0, limit=50) args.input = str(alternative) result = edit_pdf.execute(args) self.assertEqual(result["field_count"], 4) def test_xfa_rejected_for_fill(self): writer = PdfWriter(clone_from=self.source) acroform = writer.root_object["/AcroForm"] assert isinstance(acroform, DictionaryObject) acroform[NameObject("/XFA")] = TextStringObject("unsupported") alternative = self.root / "xfa.pdf" writer.write(alternative) args = self.args("form-fill") args.input = str(alternative) args.data = '{"name":"Alice"}' with self.assertRaisesRegex(ValueError, "XFA"): edit_pdf.execute(args) self.assertFalse(Path(args.output).exists()) def test_crop_keeps_content_and_forms(self): result = edit_pdf.execute(self.args("crop", box="50,50,500,700")) reader = PdfReader(result["path"]) self.assertEqual(list(reader.pages[0].cropbox), [50, 50, 500, 700]) self.assertIn("Original content 123", reader.pages[0].extract_text()) self.assertEqual(len(reader.get_fields() or {}), 4) def test_crop_rejects_out_of_bounds_and_nonfinite_values(self): for box in ["-1,0,300,400", "0,0,601,800", "10,0,0,20", "0,0,nan,20"]: with self.subTest(box=box), self.assertRaises(ValueError): edit_pdf.execute(self.args("crop", box=box)) self.assertFalse((self.root / "result.pdf").exists()) def test_metadata_preserves_forms_and_unspecified_fields(self): original = PdfReader(self.source).metadata args = self.args("metadata") args.data = '{"Title":"中文报告","Author":"Test"}' result = edit_pdf.execute(args) reader = PdfReader(result["path"]) metadata = reader.metadata assert metadata is not None and original is not None self.assertEqual(metadata.title, "中文报告") self.assertEqual(metadata.producer, original.producer) self.assertEqual(len(reader.get_fields() or {}), 4) def test_embedded_image_has_original_dimensions(self): args = self.args("extract-images", output_dir=str(self.root / "images"), start_image=0, max_images=20) result = edit_pdf.execute(args) self.assertEqual(len(result["images"]), 1) with Image.open(result["images"][0]["path"]) as embedded: self.assertEqual(embedded.size, (24, 16)) def test_existing_output_is_preserved_on_failure(self): target = self.root / "result.pdf" target.write_bytes(b"previous result") args = self.args("metadata") args.overwrite = True args.data = '{"Unsupported":"value"}' with self.assertRaises(ValueError): edit_pdf.execute(args) self.assertEqual(target.read_bytes(), b"previous result") def test_output_cannot_replace_source(self): args = self.args("crop", box="0,0,100,100") args.output = str(self.source) args.overwrite = True with self.assertRaises(ValueError): edit_pdf.execute(args) def test_output_directory_boundary(self): args = self.args("crop", box="0,0,100,100") args.output = str(self.root.parent / "outside.pdf") with self.assertRaises(ValueError): edit_pdf.execute(args) def test_missing_cjk_maps_rejects_incomplete_preview(self): destination = self.root / 'preview' args = render_pdf._parse_args(['--input', str(self.source), '--output-dir', str(destination)]) completed = SimpleNamespace(returncode=0, stdout='', stderr="Syntax Error: Missing language pack for 'Adobe-GB1' mapping") with patch.object(render_pdf.shutil, 'which', return_value=sys.executable), patch.object(render_pdf.subprocess, 'run', return_value=completed): with self.assertRaisesRegex(RuntimeError, 'poppler-data'): render_pdf._render(args) self.assertEqual(list(destination.glob('*.png')), []) class OCRQuality(unittest.TestCase): def recognize(self, texts, scores): boxes = [[[0, i*30], [100, i*30], [100, i*30+20], [0, i*30+20]] for i in range(len(texts))] def engine(_): return SimpleNamespace(txts=texts, scores=scores, boxes=boxes) return ocr_text._ocr_page(engine, Path("fixture.png")) def test_mixed_confidence_does_not_certify_uncertain_amount(self): result = self.recognize(["Reliable document heading and text", "9999.99"], [.99, .3]) self.assertEqual(result["status"], "good") self.assertNotIn("9999.99", result["text"]) self.assertTrue(result["needs_review"]) self.assertEqual(result["review_regions"][0]["text_candidate"], "9999.99") self.assertIsNotNone(result["review_regions"][0]["box"]) def test_unusable_page_is_not_returned_as_reliable_text(self): for texts, scores in [(["uncertain document"], [.3]), (["Hi"], [.99]), ([], [])]: with self.subTest(texts=texts): result = self.recognize(texts, scores) self.assertEqual(result["text"], "") self.assertFalse(result["usable_for_summary"]) self.assertTrue(result["needs_review"]) def test_good_ocr_needs_no_model_assistance(self): result = self.recognize(["中文识别测试 12345", "English document"], [.99, .98]) self.assertFalse(result["needs_review"]) self.assertIn("中文识别测试", result["text"]) def test_invalid_confidence_is_not_treated_as_certain(self): result = self.recognize(['Unknown confidence amount 9999'], [float('nan')]) self.assertEqual(result['text'], '') self.assertTrue(result['needs_review']) def test_missing_local_models_fails_before_engine_initialization(self): with tempfile.TemporaryDirectory() as folder: fake = SimpleNamespace(__file__=str(Path(folder) / "__init__.py")) fake.RapidOCR = lambda **_: self.fail("Engine must not download missing models") with patch.dict(sys.modules, {"rapidocr":fake}): with self.assertRaisesRegex(RuntimeError, "本地 OCR 模型"): ocr_text._create_ocr_engine() class StaticDocument(unittest.TestCase): def test_active_content_is_rejected(self): for html in ["", '', '', 'x', '', '']: with self.subTest(html=html), self.assertRaises(ValueError): create_design_pdf.StaticHTML().feed(html) def test_static_math_diagram_and_svg_are_accepted(self): html = '

报告

E=mc^2

flowchart LR\nA-->B
' create_design_pdf.StaticHTML().feed(html) if __name__ == "__main__": unittest.main()