Files
Classroom_Desktop/tests/test-classroom-printing.sh
ekklesiaandClaude Opus 5.5 594fbd810e fix: black and white is shown as it prints, and right-click prints the picture
Printing through Elmer found two things. Black and white came out grey
while the preview stayed in colour: 0.2.6.2 only told the driver. And
right-clicking a picture in a lesson and choosing Print printed the whole
page around it, where some pictures cannot be opened on their own at all.

Black and white is now made before the preview. Ghostscript makes a grey
copy, the preview shows it, and CUPS is given that same file, still with
ColorModel=Grayscale so Elmer's PCL 6 driver prints with pxlmono. Measured
first: Ghostscript's own grey of the colour original and of the copy give
the same greys (Elmer's driver is Ghostscript); poppler's do not. It takes
up to 4.4 s for a lesson page of pictures, so it runs in the background
with a minute to finish, is made once and kept, and a failure is said so
and never printed in colour instead. Ghostscript is now a named dependency;
cups-filters already brought it.

Right-click asks the engine's hit test: on a picture or a canvas it offers
Print image and Print page, elsewhere only Print page. The picture comes
from the engine's own copy image — the only one of five ways measured that
worked for a signed-in picture, a hotlink-protected one, a one-time
address, blob:, data:, another site's, a canvas and WebP — and it fetches
nothing, so the teacher's sign-in is not needed again. It goes through the
clipboard, which is kept and put back; measured under KWin with another
program owning it. A picture that never arrives is said so; the page is
not printed in its place. It then gets the 0.2.6.2 picture layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:34:38 -05:00

320 lines
15 KiB
Bash
Executable File

#!/bin/bash
# Wonder Ink's printing: which printers, which options, what is sent.
#
# CUPS is not on the development host, so lpstat, lpoptions, lp and
# pdfinfo are stand-ins that answer with the output the real ones gave in
# the lab — Elmer's own PPD options included. Whether a job really goes
# through Elmer's filters is tools/lab/wonderink-print-check's question.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/bin"
cat > "$WORK/bin/lpstat" <<EOF
#!/bin/bash
[[ -f "$WORK/cups-down" ]] && { echo "lpstat: Scheduler is not running." >&2; exit 1; }
case "\$1" in
-e) cat "$WORK/printers" 2>/dev/null ;;
-d) cat "$WORK/default" 2>/dev/null || echo "no system default destination" ;;
esac
EOF
# lpoptions -p NAME -l, as the real one printed it for Elmer's PPD.
cat > "$WORK/bin/lpoptions" <<EOF
#!/bin/bash
cat "$WORK/options-\$2" 2>/dev/null
EOF
cat > "$WORK/bin/lp" <<EOF
#!/bin/bash
printf '%s\n' "\$@" > "$WORK/lp-args"
if [[ -f "$WORK/lp-refuses" ]]; then
echo 'lp: Destination "Elmer" is not accepting jobs.' >&2
exit 1
fi
echo "request id is Elmer-42 (1 file(s))"
EOF
cat > "$WORK/bin/pdfinfo" <<'EOF'
#!/bin/bash
[[ "$(head -c 5 "$1" 2>/dev/null)" == "%PDF-" ]] || { echo "Syntax Error" >&2; exit 1; }
echo "Producer: test"
echo "Pages: $(grep -c '^page' "$1")"
EOF
chmod +x "$WORK"/bin/*
printf 'Elmer\n' > "$WORK/printers"
echo "system default destination: Elmer" > "$WORK/default"
cat > "$WORK/options-Elmer" <<'EOF'
PrintoutMode/Print Quality: Draft Draft.Gray *Normal Normal.Gray High High.Gray
PageSize/Page Size: Custom.WIDTHxHEIGHT *Letter A4 11x17 A3 A5 B5 Env10 EnvC5 EnvDL EnvISOB5 EnvMonarch Executive Legal
Duplex/Double-Sided Printing: DuplexNoTumble DuplexTumble *None
ColorModel/Color Mode: FromPrintoutMode *Color Grayscale
PrinterResolution/Resolution: *FromPrintoutMode 300x300dpi 600x600dpi 1200x600dpi 1200x1200dpi
EOF
cat > "$WORK/options-Office" <<'EOF'
PageSize/Media Size: Letter *A4 Legal
EOF
PATH="$WORK/bin:$PATH" WORK="$WORK" PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
import os
import re
import sys
from pathlib import Path
import classroom_printing as printing
root = Path(sys.argv[1])
work = Path(os.environ["WORK"])
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
if detail:
print(f" {detail}")
failed += 1
def lp_args():
return (work / "lp-args").read_text().splitlines()
print("1. which printers a teacher is offered")
check("Elmer, and Elmer is the default", printing.printers() == (["Elmer"], "Elmer"))
(work / "printers").write_text("Elmer\nOffice\n")
(work / "default").write_text("system default destination: Office\n")
check("the system default is preselected, whichever it is",
printing.printers() == (["Elmer", "Office"], "Office"))
(work / "default").write_text("no system default destination\n")
check("with no default, the first printer",
printing.printers() == (["Elmer", "Office"], "Elmer"))
(work / "default").write_text("system default destination: Gone\n")
check("a default that is not in the list is not offered",
printing.printers() == (["Elmer", "Office"], "Elmer"))
(work / "printers").write_text("")
check("no printers at all is an empty list, not an error",
printing.printers() == ([], None))
(work / "cups-down").touch()
check("and so is CUPS not running", printing.printers() == ([], None))
(work / "cups-down").unlink()
(work / "printers").write_text("Elmer\n")
(work / "default").write_text("system default destination: Elmer\n")
print()
print("2. what each printer's driver offers")
elmer = printing.options("Elmer")
check("Elmer's paper is Letter", elmer["paper"] == "Letter", elmer)
# Measured: this driver ignores print-color-mode and prints in colour.
check("black and white is asked for in the driver's own terms",
elmer["gray"] == ("ColorModel", "Grayscale"), elmer)
office = printing.options("Office")
check("an A4 printer is A4", office["paper"] == "A4", office)
check("a printer with no grey choice offers none", office["gray"] is None, office)
unknown = printing.options("Nobody")
check("a printer that says nothing is Letter, colour only",
unknown == {"paper": "Letter", "gray": None}, unknown)
print()
print("3. a teacher's page list")
for text, expected in (("", None), ("all", None), (" ", None), ("2", "2"),
("1-3", "1-3"), ("1, 3, 5-6", "1,3,5-6"), ("3-3", "3"),
("1 - 2", "1-2"), ("6", "6")):
try:
got = printing.parse_pages(text, 6)
except ValueError as error:
got = f"refused: {error}"
check(f"{text!r} is {expected!r}", got == expected, got)
for text in ("7", "0", "3-1", "1-9", "one", "1;lp -d x", "-2", "1--2", "2,", "1.5"):
try:
printing.parse_pages(text, 6)
check(f"{text!r} is refused", False)
except ValueError as error:
check(f"{text!r} is refused, and says why: {error}", bool(str(error)))
print()
print("4. what is handed to CUPS")
pdf = work / "Colour the Ark.pdf"
pdf.write_text("%PDF-1.4\npage\npage\n")
check("a PDF with two pages has two", printing.page_count(pdf) == 2)
check("something that is not a PDF has none",
printing.page_count(work / "printers") == 0)
ok, answer = printing.submit(pdf, "Elmer")
args = lp_args()
check("a job is accepted, with its id", (ok, answer) == (True, "Elmer-42"), answer)
check("to Elmer, one copy, named for the lesson",
args[:6] == ["-d", "Elmer", "-n", "1", "-t", "Wonder Ink"], args)
check("the file comes after --, so no name can be read as an option",
args[-2:] == ["--", str(pdf)], args)
check("and nothing else is asked for", "-P" not in args and "-o" not in args, args)
printing.submit(pdf, "Elmer", copies=3, pages="2-3", gray=("ColorModel", "Grayscale"),
title="Noah")
args = lp_args()
check("copies, pages and black and white arrive as lp options",
args[args.index("-n") + 1] == "3"
and args[args.index("-P") + 1] == "2-3"
and args[args.index("-o") + 1] == "ColorModel=Grayscale"
and args[args.index("-t") + 1] == "Noah", args)
printing.submit(pdf, "Elmer", copies=500)
check(f"copies are capped at {printing.MAX_COPIES}",
lp_args()[lp_args().index("-n") + 1] == str(printing.MAX_COPIES))
printing.submit(pdf, "Elmer", copies=0)
check("and never fewer than one", lp_args()[lp_args().index("-n") + 1] == "1")
(work / "lp-refuses").touch()
ok, answer = printing.submit(pdf, "Elmer")
check("a refused job is a failure, in CUPS's own words",
not ok and answer == 'Destination "Elmer" is not accepting jobs.', answer)
print()
print("4b. where a picture goes on the paper")
LETTER, MARGIN = (612, 792), 28.35
area = (MARGIN, MARGIN, 612 - 2 * MARGIN, 792 - 2 * MARGIN)
close = lambda a, b: all(abs(x - y) < 0.01 for x, y in zip(a, b))
x, y, w, h = printing.picture_rect(LETTER, MARGIN, (1600, 1000))
check("a wide picture fits the width and keeps its shape",
abs(w - area[2]) < 0.01 and abs(w / h - 1.6) < 0.001, (x, y, w, h))
check("and is centred on the page", abs(y + h / 2 - 396) < 0.01)
check("nothing of it falls outside the margins",
x >= MARGIN - 0.01 and y >= MARGIN - 0.01
and x + w <= 612 - MARGIN + 0.01 and y + h <= 792 - MARGIN + 0.01)
x, y, w, h = printing.picture_rect(LETTER, MARGIN, (1600, 1000), 50, "top left")
check("50% in the top left is half the fitted size, at the margin",
close((x, y, w), (MARGIN, MARGIN, area[2] / 2)), (x, y, w, h))
x, y, w, h = printing.picture_rect(LETTER, MARGIN, (1600, 1000), 25, "bottom right")
check("25% in the bottom right ends at the margin",
close((x + w, y + h), (612 - MARGIN, 792 - MARGIN)), (x, y, w, h))
x, y, w, h = printing.picture_rect(LETTER, MARGIN, (900, 1400), 100, "right")
check("a tall picture fits the height and sits at the right",
abs(h - area[3]) < 0.01 and abs(x + w - (612 - MARGIN)) < 0.01, (x, y, w, h))
x, y, w, h = printing.picture_rect(LETTER, MARGIN, (1600, 1000), fill=True)
check("fill covers the whole printable area",
x <= MARGIN and y <= MARGIN + 0.01 and x + w >= 612 - MARGIN and y + h >= 792 - MARGIN - 0.01)
check("and keeps the picture's shape, cutting off what overhangs",
abs(w / h - 1.6) < 0.001 and w > area[2])
check("fill ignores size and position",
printing.picture_rect(LETTER, MARGIN, (1600, 1000), 25, "top left", True)
== printing.picture_rect(LETTER, MARGIN, (1600, 1000), fill=True))
check("an unknown position is the centre",
printing.picture_rect(LETTER, MARGIN, (1600, 1000), 50, "upside down")
== printing.picture_rect(LETTER, MARGIN, (1600, 1000), 50, "centre"))
try:
printing.picture_rect(LETTER, MARGIN, (0, 10))
check("a picture with no size is refused", False)
except ValueError:
check("a picture with no size is refused", True)
check("automatic paper is landscape for a wide picture",
printing.picture_landscape("auto", 1600, 1000))
check("and portrait for a tall or square one",
not printing.picture_landscape("auto", 900, 1400)
and not printing.picture_landscape("auto", 1000, 1000))
check("and the teacher's choice overrides it",
not printing.picture_landscape("portrait", 1600, 1000)
and printing.picture_landscape("landscape", 900, 1400))
check("every position is on a three by three grid",
len(printing.PICTURE_POSITIONS) == 9
and sorted(printing.PICTURE_POSITIONS.values()) == [(c, r) for c in range(3) for r in range(3)])
print()
print("4c. black and white, made before the preview")
command = printing.grayscale_command(work / "in.pdf", work / "out gray.pdf")
check("Ghostscript writes a PDF", command[0] == "gs" and "-sDEVICE=pdfwrite" in command, command)
check("in grey and nothing else",
"-sColorConversionStrategy=Gray" in command and "-dProcessColorModel=/DeviceGray" in command, command)
# pdfwrite otherwise turns a page to match its text; a landscape picture
# on a portrait sheet would come back turned.
check("never turning a page", "-dAutoRotatePages=/None" in command, command)
check("annotations drawn in as they print, not left for a filter to draw in colour",
"-dPreserveAnnots=false" in command and "-dPrinted" in command, command)
check("safely, and without stopping to ask",
"-dSAFER" in command and "-dBATCH" in command and "-dNOPAUSE" in command, command)
check("to the file asked for, even with a space in its name",
f"-sOutputFile={work / 'out gray.pdf'}" in command, command)
check("and the source after --, so no name can be read as an option",
command[-2:] == ["--", str(work / "in.pdf")], command)
print()
print("5. Wonder Ink prints through this, and only this")
ink = (root / "scripts/classroom-wonderink").read_text()
code = "\n".join(line for line in ink.splitlines() if not line.lstrip().startswith("#"))
# The 0.2.2 crash, and the blank worksheet, both came from Qt's print path.
check("no QPrinter", "QPrinter(" not in code and "QPrintDialog" not in code)
check("no QWebEngineView.print()", not re.search(r"\bview\.print\(", code))
check("a lesson page becomes a PDF by printToPdf", "printToPdf(" in code)
check("which is given up on if the engine never answers",
"def _wait(self, message, too_long, timeout=PREPARE_TIMEOUT_MS):" in code
and "self._timer.start(timeout)" in code)
check("black and white has longer, since a page of photographs is slow, not stuck",
"GRAY_TIMEOUT_MS = 60_000" in code and " GRAY_TIMEOUT_MS,\n" in code)
check("the preview submits with classroom_printing.submit",
"classroom_printing.submit(" in code)
check("there is no file chooser in it", "QFileDialog" not in code)
check("a worksheet tab is printed from its file",
"self._pdf_views[view] = resolved" in code)
check("an inline PDF is fetched as a file first", "view.page().download(view.url())" in code)
check("a page's background is not printed, as in Chrome",
"PrintElementBackgrounds, False" in code)
check("a picture on its own is laid out, not printed as the viewer page",
"dialog.show_picture(image)" in code and "document.contentType" in ink)
preview_code = code[code.index("class PrintPreview"):code.index("\ndef notify")]
check("the print interface says Color, not Colour",
'"Color"' in preview_code and '"Colour"' not in preview_code)
check("an exception in a handler is logged, not fatal",
"sys.excepthook = keep_going" in code)
# 0.2.6.3: black and white is shown as it prints.
check("black and white is made by classroom_printing.grayscale_command, in the background",
"classroom_printing.grayscale_command(" in preview_code and "QProcess(" in preview_code)
check("choosing Color or Black and white lays the preview out again",
"self._colour.currentIndexChanged.connect" in preview_code)
check("a failed conversion is judged by its exit and its pages, never printed in colour",
"process.exitCode() != 0" in preview_code
and "page_count(target) != expected" in preview_code)
check("the driver is still told ColorModel=Grayscale for the grey file",
'gray = self._options[printer]["gray"]' in preview_code)
check("and nothing prints that is not what is shown",
"(self._gray is not None and self._pdf == self._gray[1]) != (gray is not None)" in preview_code)
# 0.2.6.3: right-click says what it prints.
menu_code = code[code.index(" def contextMenuEvent"):code.index("\nclass WonderInk")]
check("right-click offers Print page always",
'"Print page…"' in menu_code and "print_view(self)" in menu_code)
check("and Print image only where the engine's hit test found a picture or a canvas",
'"Print image…"' in menu_code and "MediaTypeImage" in menu_code
and "MediaTypeCanvas" in menu_code and "print_image(self)" in menu_code)
check("the old single Print… is gone from the menu", '"Print…"' not in menu_code)
take_code = code[code.index(" def _take_picture"):code.index(" def _show_preview")]
check("the picture is the engine's own copy, not fetched again",
"WebAction.CopyImageToClipboard" in take_code
and "download(" not in take_code and "QNetwork" not in take_code)
check("what was on the clipboard is kept and put back",
"keep_clipboard(clipboard)" in take_code and "put_back()" in take_code)
check("a picture that never arrives is said so, not printed as the page",
"TAKE_PICTURE_MS" in take_code and "dialog.fail(" in take_code
and "print_view" not in take_code)
check("and it goes to the picture layout, on white",
"dialog.show_picture(printable_picture(image))" in take_code
and "Qt.GlobalColor.white" in code)
check("the module is packaged",
"lib/classroom_printing.py" in (root / "tools/build-release.sh").read_text())
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY