test: the lab prints from a signed-in lesson, and judges black and white on the job

A lesson site for the lab (session cookie, one-time addresses, a second
origin, a request log) and real right-clicks on its words and on a JPEG, a
PNG, a transparent PNG, a one-time picture, another site's, a blob:, a
WebP, a canvas, a linked picture, a broken one, and Cancel. Each is judged
on the job CUPS kept — where and how large, no other ink — and on the site
log: nothing was fetched again. The clipboard is the teacher's afterwards.

Black and white: a colourful lesson turned back and forth five times on
page 2, a worksheet, a picture at 50% top left, a page full of pictures,
back to Color, and Cancel. Every grey preview is grey pixel for pixel and
the job is byte-for-byte the file shown, through pxlmono. The three 0.2.6.2
black-and-white scenarios now wait for the grey file before Print, as a
teacher has to. 343 checks, none failing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-26 16:34:38 -05:00
co-authored by Claude Opus 5.5
parent 594fbd810e
commit cb12e59dea
4 changed files with 535 additions and 35 deletions
+65 -2
View File
@@ -3,6 +3,7 @@
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
@@ -55,10 +56,42 @@ def make(directory):
# 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 page_pixels(pdf):
ppm = subprocess.run(["pdftoppm", "-r", "72", "-f", "1", "-l", "1", "-singlefile", pdf],
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("<html></html>")
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
@@ -106,6 +139,19 @@ def measure(pdf, picture, size, position, paper, fill):
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
@@ -119,9 +165,24 @@ def measure(pdf, picture, size, position, paper, fill):
"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)
@@ -140,5 +201,7 @@ if __name__ == "__main__":
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:])