"""Importing a magazine from page images: conversion to a scan-style PDF and its refusals.""" import io import os import tempfile import unittest from pathlib import Path os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-test-config-")) from PIL import Image from core import magazine as mag def png(w=300, h=400, dpi=None, color=(250, 250, 250)): buf = io.BytesIO() kw = {"dpi": (dpi, dpi)} if dpi else {} Image.new("RGB", (w, h), color).save(buf, format="PNG", **kw) return buf.getvalue() def jpeg(w=300, h=400, orientation=None): buf = io.BytesIO() im = Image.new("RGB", (w, h), (200, 200, 200)) exif = Image.Exif() if orientation: exif[0x0112] = orientation im.save(buf, format="JPEG", exif=exif) return buf.getvalue() class ImagesToPdfTests(unittest.TestCase): def setUp(self): self.dir = Path(tempfile.mkdtemp(prefix="ttvc-img-")) self.dest = self.dir / "source.pdf" def pages(self): import pymupdf with pymupdf.open(str(self.dest)) as d: return [(round(p.rect.width), round(p.rect.height), len(p.get_text().strip())) for p in d] def test_pages_in_order_sized_from_resolution_with_no_text_layer(self): n = mag.images_to_pdf([("a.png", png(600, 900, dpi=300)), ("b.jpg", jpeg(150, 300))], self.dest) self.assertEqual(n, 2) # 600x900 px at 300 dpi = 144x216 pt; the jpeg has no dpi -> assumed 150 dpi = 72x144 pt self.assertEqual(self.pages(), [(144, 216, 0), (72, 144, 0)]) def test_exif_rotation_is_applied(self): mag.images_to_pdf([("r.jpg", jpeg(300, 200, orientation=6))], self.dest) # rotated 90°: becomes portrait w, h, _ = self.pages()[0] self.assertLess(w, h) def test_unrotated_jpeg_is_not_reencoded(self): data = jpeg(300, 200) mag.images_to_pdf([("r.jpg", data)], self.dest) import pymupdf with pymupdf.open(str(self.dest)) as d: extracted = d.extract_image(d[0].get_images()[0][0])["image"] self.assertEqual(extracted, data) def test_rejects_a_renamed_non_image_by_content(self): with self.assertRaisesRegex(mag.ImageImportError, "not a PNG or JPEG"): mag.images_to_pdf([("page.png", b"not an image")], self.dest) self.assertFalse(self.dest.exists()) def test_rejects_a_truncated_image(self): with self.assertRaisesRegex(mag.ImageImportError, "could not be read"): mag.images_to_pdf([("t.png", png()[:40])], self.dest) def test_rejects_too_many_pixels_before_decoding(self): old = mag.IMAGE_IMPORT_MAX_PIXELS mag.IMAGE_IMPORT_MAX_PIXELS = 1000 try: with self.assertRaisesRegex(mag.ImageImportError, "too many pixels"): mag.images_to_pdf([("big.png", png(300, 400))], self.dest) finally: mag.IMAGE_IMPORT_MAX_PIXELS = old def test_rejects_oversize_file_and_empty_and_too_many(self): with self.assertRaisesRegex(mag.ImageImportError, "larger than"): mag.images_to_pdf([("x.png", b"\x89PNG\r\n\x1a\n" + b"0" * (mag.IMAGE_IMPORT_MAX_BYTES + 1))], self.dest) with self.assertRaises(mag.ImageImportError): mag.images_to_pdf([], self.dest) with self.assertRaisesRegex(mag.ImageImportError, "At most"): mag.images_to_pdf([("a.png", png(10, 10))] * (mag.IMAGE_IMPORT_MAX_FILES + 1), self.dest) def test_one_bad_image_leaves_no_partial_pdf(self): with self.assertRaises(mag.ImageImportError): mag.images_to_pdf([("a.png", png()), ("b.png", b"nope")], self.dest) self.assertEqual(list(self.dir.iterdir()), []) def test_name_is_only_used_as_a_short_label(self): with self.assertRaises(mag.ImageImportError) as ctx: mag.images_to_pdf([("../../etc/passwd" + "x" * 200, b"nope")], self.dest) self.assertNotIn("..", str(ctx.exception)) class EndpointTests(unittest.TestCase): def setUp(self): from fastapi import FastAPI from fastapi.testclient import TestClient from unittest import mock import routes.magazine as routes self.routes = routes self.lib = Path(tempfile.mkdtemp(prefix="ttvc-lib-")) self.patch = mock.patch.object(routes, "_LIB", self.lib) self.patch.start() self.addCleanup(self.patch.stop) app = FastAPI() app.include_router(routes.router) self.client = TestClient(app) self.doc = self.client.post("/api/magazine/docs", json={"title": "T"}).json()["id"] def test_upload_builds_the_source_pdf(self): r = self.client.post(f"/api/magazine/{self.doc}/source-images", files=[("files", ("1.png", png(), "image/png")), ("files", ("2.jpg", jpeg(), "image/jpeg"))]) self.assertEqual(r.status_code, 200, r.text) self.assertEqual(r.json()["pages"], 2) self.assertTrue((self.lib / self.doc / "source.pdf").is_file()) def test_a_second_upload_cannot_replace_existing_source(self): files = [("files", ("1.png", png(), "image/png"))] self.assertEqual(self.client.post(f"/api/magazine/{self.doc}/source-images", files=files).status_code, 200) self.assertEqual(self.client.post(f"/api/magazine/{self.doc}/source-images", files=files).status_code, 409) def test_bad_content_is_a_400_with_a_message(self): r = self.client.post(f"/api/magazine/{self.doc}/source-images", files=[("files", ("1.png", b"GIF89a....", "image/png"))]) self.assertEqual(r.status_code, 400) self.assertIn("not a PNG or JPEG", r.json()["detail"]) self.assertFalse((self.lib / self.doc / "source.pdf").exists()) def test_unknown_document_is_rejected(self): r = self.client.post("/api/magazine/nope/source-images", files=[("files", ("1.png", png(), "image/png"))]) self.assertIn(r.status_code, (400, 404)) if __name__ == "__main__": unittest.main()