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:
co-authored by
Claude Opus 5.5
parent
594fbd810e
commit
cb12e59dea
@@ -0,0 +1,128 @@
|
||||
"""A signed-in lesson with pictures in it, for right-click printing.
|
||||
|
||||
wonderink-lesson-site.py PICTURES PORT OTHER_PORT LOG
|
||||
|
||||
Run inside the lab by tools/lab/wonderink-print-check. Stands in for
|
||||
Wonder Ink's own site, which the lab has no account for, with every kind
|
||||
of picture a lesson might put under a teacher's pointer:
|
||||
|
||||
/login sets a session cookie (HttpOnly, SameSite=Lax), as a
|
||||
sign-in does, and goes on to /lesson
|
||||
/lesson the page; its pictures are all behind that cookie
|
||||
/private/NAME a picture, 403 without the cookie
|
||||
/once/N/NAME a picture that can be fetched exactly once — so a
|
||||
print that fetches it again gets a 404, not a picture
|
||||
OTHER_PORT the same pictures from another origin, with no CORS
|
||||
|
||||
Every request is logged, one JSON line each, so a check can say what was
|
||||
fetched while it printed. What was measured with this, and why Print
|
||||
image fetches nothing, is in classroom-wonderink's _take_picture.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
PICTURES = Path(sys.argv[1])
|
||||
PORT, OTHER = int(sys.argv[2]), int(sys.argv[3])
|
||||
LOG = Path(sys.argv[4])
|
||||
TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp"}
|
||||
fetched_once = set()
|
||||
lock = threading.Lock()
|
||||
|
||||
LESSON = """<html><head><title>Picture lesson</title><style>
|
||||
body { margin: 8px; font: 15px sans-serif; }
|
||||
img, canvas { width: 180px; height: 120px; object-fit: contain; border: 1px solid #ccc; margin: 4px; }
|
||||
</style></head><body>
|
||||
<h1 style="color:#c00">Noah's Ark</h1>
|
||||
<p id="words">Noah built an ark, and the animals came two by two. Colour the pictures below.</p>
|
||||
<img id="private-jpg" src="/private/ark-wide.jpg">
|
||||
<img id="private-png" src="/private/ark-tall.png">
|
||||
<img id="clear" src="/private/ark-clear.png">
|
||||
<img id="webp" src="/private/ark-wide.webp">
|
||||
<img id="once" src="/once/NONCE/ark-wide.png">
|
||||
<img id="cross" src="http://localhost:OTHER/img/ark-tall.png">
|
||||
<img id="blob">
|
||||
<canvas id="canvas" width="1600" height="1000"></canvas>
|
||||
<img id="broken" src="/private/not-there.png">
|
||||
<a href="/lesson#more"><img id="linked" src="/private/ark-tall.png"></a>
|
||||
<script>
|
||||
var waiting = 2;
|
||||
function one() { if (--waiting === 0) window.__ready = true; }
|
||||
fetch('/private/ark-tall.png', {credentials: 'include'})
|
||||
.then(function (r) { return r.blob(); })
|
||||
.then(function (b) { var i = document.getElementById('blob'); i.onload = one; i.src = URL.createObjectURL(b); });
|
||||
var art = new Image();
|
||||
art.onload = function () { document.getElementById('canvas').getContext('2d').drawImage(art, 0, 0); one(); };
|
||||
art.src = '/private/ark-wide.png';
|
||||
</script></body></html>"""
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
def answer(self, code, body=b"", kind="text/plain", headers=()):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", kind)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
for name, value in headers:
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def picture(self, name):
|
||||
path = PICTURES / Path(name).name
|
||||
if not path.is_file() or path.suffix not in TYPES:
|
||||
return self.answer(404, b"no such picture")
|
||||
return self.answer(200, path.read_bytes(), TYPES[path.suffix],
|
||||
[("Cache-Control", "private, max-age=600")])
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
signed_in = "session=ok" in self.headers.get("Cookie", "")
|
||||
|
||||
with lock, open(LOG, "a") as log:
|
||||
log.write(json.dumps({"t": time.time(), "port": self.server.server_port,
|
||||
"path": path, "signed_in": signed_in}) + "\n")
|
||||
|
||||
if self.server.server_port == OTHER:
|
||||
if path.startswith("/img/"):
|
||||
return self.picture(path[5:])
|
||||
return self.answer(404)
|
||||
|
||||
if path == "/login":
|
||||
return self.answer(302, headers=[
|
||||
("Set-Cookie", "session=ok; Path=/; HttpOnly; SameSite=Lax"),
|
||||
("Location", "/lesson")])
|
||||
|
||||
if not signed_in:
|
||||
return self.answer(403, b"sign in first")
|
||||
|
||||
if path == "/lesson":
|
||||
page = LESSON.replace("NONCE", uuid.uuid4().hex).replace("OTHER", str(OTHER))
|
||||
return self.answer(200, page.encode(), "text/html")
|
||||
|
||||
if path.startswith("/private/"):
|
||||
return self.picture(path[9:])
|
||||
|
||||
if path.startswith("/once/"):
|
||||
with lock:
|
||||
if path in fetched_once:
|
||||
return self.answer(404, b"this address has been used")
|
||||
fetched_once.add(path)
|
||||
return self.picture(path.rsplit("/", 1)[1])
|
||||
|
||||
return self.answer(404)
|
||||
|
||||
|
||||
for port in (OTHER, PORT):
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
|
||||
threading.Event().wait()
|
||||
@@ -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:])
|
||||
|
||||
+173
-27
@@ -27,6 +27,9 @@ lab 'mkdir -p /opt/wonderink-check'
|
||||
docker cp "$ROOT/tools/lab/wonderink-print-rig.py" "$NAME:$RIG" >/dev/null
|
||||
MEASURE=/opt/wonderink-check/wonderink-picture-measure.py
|
||||
docker cp "$ROOT/tools/lab/wonderink-picture-measure.py" "$NAME:$MEASURE" >/dev/null
|
||||
SITE=/opt/wonderink-check/wonderink-lesson-site.py
|
||||
SITE_LOG=/opt/wonderink-check/site.log
|
||||
docker cp "$ROOT/tools/lab/wonderink-lesson-site.py" "$NAME:$SITE" >/dev/null
|
||||
lab "chmod -R a+rX /opt/wonderink-check && test -r $RIG" || {
|
||||
echo "could not put the rig in the lab" >&2
|
||||
exit 1
|
||||
@@ -70,9 +73,10 @@ mkdir -p /opt/wonderink-check/www
|
||||
cupsctl PreserveJobFiles=Yes
|
||||
' || { echo "could not prepare the lab's CUPS" >&2; exit 1; }
|
||||
|
||||
lab "test -f /opt/wonderink-check/www/ark-clear.png || \
|
||||
QT_QPA_PLATFORM=offscreen python3 $MEASURE make /opt/wonderink-check/www; \
|
||||
chmod -R a+r /opt/wonderink-check/www; test -f /opt/wonderink-check/www/ark-clear.png" || {
|
||||
lab "test -f /opt/wonderink-check/www/ark-wide.webp || \
|
||||
QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS='--no-sandbox --disable-gpu' \
|
||||
python3 $MEASURE make /opt/wonderink-check/www; \
|
||||
chmod -R a+r /opt/wonderink-check/www; test -f /opt/wonderink-check/www/ark-wide.webp" || {
|
||||
echo "could not make the test pictures" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -82,6 +86,15 @@ lab "test -f /opt/wonderink-check/www/ark-clear.png || \
|
||||
lab 'pgrep -f "[h]ttp.server 8089" >/dev/null' || \
|
||||
docker exec -d -w /opt/wonderink-check/www "$NAME" python3 -m http.server 8089
|
||||
|
||||
# The signed-in lesson with pictures in it, restarted each run so that its
|
||||
# one-time addresses start unused.
|
||||
lab "pkill -f '[w]onderink-lesson-site.py' ; : > $SITE_LOG; chmod a+rw $SITE_LOG"
|
||||
docker exec -d "$NAME" python3 "$SITE" /opt/wonderink-check/www 8094 8095 "$SITE_LOG"
|
||||
lab 'for _ in $(seq 20); do curl -s -o /dev/null http://127.0.0.1:8094/login && exit 0; sleep 0.3; done; exit 1' || {
|
||||
echo "the lesson site did not start" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
|
||||
@@ -112,6 +125,48 @@ count() { wc -w <<< "$jobs"; }
|
||||
# The file CUPS was handed for a job, kept by PreserveJobFiles.
|
||||
spool_file() { local id="${1%% *}"; printf '/var/spool/cups/d%05d-001' "${id##*-}"; }
|
||||
said() { grep -q -- "$1" <<< "$out"; }
|
||||
# Every line of the rig's output that starts with WORD.
|
||||
lines() { grep -o -- "$1.*" <<< "$out"; }
|
||||
# Is every page of a PDF in the lab grey? {"pages", "spreads", "gray"}
|
||||
colour_of() { lab "python3 $MEASURE colour $1"; }
|
||||
is_gray() { python3 -c 'import json,sys; sys.exit(0 if json.loads(sys.argv[1])["gray"] else 1)' "$1"; }
|
||||
# What the lesson site was asked for after the right-click.
|
||||
fetched_since() {
|
||||
local since
|
||||
since=$(grep -o 'since=[0-9.]*' <<< "$out" | head -1 | cut -d= -f2)
|
||||
lab "python3 -c 'import json,sys; print(sum(1 for l in open(\"$SITE_LOG\") if json.loads(l)[\"t\"] > float(sys.argv[1])))' ${since:-0}"
|
||||
}
|
||||
teacher_text_kept() { said "CLIPBOARD \"TEACHER'S TEXT, John 3:16\""; }
|
||||
# picture layout, measured on the page CUPS kept: judges its own lines.
|
||||
judge_layout() { # FILE_DIMS SIZE POSITION PAPER FILL COLOUR
|
||||
local dims=$1 size=$2 position=$3 paper=$4 fill=$5 color=$6 spool result
|
||||
spool=$(spool_file "$jobs")
|
||||
judge "CUPS was given exactly the page the preview showed" \
|
||||
"[[ -n '$spool' ]] && lab 'cmp -s /tmp/rig-out/preview.pdf $spool'"
|
||||
result=$(lab "python3 $MEASURE measure $spool $dims $size $position $paper $fill")
|
||||
field() { python3 -c "import json,sys; print(json.loads(sys.argv[1])[sys.argv[2]])" "$result" "$1"; }
|
||||
judge "the paper is turned the way asked ($paper): $(field page)" \
|
||||
'[[ "$(field page_landscape)" == "$(field wanted_landscape)" ]]'
|
||||
judge "the picture is where and as large as asked, within 3 points (off by $(field off_by_points))" \
|
||||
'[[ "$(field off_by_points)" != None ]] && (( $(field off_by_points) <= 3 ))'
|
||||
judge "no ink anywhere else on the page — no bars ($(field ink_outside_picture) pixels)" \
|
||||
'[[ "$(field ink_outside_picture)" == 0 ]]'
|
||||
if [[ "$fill" == fill ]]; then
|
||||
# The mark is in the picture's left fifth, which filling a
|
||||
# portrait page with a wide picture cuts off. That is what the
|
||||
# control says it does; the fit scenarios prove it happens only
|
||||
# when asked.
|
||||
judge "Fill cut the edges off, as it says it will" '(( $(field pink_pixels) == 0 ))'
|
||||
elif [[ "$color" == gray ]]; then
|
||||
judge "the corner mark is on the paper, in grey — nothing cut off" '[[ "$(field mark_inked)" == True ]]'
|
||||
judge "and the page is grey, as the preview showed (colour spread $(field colour_spread))" \
|
||||
'(( $(field colour_spread) == 0 ))'
|
||||
judge "and it went in black and white, by the driver's own option" \
|
||||
'[[ "$(job_options $jobs)" == *ColorModel=Grayscale* && "$(job_device $jobs)" == *pxlmono* ]]'
|
||||
else
|
||||
judge "the pink corner mark is on the paper — nothing cut off" '(( $(field pink_pixels) > 0 ))'
|
||||
fi
|
||||
}
|
||||
exited_cleanly() { said 'exit=0' && ! said 'Traceback' && ! said 'Segmentation' && ! said 'Aborted'; }
|
||||
|
||||
# Where a picture goes, measured on the page CUPS was given:
|
||||
@@ -125,12 +180,36 @@ PICTURES=(
|
||||
picture:ark-wide.png:100:centre:portrait:fill
|
||||
picture:ark-wide.jpg:75:right:auto:fit:gray
|
||||
picture:ark-clear.png:50:top:auto:fit
|
||||
picture:ark-wide.png:50:top-left:auto:fit:gray
|
||||
)
|
||||
|
||||
# Black and white shown as it prints, and Color again (0.2.6.3).
|
||||
COLOURS=(bw-web bw-pdf bw-colour-again bw-cancel)
|
||||
|
||||
# Right-click in a signed-in lesson (0.2.6.3):
|
||||
# rclick-image:ELEMENT:SIZE:POSITION[:gray], the elements in
|
||||
# wonderink-lesson-site.py.
|
||||
RIGHT_CLICKS=(
|
||||
rclick-page
|
||||
rclick-page-gray
|
||||
rclick-image:private-jpg:50:top-left
|
||||
rclick-image:private-png:100:centre
|
||||
rclick-image:clear:50:top-left
|
||||
rclick-image:once:100:centre
|
||||
rclick-image:cross:75:right
|
||||
rclick-image:blob:100:centre
|
||||
rclick-image:webp:100:centre
|
||||
rclick-image:canvas:100:centre
|
||||
rclick-image:linked:33:bottom-right
|
||||
rclick-image:private-jpg:50:top-left:gray
|
||||
rclick-broken
|
||||
rclick-cancel
|
||||
)
|
||||
|
||||
scenarios=("$@")
|
||||
[[ ${#scenarios[@]} -gt 0 ]] || scenarios=(web pdf pdf-options pdf-viewer-button inline
|
||||
options bad-range broken cancel page-closes-itself close-window page-prints-again repeat
|
||||
dark-page "${PICTURES[@]}")
|
||||
dark-page "${PICTURES[@]}" "${COLOURS[@]}" "${RIGHT_CLICKS[@]}")
|
||||
|
||||
# The inline scenario needs a PDF to serve; the worksheet scenario writes one.
|
||||
lab 'test -f /opt/wonderink-check/www/ark.pdf' || { run pdf >/dev/null; lab 'cp "/home/teacher/Downloads/Wonder Ink/Colour the Ark.pdf" /opt/wonderink-check/www/ark.pdf; chmod a+r /opt/wonderink-check/www/ark.pdf'; }
|
||||
@@ -204,31 +283,98 @@ for scenario in "${scenarios[@]}"; do
|
||||
IFS=: read -r _ file size position paper fill color <<< "$scenario"
|
||||
judge "the picture controls appeared, and none for a page" "said 'PICTURE picture_box=True'"
|
||||
judge "the print interface says Color" "said \"labels='Color','Color'\""
|
||||
judge "one job reached CUPS" '[[ $(count) -eq 1 ]]'
|
||||
spool=$(spool_file "$jobs")
|
||||
judge "CUPS was given exactly the page the preview showed" \
|
||||
"[[ -n '$spool' ]] && lab 'cmp -s /tmp/rig-out/preview.pdf $spool'"
|
||||
dims=$(lab "python3 -c 'from PyQt6.QtGui import QImage; i=QImage(\"/opt/wonderink-check/www/$file\"); print(f\"{i.width()}x{i.height()}\")'" 2>/dev/null)
|
||||
result=$(lab "python3 $MEASURE measure $spool $dims $size $position $paper $fill")
|
||||
field() { python3 -c "import json,sys; print(json.loads(sys.argv[1])[sys.argv[2]])" "$result" "$1"; }
|
||||
judge "the paper is turned the way asked ($paper): $(field page)" \
|
||||
'[[ "$(field page_landscape)" == "$(field wanted_landscape)" ]]'
|
||||
judge "the picture is where and as large as asked, within 3 points (off by $(field off_by_points))" \
|
||||
'[[ "$(field off_by_points)" != None ]] && (( $(field off_by_points) <= 3 ))'
|
||||
judge "no ink anywhere else on the page — no bars ($(field ink_outside_picture) pixels)" \
|
||||
'[[ "$(field ink_outside_picture)" == 0 ]]'
|
||||
if [[ "$fill" == fill ]]; then
|
||||
# The mark is in the picture's left fifth, which filling a
|
||||
# portrait page with a wide picture cuts off. That is what the
|
||||
# control says it does; the fit scenarios prove it happens only
|
||||
# when asked.
|
||||
judge "Fill cut the edges off, as it says it will" '(( $(field pink_pixels) == 0 ))'
|
||||
else
|
||||
judge "the pink corner mark is on the paper — nothing cut off" '(( $(field pink_pixels) > 0 ))'
|
||||
fi
|
||||
if [[ "$color" == gray ]]; then
|
||||
judge "and it went in black and white" '[[ "$(job_device $jobs)" == *pxlmono* ]]'
|
||||
judge "the preview shows it in black and white" "said 'shown_gray=True'"
|
||||
fi
|
||||
judge "one job reached CUPS" '[[ $(count) -eq 1 ]]'
|
||||
dims=$(lab "python3 -c 'from PyQt6.QtGui import QImage; i=QImage(\"/opt/wonderink-check/www/$file\"); print(f\"{i.width()}x{i.height()}\")'" 2>/dev/null)
|
||||
judge_layout "$dims" "$size" "$position" "$paper" "$fill" "$color" ;;
|
||||
bw-*)
|
||||
judge "the preview starts in colour" "said 'COLOUR FIRST shown_gray=False'"
|
||||
if [[ "$scenario" != bw-colour-again ]]; then
|
||||
judge "Black and white shows a grey page, every time it is chosen" \
|
||||
'! lines "want=gray" | grep -qv "shown_gray=True" && lines "want=gray" | grep -q .'
|
||||
fi
|
||||
if lines "want=colour" | grep -q .; then
|
||||
judge "Color shows the colour page again, every time" '! lines "want=colour" | grep -qv "shown_gray=False"'
|
||||
fi
|
||||
case "$scenario" in
|
||||
bw-web)
|
||||
judge "turned over and over, it stays on page 2 of 3" '! lines TOGGLE | grep -qv "page=2/3"'
|
||||
judge "black and white is made once, and the same file shown each time" \
|
||||
'[[ $(lines "want=gray" | grep -o "pdf=[^ ]*" | sort -u | wc -l) -eq 1 ]]'
|
||||
judge "and shown again at once (after the first: $(lines 'want=gray' | tail -n +2 | grep -o 'took=[0-9.]*' | tr '\n' ' '))" \
|
||||
'! lines "want=gray" | tail -n +2 | grep -o "took=[0-9.]*" | cut -d= -f2 | grep -qv "^0\.[0-4]"'
|
||||
judge "one job" '[[ $(count) -eq 1 ]]'
|
||||
judge "page 2 only, two copies" \
|
||||
'[[ "$(job_options $jobs)" == *page-ranges=2* && "$(job_options $jobs)" == *"argv[4]=\"2\""* ]]'
|
||||
judge "in black and white, by the driver's own option" \
|
||||
'[[ "$(job_options $jobs)" == *ColorModel=Grayscale* && "$(job_device $jobs)" == *pxlmono* ]]' ;;
|
||||
bw-pdf)
|
||||
judge "one job" '[[ $(count) -eq 1 ]]'
|
||||
judge "in black and white, by the driver's own option" \
|
||||
'[[ "$(job_options $jobs)" == *ColorModel=Grayscale* && "$(job_device $jobs)" == *pxlmono* ]]' ;;
|
||||
bw-colour-again)
|
||||
judge "one job" '[[ $(count) -eq 1 ]]'
|
||||
judge "in colour, with no grey option" \
|
||||
'[[ "$(job_options $jobs)" != *ColorModel=Grayscale* && "$(job_device $jobs)" == *pxlcolor* ]]' ;;
|
||||
bw-cancel)
|
||||
judge "Cancel sends nothing" '[[ $(count) -eq 0 ]]'
|
||||
judge "and nothing was left behind" "said 'scratch_left=\[\]'" ;;
|
||||
esac
|
||||
if [[ $(count) -eq 1 ]]; then
|
||||
spool=$(spool_file "$jobs")
|
||||
judge "CUPS was given exactly the file the preview showed" "lab 'cmp -s /tmp/rig-out/preview.pdf $spool'"
|
||||
answer=$(colour_of "$spool")
|
||||
if [[ "$scenario" == bw-colour-again ]]; then
|
||||
judge "and it is in colour ($answer)" '! is_gray "$answer"'
|
||||
else
|
||||
judge "and every page of it is grey ($answer)" 'is_gray "$answer"'
|
||||
fi
|
||||
fi ;;
|
||||
rclick-page)
|
||||
judge "right-click on the words offers Print page, and not Print image" \
|
||||
"said \"MENU .*'Print page…'\" && ! said \"'Print image…'\""
|
||||
judge "which prints the lesson page, not a picture" "said 'PREVIEW picture=False'"
|
||||
judge "one job" '[[ $(count) -eq 1 ]]'
|
||||
judge "CUPS was given exactly the page the preview showed" \
|
||||
"lab 'cmp -s /tmp/rig-out/preview.pdf $(spool_file "$jobs")'"
|
||||
judge "the teacher's clipboard is untouched" teacher_text_kept ;;
|
||||
rclick-page-gray)
|
||||
judge "a lesson page full of pictures turns grey in the preview ($(lines 'GRAY PAGE' | grep -o 'took=[0-9.]*'))" \
|
||||
"said 'GRAY PAGE .*shown_gray=True'"
|
||||
judge "one job" '[[ $(count) -eq 1 ]]'
|
||||
spool=$(spool_file "$jobs")
|
||||
judge "CUPS was given exactly the file the preview showed" "lab 'cmp -s /tmp/rig-out/preview.pdf $spool'"
|
||||
answer=$(colour_of "$spool")
|
||||
judge "and every page of it is grey ($answer)" 'is_gray "$answer"'
|
||||
judge "in black and white, by the driver's own option" \
|
||||
'[[ "$(job_options $jobs)" == *ColorModel=Grayscale* && "$(job_device $jobs)" == *pxlmono* ]]' ;;
|
||||
rclick-image:*)
|
||||
IFS=: read -r _ element size position color <<< "$scenario"
|
||||
judge "right-click on the picture offers Print image and Print page" \
|
||||
"said \"MENU .*'Print image…', 'Print page…'\""
|
||||
judge "Print image opens the picture layout" "said 'PICTURE picture_box=True'"
|
||||
judge "named for the lesson it came from" "said \"title='Picture from Picture lesson'\""
|
||||
judge "nothing was fetched again to get it ($(fetched_since) requests)" '[[ $(fetched_since) -eq 0 ]]'
|
||||
judge "the teacher's clipboard is theirs again" teacher_text_kept
|
||||
if [[ "$color" == gray ]]; then
|
||||
judge "the preview shows it in black and white" "said 'shown_gray=True'"
|
||||
fi
|
||||
judge "one job reached CUPS" '[[ $(count) -eq 1 ]]'
|
||||
dims=$(grep -o 'image=[0-9]*x[0-9]*' <<< "$out" | head -1 | cut -d= -f2)
|
||||
judge_layout "$dims" "$size" "$position" auto fit "$color" ;;
|
||||
rclick-broken)
|
||||
judge "a picture that never loaded is still offered as a picture" "said \"MENU .*'Print image…'\""
|
||||
judge "and the teacher is told it could not be taken, with Print off" \
|
||||
"said \"BROKEN status='This picture could not be taken from the page\" && said 'print_enabled=False pages=0 picture=False'"
|
||||
judge "the whole page is not printed instead" '[[ $(count) -eq 0 ]]'
|
||||
judge "the teacher's clipboard is untouched" teacher_text_kept ;;
|
||||
rclick-cancel)
|
||||
judge "the picture preview came up" "said 'PREVIEW picture=True'"
|
||||
judge "Cancel sends nothing" '[[ $(count) -eq 0 ]]'
|
||||
judge "the teacher's clipboard is theirs again" teacher_text_kept
|
||||
judge "and nothing was left behind" "said 'scratch_left=\[\]'" ;;
|
||||
esac
|
||||
|
||||
[[ "$failed" -eq 0 ]] || { echo "$out" | tail -15 | sed 's/^/ /'; }
|
||||
|
||||
@@ -59,7 +59,53 @@ def when(cond, then, label, limit=30):
|
||||
def preview(): return window._preview
|
||||
def ready(): return preview() is not None and preview()._pdf is not None
|
||||
def describe(d):
|
||||
return f"picture={d._picture_box.isVisible()} shown={d._page in d._images} pages={d._pages} printers={[d._printer.itemText(i) for i in range(d._printer.count())]} paper={d.paper()} colour_offered={d._colour.isVisible()} status={d._status.text()!r}"
|
||||
return f"picture={d._picture_box.isVisible()} shown={(d._pdf, d._page) in d._images} pages={d._pages} printers={[d._printer.itemText(i) for i in range(d._printer.count())]} paper={d.paper()} colour_offered={d._colour.isVisible()} status={d._status.text()!r}"
|
||||
|
||||
def shown_gray():
|
||||
"""Is the page the preview is drawing grey, pixel for pixel?"""
|
||||
pixmap = preview()._view._pixmap
|
||||
return None if pixmap is None else pixmap.toImage().allGray()
|
||||
|
||||
def keep_preview():
|
||||
import shutil
|
||||
out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True)
|
||||
shutil.copy(preview()._pdf, out / "preview.pdf")
|
||||
|
||||
def toggles(sequence, then):
|
||||
"""Color (0) and Black and white (1) in turn, as a teacher would, each shown before the next."""
|
||||
def step(i):
|
||||
if i == len(sequence): return then()
|
||||
start = time.monotonic(); preview()._colour.setCurrentIndex(sequence[i])
|
||||
def shown():
|
||||
d = preview()
|
||||
stamp(f"TOGGLE {i} want={'gray' if sequence[i] else 'colour'} took={time.monotonic() - start:.2f} "
|
||||
f"pdf={d._pdf.name} source={d._source.name} page={d._page}/{d._pages} shown_gray={shown_gray()}")
|
||||
step(i + 1)
|
||||
when(ready, shown, f"toggle {i}")
|
||||
step(0)
|
||||
|
||||
def colour_lesson(view, title="Rainbow lesson"):
|
||||
# Backgrounds do not print, so the colour is in the words and the drawings.
|
||||
shapes = ("<svg width='600' height='120'><circle cx='60' cy='60' r='50' fill='#e00000'/>"
|
||||
"<rect x='130' y='10' width='200' height='100' fill='#0050e0'/>"
|
||||
"<circle cx='450' cy='60' r='50' fill='#00a000'/></svg>")
|
||||
view.setHtml(f"<html><head><title>{title}</title></head><body>" + "".join(
|
||||
f"<h1 style='color:#c00'>{title}, part {i}</h1>{shapes}"
|
||||
f"<p style='color:#060'>{'God put a rainbow in the sky. ' * 60}</p>" for i in range(4)
|
||||
) + "</body></html>", QUrl("https://app.wonderink.org/rainbow"))
|
||||
|
||||
def colour_worksheet(name="Colour the Rainbow"):
|
||||
from PyQt6.QtGui import QColor
|
||||
d = wi.DOWNLOADS; d.mkdir(parents=True, exist_ok=True)
|
||||
p = d / f"{name}.pdf"
|
||||
w = QPdfWriter(str(p)); w.setPageSize(QPageSize(QPageSize.PageSizeId.Letter)); w.setResolution(300)
|
||||
pa = QPainter(w); pa.setFont(QFont("DejaVu Sans", 28))
|
||||
for page, colours in enumerate((("#e00000", "#ff9900"), ("#00a000", "#0050e0"))):
|
||||
if page: w.newPage()
|
||||
pa.setPen(QColor(colours[0])); pa.drawText(200, 400, f"WORKSHEET {name} page {page + 1}")
|
||||
pa.fillRect(200, 600, 1000, 800, QColor(colours[1]))
|
||||
pa.end()
|
||||
return p
|
||||
|
||||
def finish(code=0):
|
||||
stamp(f"RESULT notes={notes} preview_open={preview() is not None} tabs={window._tabs.count()}")
|
||||
@@ -76,7 +122,8 @@ actions = {
|
||||
"cancel": lambda view: when(ready, lambda: (stamp("PREVIEW " + describe(preview())), preview()._cancel.click(), finish()), "preview"),
|
||||
"options": lambda view: when(ready, lambda: (
|
||||
preview()._which.setCurrentIndex(1), preview()._range.setText("2-3"),
|
||||
preview()._copies.setValue(2), preview()._colour.setCurrentIndex(1), press_print()), "preview"),
|
||||
preview()._copies.setValue(2), preview()._colour.setCurrentIndex(1),
|
||||
when(ready, press_print, "black and white")), "preview"),
|
||||
"bad-range": lambda view: when(ready, lambda: (
|
||||
preview()._which.setCurrentIndex(1), preview()._range.setText("7"), preview()._print.click(),
|
||||
stamp(f"bad range: open={preview() is not None} status={preview()._status.text()!r}"),
|
||||
@@ -100,7 +147,120 @@ actions = {
|
||||
QTimer.singleShot(800, lambda: (stamp(f"second request while open: still one preview={preview() is not None}"), press_print()))), "preview"),
|
||||
}
|
||||
|
||||
if scenario.startswith("picture:"):
|
||||
SITE = "http://127.0.0.1:8094"
|
||||
|
||||
if scenario.startswith("bw-"):
|
||||
# bw-web: a colourful lesson, turned to page 2, Color and Black and white
|
||||
# back and forth, then page 2 twice in black and white.
|
||||
# bw-pdf: a colourful worksheet, all of it, in black and white.
|
||||
# bw-colour-again: black and white, then back to Color, then Print.
|
||||
# bw-cancel: black and white, then Cancel.
|
||||
if scenario == "bw-pdf":
|
||||
window.open_pdf(colour_worksheet()); view = window.current()
|
||||
else:
|
||||
view = window.open_tab(); colour_lesson(view)
|
||||
def colour_first():
|
||||
stamp(f"COLOUR FIRST shown_gray={shown_gray()} pdf={preview()._pdf.name}")
|
||||
if scenario == "bw-web":
|
||||
preview()._turn(1)
|
||||
toggles([1, 0, 1, 0, 1], lambda: (
|
||||
preview()._which.setCurrentIndex(1), preview()._range.setText("2"),
|
||||
preview()._copies.setValue(2), keep_preview(), press_print()))
|
||||
elif scenario == "bw-pdf":
|
||||
toggles([1], lambda: (keep_preview(), press_print()))
|
||||
elif scenario == "bw-colour-again":
|
||||
toggles([1, 0], lambda: (keep_preview(), press_print()))
|
||||
else:
|
||||
toggles([1], lambda: (stamp("PREVIEW " + describe(preview())), preview()._cancel.click(), finish()))
|
||||
begin = lambda *_: (QTimer.singleShot(0, lambda: window.print_view(view)), when(ready, colour_first, "preview"))
|
||||
if scenario == "bw-pdf":
|
||||
QTimer.singleShot(2500, begin)
|
||||
else:
|
||||
view.loadFinished.connect(begin)
|
||||
elif scenario.startswith("rclick"):
|
||||
# A lesson behind a sign-in, with pictures in it; a real right-click.
|
||||
# rclick-page on the words: the page prints
|
||||
# rclick-image:ID:SIZE:POSITION[:gray] on a picture: that picture prints
|
||||
# rclick-broken a picture that never loaded
|
||||
# rclick-cancel a picture, then Cancel
|
||||
from PyQt6.QtCore import QEvent, QPointF, Qt
|
||||
from PyQt6.QtGui import QMouseEvent
|
||||
from PyQt6.QtWidgets import QApplication as Application
|
||||
parts = scenario.split(":")
|
||||
target = {"rclick-page": "words", "rclick-page-gray": "words", "rclick-broken": "broken",
|
||||
"rclick-cancel": "private-jpg"}.get(parts[0], parts[1] if len(parts) > 1 else "")
|
||||
clipboard = Application.clipboard()
|
||||
view = window.open_tab(); view.load(QUrl(f"{SITE}/login"))
|
||||
page_ready = []
|
||||
def poll_ready():
|
||||
if view.url().path() == "/lesson":
|
||||
view.page().runJavaScript("window.__ready === true", lambda ok: page_ready.append(1) if ok else None)
|
||||
if not page_ready: QTimer.singleShot(200, poll_ready)
|
||||
QTimer.singleShot(500, poll_ready)
|
||||
def right_click(then):
|
||||
js = ("(function(){var e=document.getElementById(%r);e.scrollIntoView({block:'center'});"
|
||||
"var r=e.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()" % target)
|
||||
def at(xy):
|
||||
zoom = view.zoomFactor(); x, y = xy[0] * zoom, xy[1] * zoom
|
||||
clipboard.setText("TEACHER'S TEXT, John 3:16")
|
||||
stamp(f"RCLICK {target} at {x:.0f},{y:.0f} since={time.time():.3f}")
|
||||
for kind, buttons in ((QEvent.Type.MouseButtonPress, Qt.MouseButton.RightButton),
|
||||
(QEvent.Type.MouseButtonRelease, Qt.MouseButton.NoButton)):
|
||||
Application.sendEvent(view.focusProxy(), QMouseEvent(
|
||||
kind, QPointF(x, y), QPointF(view.mapToGlobal(QPointF(x, y))),
|
||||
Qt.MouseButton.RightButton, buttons, Qt.KeyboardModifier.NoModifier))
|
||||
when(lambda: Application.activePopupWidget() is not None,
|
||||
lambda: then(Application.activePopupWidget()), "menu")
|
||||
view.page().runJavaScript(js, at)
|
||||
def choose(menu, text, after):
|
||||
items = [a.text() for a in menu.actions() if a.text()]
|
||||
stamp(f"MENU {items}")
|
||||
action = next((a for a in menu.actions() if a.text() == text), None)
|
||||
menu.close()
|
||||
if action is None:
|
||||
stamp(f"NO {text!r} IN THE MENU"); return finish(5)
|
||||
after()
|
||||
QTimer.singleShot(0, action.trigger)
|
||||
def picture_ready():
|
||||
d = preview(); return d is not None and d._picture is not None and d._pdf is not None
|
||||
def clipboard_line():
|
||||
stamp(f"CLIPBOARD {clipboard.text()!r}")
|
||||
if parts[0] == "rclick-page":
|
||||
go = lambda: right_click(lambda m: choose(m, "Print page…", lambda: when(ready, lambda: (
|
||||
clipboard_line(), keep_preview(), press_print()), "page preview")))
|
||||
elif parts[0] == "rclick-page-gray":
|
||||
# The page with all its pictures, in black and white: the slow case.
|
||||
def gray_page():
|
||||
start = time.monotonic(); preview()._colour.setCurrentIndex(1)
|
||||
when(ready, lambda: (
|
||||
stamp(f"GRAY PAGE took={time.monotonic() - start:.1f} shown_gray={shown_gray()} pages={preview()._pages}"),
|
||||
keep_preview(), press_print()), "black and white", limit=70)
|
||||
go = lambda: right_click(lambda m: choose(m, "Print page…", lambda: when(ready, gray_page, "page preview")))
|
||||
elif parts[0] == "rclick-broken":
|
||||
go = lambda: right_click(lambda m: choose(m, "Print image…", lambda: when(
|
||||
lambda: preview() is not None and "could not be taken" in preview()._status.text(), lambda: (
|
||||
stamp(f"BROKEN status={preview()._status.text()!r} print_enabled={preview()._print.isEnabled()} "
|
||||
f"pages={preview()._pages} picture={preview()._picture is not None}"),
|
||||
clipboard_line(), preview()._cancel.click(), finish()), "the picture failing", limit=15)))
|
||||
elif parts[0] == "rclick-cancel":
|
||||
go = lambda: right_click(lambda m: choose(m, "Print image…", lambda: when(picture_ready, lambda: (
|
||||
stamp("PREVIEW " + describe(preview())), clipboard_line(), preview()._cancel.click(), finish()),
|
||||
"picture preview")))
|
||||
else:
|
||||
_, _, size, position, *colour = parts
|
||||
def lay_out():
|
||||
d = preview()
|
||||
d._size.setCurrentIndex(d._size.findData(int(size)))
|
||||
d._place.choose(position.replace("-", " "))
|
||||
if colour == ["gray"]:
|
||||
d._colour.setCurrentIndex(1)
|
||||
QTimer.singleShot(900, lambda: when(ready, lambda: (
|
||||
stamp(f"PICTURE picture_box={d._picture_box.isVisible()} image={d._picture.width()}x{d._picture.height()} "
|
||||
f"title={d._title!r} shown_gray={shown_gray()}"),
|
||||
clipboard_line(), keep_preview(), press_print()), "layout"))
|
||||
go = lambda: right_click(lambda m: choose(m, "Print image…", lambda: when(picture_ready, lay_out, "picture preview")))
|
||||
when(lambda: bool(page_ready), go, "the lesson and its pictures", limit=30)
|
||||
elif scenario.startswith("picture:"):
|
||||
# picture:FILE:SIZE:POSITION:PAPER:FILL[:color]
|
||||
# Opens a picture on its own, chooses the layout with the preview's own
|
||||
# controls, keeps the exact PDF the preview showed, then prints it.
|
||||
@@ -117,14 +277,16 @@ if scenario.startswith("picture:"):
|
||||
d._fill.setChecked(fill == "fill")
|
||||
if rest and rest[0] == "gray":
|
||||
d._colour.setCurrentIndex(1)
|
||||
QTimer.singleShot(900, keep_and_print)
|
||||
# The layout settles (120 ms), then black and white if asked for.
|
||||
QTimer.singleShot(900, lambda: when(ready, keep_and_print, "layout"))
|
||||
def keep_and_print():
|
||||
import shutil
|
||||
d = preview()
|
||||
out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True)
|
||||
shutil.copy(d._pdf, out / "preview.pdf")
|
||||
stamp(f"PICTURE picture_box={d._picture_box.isVisible()} size_enabled={d._size.isEnabled()} "
|
||||
f"image={d._picture.width()}x{d._picture.height()} labels={d._colour_label.text()!r},{d._colour.itemText(0)!r}")
|
||||
f"image={d._picture.width()}x{d._picture.height()} labels={d._colour_label.text()!r},{d._colour.itemText(0)!r} "
|
||||
f"shown_gray={shown_gray()}")
|
||||
press_print()
|
||||
QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
||||
when(ready_picture, choose, "picture preview")))
|
||||
@@ -154,7 +316,8 @@ elif scenario in ("pdf", "pdf-options"):
|
||||
if scenario == "pdf-options":
|
||||
actions["pdf-options"] = lambda view: when(ready, lambda: (
|
||||
preview()._which.setCurrentIndex(1), preview()._range.setText("2"),
|
||||
preview()._copies.setValue(3), preview()._colour.setCurrentIndex(1), press_print()), "preview")
|
||||
preview()._copies.setValue(3), preview()._colour.setCurrentIndex(1),
|
||||
when(ready, press_print, "black and white")), "preview")
|
||||
QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
||||
actions[scenario if scenario == "pdf-options" else "web"](view)))
|
||||
elif scenario == "pdf-viewer-button":
|
||||
|
||||
Reference in New Issue
Block a user