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 make DIRECTORY
|
||||||
wonderink-picture-measure.py measure PDF WIDTHxHEIGHT SIZE POSITION PAPER FILL
|
wonderink-picture-measure.py measure PDF WIDTHxHEIGHT SIZE POSITION PAPER FILL
|
||||||
wonderink-picture-measure.py darkness PDF
|
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
|
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
|
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
|
# Transparent around the drawing: on paper that has to be white, not
|
||||||
# black, which is what a careless composite would make of it.
|
# black, which is what a careless composite would make of it.
|
||||||
art(1200, 1200, "ark-clear.png", "Clear", transparent=True)
|
art(1200, 1200, "ark-clear.png", "Clear", transparent=True)
|
||||||
|
webp(directory)
|
||||||
|
|
||||||
|
|
||||||
def page_pixels(pdf):
|
def webp(directory):
|
||||||
ppm = subprocess.run(["pdftoppm", "-r", "72", "-f", "1", "-l", "1", "-singlefile", pdf],
|
"""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
|
capture_output=True, check=True).stdout
|
||||||
# P6 header: magic, width, height, maxval, then raw RGB.
|
# P6 header: magic, width, height, maxval, then raw RGB.
|
||||||
fields, index = [], 0
|
fields, index = [], 0
|
||||||
@@ -106,6 +139,19 @@ def measure(pdf, picture, size, position, paper, fill):
|
|||||||
if magenta:
|
if magenta:
|
||||||
pink += 1
|
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
|
found = (min(xs), min(ys), max(xs) + 1, max(ys) + 1) if xs else None
|
||||||
expected = (round(left), round(top), round(right), round(bottom))
|
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
|
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,
|
"off_by_points": off,
|
||||||
"ink_outside_picture": outside,
|
"ink_outside_picture": outside,
|
||||||
"pink_pixels": pink,
|
"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):
|
def darkness(pdf):
|
||||||
"""How much of the first page is dark, 0 to 1."""
|
"""How much of the first page is dark, 0 to 1."""
|
||||||
width, height, rgb = page_pixels(pdf)
|
width, height, rgb = page_pixels(pdf)
|
||||||
@@ -140,5 +201,7 @@ if __name__ == "__main__":
|
|||||||
make(sys.argv[2])
|
make(sys.argv[2])
|
||||||
elif sys.argv[1] == "darkness":
|
elif sys.argv[1] == "darkness":
|
||||||
darkness(sys.argv[2])
|
darkness(sys.argv[2])
|
||||||
|
elif sys.argv[1] == "colour":
|
||||||
|
colour(sys.argv[2])
|
||||||
else:
|
else:
|
||||||
measure(*sys.argv[2:])
|
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
|
docker cp "$ROOT/tools/lab/wonderink-print-rig.py" "$NAME:$RIG" >/dev/null
|
||||||
MEASURE=/opt/wonderink-check/wonderink-picture-measure.py
|
MEASURE=/opt/wonderink-check/wonderink-picture-measure.py
|
||||||
docker cp "$ROOT/tools/lab/wonderink-picture-measure.py" "$NAME:$MEASURE" >/dev/null
|
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" || {
|
lab "chmod -R a+rX /opt/wonderink-check && test -r $RIG" || {
|
||||||
echo "could not put the rig in the lab" >&2
|
echo "could not put the rig in the lab" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -70,9 +73,10 @@ mkdir -p /opt/wonderink-check/www
|
|||||||
cupsctl PreserveJobFiles=Yes
|
cupsctl PreserveJobFiles=Yes
|
||||||
' || { echo "could not prepare the lab's CUPS" >&2; exit 1; }
|
' || { echo "could not prepare the lab's CUPS" >&2; exit 1; }
|
||||||
|
|
||||||
lab "test -f /opt/wonderink-check/www/ark-clear.png || \
|
lab "test -f /opt/wonderink-check/www/ark-wide.webp || \
|
||||||
QT_QPA_PLATFORM=offscreen python3 $MEASURE make /opt/wonderink-check/www; \
|
QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS='--no-sandbox --disable-gpu' \
|
||||||
chmod -R a+r /opt/wonderink-check/www; test -f /opt/wonderink-check/www/ark-clear.png" || {
|
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
|
echo "could not make the test pictures" >&2
|
||||||
exit 1
|
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' || \
|
lab 'pgrep -f "[h]ttp.server 8089" >/dev/null' || \
|
||||||
docker exec -d -w /opt/wonderink-check/www "$NAME" python3 -m http.server 8089
|
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
|
passed=0
|
||||||
failed=0
|
failed=0
|
||||||
|
|
||||||
@@ -112,6 +125,48 @@ count() { wc -w <<< "$jobs"; }
|
|||||||
# The file CUPS was handed for a job, kept by PreserveJobFiles.
|
# The file CUPS was handed for a job, kept by PreserveJobFiles.
|
||||||
spool_file() { local id="${1%% *}"; printf '/var/spool/cups/d%05d-001' "${id##*-}"; }
|
spool_file() { local id="${1%% *}"; printf '/var/spool/cups/d%05d-001' "${id##*-}"; }
|
||||||
said() { grep -q -- "$1" <<< "$out"; }
|
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'; }
|
exited_cleanly() { said 'exit=0' && ! said 'Traceback' && ! said 'Segmentation' && ! said 'Aborted'; }
|
||||||
|
|
||||||
# Where a picture goes, measured on the page CUPS was given:
|
# 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.png:100:centre:portrait:fill
|
||||||
picture:ark-wide.jpg:75:right:auto:fit:gray
|
picture:ark-wide.jpg:75:right:auto:fit:gray
|
||||||
picture:ark-clear.png:50:top:auto:fit
|
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=("$@")
|
||||||
[[ ${#scenarios[@]} -gt 0 ]] || scenarios=(web pdf pdf-options pdf-viewer-button inline
|
[[ ${#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
|
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.
|
# 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'; }
|
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"
|
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 picture controls appeared, and none for a page" "said 'PICTURE picture_box=True'"
|
||||||
judge "the print interface says Color" "said \"labels='Color','Color'\""
|
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
|
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 ;;
|
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
|
esac
|
||||||
|
|
||||||
[[ "$failed" -eq 0 ]] || { echo "$out" | tail -15 | sed 's/^/ /'; }
|
[[ "$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 preview(): return window._preview
|
||||||
def ready(): return preview() is not None and preview()._pdf is not None
|
def ready(): return preview() is not None and preview()._pdf is not None
|
||||||
def describe(d):
|
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):
|
def finish(code=0):
|
||||||
stamp(f"RESULT notes={notes} preview_open={preview() is not None} tabs={window._tabs.count()}")
|
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"),
|
"cancel": lambda view: when(ready, lambda: (stamp("PREVIEW " + describe(preview())), preview()._cancel.click(), finish()), "preview"),
|
||||||
"options": lambda view: when(ready, lambda: (
|
"options": lambda view: when(ready, lambda: (
|
||||||
preview()._which.setCurrentIndex(1), preview()._range.setText("2-3"),
|
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: (
|
"bad-range": lambda view: when(ready, lambda: (
|
||||||
preview()._which.setCurrentIndex(1), preview()._range.setText("7"), preview()._print.click(),
|
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}"),
|
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"),
|
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]
|
# picture:FILE:SIZE:POSITION:PAPER:FILL[:color]
|
||||||
# Opens a picture on its own, chooses the layout with the preview's own
|
# 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.
|
# controls, keeps the exact PDF the preview showed, then prints it.
|
||||||
@@ -117,14 +277,16 @@ if scenario.startswith("picture:"):
|
|||||||
d._fill.setChecked(fill == "fill")
|
d._fill.setChecked(fill == "fill")
|
||||||
if rest and rest[0] == "gray":
|
if rest and rest[0] == "gray":
|
||||||
d._colour.setCurrentIndex(1)
|
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():
|
def keep_and_print():
|
||||||
import shutil
|
import shutil
|
||||||
d = preview()
|
d = preview()
|
||||||
out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True)
|
out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True)
|
||||||
shutil.copy(d._pdf, out / "preview.pdf")
|
shutil.copy(d._pdf, out / "preview.pdf")
|
||||||
stamp(f"PICTURE picture_box={d._picture_box.isVisible()} size_enabled={d._size.isEnabled()} "
|
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()
|
press_print()
|
||||||
QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
||||||
when(ready_picture, choose, "picture preview")))
|
when(ready_picture, choose, "picture preview")))
|
||||||
@@ -154,7 +316,8 @@ elif scenario in ("pdf", "pdf-options"):
|
|||||||
if scenario == "pdf-options":
|
if scenario == "pdf-options":
|
||||||
actions["pdf-options"] = lambda view: when(ready, lambda: (
|
actions["pdf-options"] = lambda view: when(ready, lambda: (
|
||||||
preview()._which.setCurrentIndex(1), preview()._range.setText("2"),
|
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)),
|
QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
||||||
actions[scenario if scenario == "pdf-options" else "web"](view)))
|
actions[scenario if scenario == "pdf-options" else "web"](view)))
|
||||||
elif scenario == "pdf-viewer-button":
|
elif scenario == "pdf-viewer-button":
|
||||||
|
|||||||
Reference in New Issue
Block a user