contentpanel - Aushangworkflow um docx Vorlage erweitern
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Integration checks against the production Chromium/Rust renderer, no LLM."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from betteroffice_render import RenderError, _check_renderer, _server, render
|
||||
from docx_template import fill_template
|
||||
from process import _date_label
|
||||
|
||||
TEMPLATE = Path(os.getenv("CONTENTPANEL_TEST_TEMPLATE", "/var/www/html/intranet_repo/projects/contentpanel/insertion/template/aushang-template.docx"))
|
||||
|
||||
|
||||
class RenderingTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="contentpanel_render_test_")
|
||||
cls.output = Path(os.getenv("CONTENTPANEL_TEST_OUTPUT", cls.temp.name))
|
||||
cls.output.mkdir(parents=True, exist_ok=True)
|
||||
cls.document = cls.output / "verified.docx"
|
||||
with zipfile.ZipFile(TEMPLATE) as archive:
|
||||
(cls.output / "original-logo.png").write_bytes(archive.read("word/media/image1.png"))
|
||||
paragraphs = [
|
||||
("Dies ist ein langer Testabsatz zur Prüfung von Schriftgröße, Zeilenumbruch und Seitenaufteilung. " * 12).strip()
|
||||
for _ in range(3)
|
||||
] + ["Erste Gedichtzeile\nZweite Gedichtzeile\n\nVon Herzen\nEure Gastgeber",
|
||||
"Abschlussmarker: Dieser letzte Absatz muss vollständig auf der letzten Seite erscheinen."]
|
||||
fill_template(TEMPLATE, cls.document,
|
||||
"Verabschiedung von Diakon Georg aus der Gemeinde Reinickendorf",
|
||||
_date_label("2026-09-02"), paragraphs)
|
||||
with patch.dict(os.environ, {"CONTENTPANEL_RENDER_SCALE": "1"}):
|
||||
cls.page_count = render(cls.document, cls.output)
|
||||
cls.metrics = []
|
||||
server = _server(cls.output, cls.document, ROOT / "fonts")
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=True, args=["--disable-dev-shm-usage"])
|
||||
page = browser.new_page()
|
||||
for number in range(1, cls.page_count + 1):
|
||||
page.goto(f"http://127.0.0.1:{server.server_address[1]}/page-{number}.png")
|
||||
page.wait_for_function("document.querySelector('img')?.complete")
|
||||
cls.metrics.append(page.evaluate("""async () => {
|
||||
const img = document.querySelector('img');
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth; canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0);
|
||||
const {data} = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const blue = {left:canvas.width, top:canvas.height, right:0, bottom:0, count:0};
|
||||
const rows = Array(canvas.height).fill(0);
|
||||
let rightOverflow = 0;
|
||||
for (let y=0;y<canvas.height;y++) for (let x=0;x<canvas.width;x++) {
|
||||
const i=(y*canvas.width+x)*4, r=data[i], g=data[i+1], b=data[i+2];
|
||||
if (b>130 && r<90 && g>60 && b>g+20) {
|
||||
blue.left=Math.min(blue.left,x); blue.right=Math.max(blue.right,x);
|
||||
blue.top=Math.min(blue.top,y); blue.bottom=Math.max(blue.bottom,y); blue.count++;
|
||||
}
|
||||
if (r<100 && g<100 && b<100) {
|
||||
if (x>=50 && x<725) rows[y]++;
|
||||
if (x>=725 && y>300 && y<930) rightOverflow++;
|
||||
}
|
||||
}
|
||||
const bands=[]; let start=null;
|
||||
rows.forEach((count,y)=>{
|
||||
if(count>=4 && start===null) start=y;
|
||||
if((count<4 || y===rows.length-1) && start!==null) {
|
||||
bands.push({top:start,bottom:y-1,height:y-start}); start=null;
|
||||
}
|
||||
});
|
||||
// Compare the full white symbol, not just its blue box.
|
||||
// A shifted duplicate used to pass the position check.
|
||||
const reference = new Image(); reference.src = '/original-logo.png';
|
||||
await reference.decode();
|
||||
const refCanvas = document.createElement('canvas');
|
||||
const w=blue.right-blue.left+1, h=blue.bottom-blue.top+1;
|
||||
refCanvas.width=w; refCanvas.height=h;
|
||||
const refCtx=refCanvas.getContext('2d'); refCtx.drawImage(reference,0,0,w,h);
|
||||
const ref=refCtx.getImageData(0,0,w,h).data;
|
||||
const actual=ctx.getImageData(blue.left,blue.top,w,h).data;
|
||||
const white=(pixels,i)=>pixels[i]>150 && pixels[i+1]>180 && pixels[i+2]>200;
|
||||
let tp=0, fp=0, fn=0;
|
||||
for(let i=0;i<ref.length;i+=4) {
|
||||
const expected=white(ref,i), rendered=white(actual,i);
|
||||
if(expected && rendered) tp++;
|
||||
else if(rendered) fp++;
|
||||
else if(expected) fn++;
|
||||
}
|
||||
const logoF1=2*tp/(2*tp+fp+fn);
|
||||
return {width:canvas.width,height:canvas.height,blue,bands,rightOverflow,logoF1};
|
||||
}"""))
|
||||
browser.close()
|
||||
finally:
|
||||
server.shutdown(); server.server_close(); thread.join(timeout=2)
|
||||
(cls.output / "pixel-metrics.json").write_text(json.dumps(cls.metrics, indent=2), encoding="utf-8")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.temp.cleanup()
|
||||
|
||||
def test_long_text_paginates_and_last_paragraph_survives(self):
|
||||
import subprocess
|
||||
self.assertGreaterEqual(self.page_count, 2)
|
||||
self.assertLessEqual(self.page_count, 4)
|
||||
text = subprocess.run(["tesseract", str(self.output / f"page-{self.page_count}.png"),
|
||||
"stdout", "-l", "deu", "--psm", "11"],
|
||||
capture_output=True, text=True, check=True, timeout=30).stdout
|
||||
self.assertIn("Abschlussmarker", text)
|
||||
|
||||
def test_real_pixels_have_large_title_and_wrapped_body(self):
|
||||
first = self.metrics[0]
|
||||
title = [band for band in first["bands"] if 90 < band["top"] < 275 and band["height"] > 10]
|
||||
self.assertEqual(len(title), 3)
|
||||
self.assertTrue(all(band["height"] >= 30 for band in title), title)
|
||||
body = [band for band in first["bands"] if 325 < band["top"] < 930]
|
||||
self.assertGreater(len(body), 15)
|
||||
self.assertTrue(all(metric["rightOverflow"] == 0 for metric in self.metrics))
|
||||
|
||||
def test_logo_is_on_the_right_and_complete_on_every_page(self):
|
||||
for number, metric in enumerate(self.metrics, 1):
|
||||
with self.subTest(page=number):
|
||||
self.assertEqual((metric["width"], metric["height"]), (794, 1123))
|
||||
logo = metric["blue"]
|
||||
self.assertGreater(logo["count"], 3500)
|
||||
self.assertAlmostEqual(logo["left"], 643, delta=2)
|
||||
self.assertAlmostEqual(logo["right"], 718, delta=2)
|
||||
self.assertGreater(logo["top"], 950)
|
||||
self.assertLess(logo["bottom"], 1060)
|
||||
self.assertAlmostEqual(logo["right"]-logo["left"], logo["bottom"]-logo["top"], delta=2)
|
||||
self.assertGreater(metric["logoF1"], .78, metric)
|
||||
|
||||
def test_missing_font_stops_before_png_export(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
with patch.dict(os.environ, {"CONTENTPANEL_FONT_DIR": temp}):
|
||||
with self.assertRaisesRegex(RenderError, "Schriftdatei fehlt"):
|
||||
render(self.document, Path(temp) / "output")
|
||||
self.assertFalse(list(Path(temp).rglob("*.png")))
|
||||
|
||||
def test_corrupt_font_stops_before_png_export(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
fonts = Path(temp) / "fonts"
|
||||
shutil.copytree(ROOT / "fonts", fonts)
|
||||
(fonts / "SuisseIntl-Light.otf").write_bytes(b"invalid font")
|
||||
with patch.dict(os.environ, {"CONTENTPANEL_FONT_DIR": str(fonts)}):
|
||||
with self.assertRaises(RenderError):
|
||||
render(self.document, Path(temp) / "output")
|
||||
self.assertFalse(list(Path(temp).rglob("*.png")))
|
||||
|
||||
def test_synthetic_metrics_are_not_accepted_as_success(self):
|
||||
class Page:
|
||||
def evaluate(self, _):
|
||||
return None
|
||||
with self.assertRaisesRegex(RenderError, "synthetic text metrics"):
|
||||
_check_renderer(Page(), ["warning: [canvasBackend] synthetic text metrics active"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user