"""Make the test pictures, and measure where a printed picture really is. wonderink-picture-measure.py make DIRECTORY wonderink-picture-measure.py measure PDF WIDTHxHEIGHT SIZE POSITION PAPER FILL wonderink-picture-measure.py darkness PDF wonderink-picture-measure.py colour PDF Run inside the lab by tools/lab/wonderink-print-check. The pictures are white with a black border drawn on their own edges and a pink mark near one corner, so the ink on a printed page says exactly where the picture landed — and any other dark ink on the page is something that was added, which is what the black bars were. The page is rendered at 72 dpi, so one pixel is one PostScript point, and compared against classroom_printing.picture_rect: the same arithmetic Wonder Ink lays the page out with, asked independently of the preview. """ import json import subprocess import sys sys.path.insert(0, "/usr/local/lib/ekklesia-classroom") import classroom_printing # noqa: E402 MARGIN_POINTS = 10 / 25.4 * 72 def make(directory): from PyQt6.QtCore import QRectF, Qt from PyQt6.QtGui import QColor, QFont, QImage, QPainter, QPen from PyQt6.QtWidgets import QApplication app = QApplication(sys.argv[:1]) # noqa: F841 — kept, or Qt has no application def art(width, height, name, label, transparent=False): image = QImage(width, height, QImage.Format.Format_ARGB32) image.fill(QColor(0, 0, 0, 0) if transparent else QColor("white")) painter = QPainter(image) painter.setPen(QPen(QColor("black"), 8)) painter.drawRect(QRectF(4, 4, width - 8, height - 8)) painter.setPen(QPen(QColor("black"), 6)) painter.drawEllipse(QRectF(width * 0.2, height * 0.25, width * 0.6, height * 0.5)) painter.setFont(QFont("DejaVu Sans", int(min(width, height) * 0.05))) painter.drawText(QRectF(0, height * 0.05, width, height * 0.15), Qt.AlignmentFlag.AlignCenter, label) painter.fillRect(QRectF(width * 0.05, height * 0.8, width * 0.1, height * 0.1), QColor("#e0006c")) painter.end() image.save(f"{directory}/{name}") art(1600, 1000, "ark-wide.png", "Colour the Ark") art(900, 1400, "ark-tall.png", "Noah") art(1600, 1000, "ark-wide.jpg", "Colour the Ark") # Transparent around the drawing: on paper that has to be white, not # black, which is what a careless composite would make of it. art(1200, 1200, "ark-clear.png", "Clear", transparent=True) webp(directory) def webp(directory): """ark-wide.png as WebP. Qt cannot write WebP, or read it; Chromium does both.""" import base64 from PyQt6.QtCore import QTimer, QUrl from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import QApplication app = QApplication.instance() or QApplication(sys.argv[:1]) source = base64.b64encode(open(f"{directory}/ark-wide.png", "rb").read()).decode() view = QWebEngineView() def made(answer): if isinstance(answer, str) and answer.startswith("data:image/webp;base64,"): with open(f"{directory}/ark-wide.webp", "wb") as handle: handle.write(base64.b64decode(answer.split(",", 1)[1])) app.quit() def loaded(_ok): view.page().runJavaScript("""var i = new Image(); i.onload = function(){ var c = document.createElement('canvas'); c.width = i.width; c.height = i.height; c.getContext('2d').drawImage(i, 0, 0); window.__webp = c.toDataURL('image/webp', 0.95); }; i.src = 'data:image/png;base64,%s'; 0""" % source) QTimer.singleShot(1500, lambda: view.page().runJavaScript("window.__webp", made)) view.loadFinished.connect(loaded) view.setHtml("") QTimer.singleShot(20000, app.quit) app.exec() def page_pixels(pdf, page=1): ppm = subprocess.run(["pdftoppm", "-r", "72", "-f", str(page), "-l", str(page), "-singlefile", pdf], capture_output=True, check=True).stdout # P6 header: magic, width, height, maxval, then raw RGB. fields, index = [], 0 while len(fields) < 4: while ppm[index:index + 1].isspace(): index += 1 start = index while not ppm[index:index + 1].isspace(): index += 1 fields.append(ppm[start:index]) width, height = int(fields[1]), int(fields[2]) return width, height, ppm[index + 1:] def measure(pdf, picture, size, position, paper, fill): width, height, rgb = page_pixels(pdf) picture_w, picture_h = (int(n) for n in picture.split("x")) landscape = classroom_printing.picture_landscape(paper, picture_w, picture_h) x, y, w, h = classroom_printing.picture_rect( (width, height), MARGIN_POINTS, (picture_w, picture_h), int(size), position.replace("-", " "), fill == "fill") # The expected ink, clipped to the printable area as the page is. left = max(x, MARGIN_POINTS) top = max(y, MARGIN_POINTS) right = min(x + w, width - MARGIN_POINTS) bottom = min(y + h, height - MARGIN_POINTS) xs, ys, outside, pink = [], [], 0, 0 for row in range(height): for column in range(width): i = (row * width + column) * 3 r, g, b = rgb[i], rgb[i + 1], rgb[i + 2] dark = r + g + b < 3 * 110 magenta = r > 150 and g < 90 and b > 60 if dark or magenta: xs.append(column) ys.append(row) if not (left - 2 <= column <= right + 2 and top - 2 <= row <= bottom + 2): outside += 1 if magenta: pink += 1 # The corner mark, wherever it landed and whatever colour it came out: # pink on a colour page, grey on a black-and-white one. Inked if most # of the middle of where it should be is not paper. mark_left = max(x + w * 0.07, MARGIN_POINTS) mark_right = min(x + w * 0.13, width - MARGIN_POINTS) mark_top = max(y + h * 0.82, MARGIN_POINTS) mark_bottom = min(y + h * 0.88, height - MARGIN_POINTS) mark = [ sum(rgb[(row * width + column) * 3:(row * width + column) * 3 + 3]) < 3 * 200 for row in range(int(mark_top), int(mark_bottom)) for column in range(int(mark_left), int(mark_right)) ] found = (min(xs), min(ys), max(xs) + 1, max(ys) + 1) if xs else None expected = (round(left), round(top), round(right), round(bottom)) off = max(abs(a - b) for a, b in zip(found, expected)) if found else None print(json.dumps({ "page": [width, height], "page_landscape": width > height, "wanted_landscape": landscape, "found": found, "expected": expected, "off_by_points": off, "ink_outside_picture": outside, "pink_pixels": pink, "mark_inked": bool(mark) and sum(mark) > 0.8 * len(mark), "colour_spread": spread(rgb), })) def spread(rgb): """The most any pixel's red, green and blue differ: 0 on a grey page.""" return max((max(rgb[i:i + 3]) - min(rgb[i:i + 3]) for i in range(0, len(rgb) - 2, 3)), default=0) def colour(pdf): """Is every page grey? The colour spread of each page, and how many.""" pages = int(subprocess.run(["pdfinfo", pdf], capture_output=True, text=True).stdout .split("Pages:")[1].split()[0]) spreads = [spread(page_pixels(pdf, n)[2]) for n in range(1, pages + 1)] print(json.dumps({"pages": pages, "spreads": spreads, "gray": max(spreads) == 0})) def darkness(pdf): """How much of the first page is dark, 0 to 1.""" width, height, rgb = page_pixels(pdf) sums = [rgb[i] + rgb[i + 1] + rgb[i + 2] for i in range(0, len(rgb) - 2, 3)] dark = sum(1 for total in sums if total < 3 * 110) # Anything printed at all. Blink darkens light text when a page's # background is not printed, so white-on-black lesson text comes out # grey on white rather than vanishing; this is what says it did. ink = sum(1 for total in sums if total < 3 * 200) print(json.dumps({"dark_fraction": round(dark / (width * height), 4), "ink_fraction": round(ink / (width * height), 4)})) if __name__ == "__main__": if sys.argv[1] == "make": make(sys.argv[2]) elif sys.argv[1] == "darkness": darkness(sys.argv[2]) elif sys.argv[1] == "colour": colour(sys.argv[2]) else: measure(*sys.argv[2:])