fix: a picture prints as the picture, where and as large as the teacher asks
The black bars above and below a printed picture were neither in the picture nor in the layout. A PNG or JPEG in a tab of its own is shown by the engine on a viewer page whose background is rgb(14, 14, 14), and Qt prints page backgrounds by default; 0.2.6.1 printed that page. A picture is now taken as itself — drawn onto white, capped at 300 dpi across Letter — and laid out on the printer's paper by the preview, with Size (as large as fits down to 25%), Position (nine squares), Paper (automatic, upright, sideways) and Fill the whole page, which says it cuts the edges off and is never the default. The preview is the PDF that prints, laid out again after each change. Worksheet PDFs and lesson pages get no picture controls and are unchanged. Page backgrounds are no longer printed for lesson pages either, as in Chrome; Blink darkens light text when it does that, so a white-on-black page prints readable grey on white. "Colour" is "Color" in the print interface and the manual. Lab: 135 print checks, now including eight picture layouts measured on the job file CUPS kept — byte-identical to the preview, within 1 point of where asked, no ink anywhere else. Printed from the real session with the pointer: 50% top left, Elmer-66, the same. Not yet on paper. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6946bae455
commit
940f63e0e5
@@ -36,7 +36,14 @@ and choose **Print…**. You will see the pages exactly as they will print.
|
||||
|
||||
* Choose **Only some pages** to print, for example, `2` or `1-3`.
|
||||
* Set **Copies** for the number of children.
|
||||
* Choose **Black and white** to save colour ink.
|
||||
* Choose **Black and white** to save color ink.
|
||||
|
||||
**Printing a picture.** When the tab is a picture on its own, the
|
||||
preview also shows **Picture**: choose its **Size** (for example 50%),
|
||||
click one of the nine **Position** squares to put it in a corner or the
|
||||
middle, and choose **Paper** upright or sideways. The preview changes as
|
||||
you choose, and shows exactly what will print. **Fill the whole page**
|
||||
makes it cover the paper but cuts off the edges.
|
||||
|
||||
Click **Print**. Wonder Ink says when the printer has it and goes back to
|
||||
the lesson. **Cancel** prints nothing.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
@@ -244,6 +244,39 @@ printed. Print pressed again while a preview is open does nothing, and so
|
||||
does a page calling `window.print()` meanwhile. The PDF viewer's own print
|
||||
button opens the same preview.
|
||||
|
||||
### Pictures (0.2.6.2)
|
||||
|
||||
A picture a lesson opens in a tab of its own — a colouring page as a PNG
|
||||
or JPEG — printed in 0.2.6.1 with **black bars** above and below it.
|
||||
Measured: the bars were neither in the picture nor in the page layout.
|
||||
The engine shows a lone picture on a viewer page whose background is
|
||||
rgb(14, 14, 14), scaled to the window, and Qt prints page backgrounds by
|
||||
default; the dark page went onto the paper around the artwork.
|
||||
|
||||
So a picture is no longer printed as that page. The preview takes the
|
||||
picture itself — drawn onto white, which is what transparent parts are on
|
||||
paper, at up to 3300 pixels (300 dpi across Letter) — and lays it out on
|
||||
the printer's own paper, and the teacher chooses:
|
||||
|
||||
* **Size** — as large as fits, 75%, 50%, 33% or 25% of that;
|
||||
* **Position** — nine squares, like the page itself;
|
||||
* **Paper** — automatic (landscape for a wide picture), upright or
|
||||
sideways;
|
||||
* **Fill the whole page** — covers the printable area and says it cuts the
|
||||
edges off. Nothing is cropped unless this is ticked.
|
||||
|
||||
The preview is the PDF that will print, laid out again after every change.
|
||||
`tools/lab/wonderink-print-check` measures it on the file CUPS actually
|
||||
kept (`PreserveJobFiles`), which is byte-for-byte the preview's: the
|
||||
picture within 1 point of where it was asked for, the paper turned as
|
||||
asked, and no ink anywhere else on the page. Worksheet PDFs and lesson
|
||||
pages have no picture controls; nothing about them changed.
|
||||
|
||||
Page backgrounds are also no longer printed for lesson pages, as in
|
||||
Chrome, where "Background graphics" is off unless asked for. A white-on-
|
||||
black page prints dark grey on white: Blink darkens light text when the
|
||||
background is left off, measured.
|
||||
|
||||
Temporary PDFs live in the teacher's `$XDG_RUNTIME_DIR` and are removed
|
||||
when the preview closes.
|
||||
|
||||
|
||||
@@ -225,3 +225,77 @@ def submit(pdf, printer, copies=1, pages=None, gray=None, title="Wonder Ink"):
|
||||
# without the program's name in front of it.
|
||||
message = re.sub(r"^lp:\s*", "", message) or "The printer did not accept the job."
|
||||
return False, message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- pictures
|
||||
#
|
||||
# A picture a lesson opens in a tab of its own is not a page: the engine
|
||||
# shows it on a near-black viewer page (body background rgb(14, 14, 14),
|
||||
# measured) scaled to the window, and printing that page printed the black
|
||||
# around the picture — the bars teachers saw above and below the artwork.
|
||||
# So a picture is laid out on the paper here instead, from the picture
|
||||
# itself, and the teacher chooses how large and where.
|
||||
|
||||
# How large, as a share of the largest size that fits the page.
|
||||
PICTURE_SIZES = (100, 75, 50, 33, 25)
|
||||
|
||||
# Where, as (column, row): 0 left or top, 1 centre, 2 right or bottom.
|
||||
PICTURE_POSITIONS = {
|
||||
"top left": (0, 0), "top": (1, 0), "top right": (2, 0),
|
||||
"left": (0, 1), "centre": (1, 1), "right": (2, 1),
|
||||
"bottom left": (0, 2), "bottom": (1, 2), "bottom right": (2, 2),
|
||||
}
|
||||
|
||||
# Portrait or landscape: "auto" follows the picture's own shape.
|
||||
ORIENTATIONS = ("auto", "portrait", "landscape")
|
||||
|
||||
|
||||
def picture_landscape(orientation, width, height):
|
||||
"""Should the page be landscape for this picture?"""
|
||||
if orientation == "landscape":
|
||||
return True
|
||||
|
||||
if orientation == "portrait":
|
||||
return False
|
||||
|
||||
return width > height
|
||||
|
||||
|
||||
def picture_rect(page, margin, picture, size=100, position="centre", fill=False):
|
||||
"""Where a picture goes on a page, as (x, y, width, height).
|
||||
|
||||
PAGE is (width, height) and MARGIN the blank edge, both in the same
|
||||
units; PICTURE is the picture's (width, height) in pixels. Never
|
||||
stretched: the picture keeps its own proportions.
|
||||
|
||||
Fit (the default) is the largest size that shows ALL of the picture
|
||||
inside the margins, times SIZE percent, placed at POSITION. Fill covers
|
||||
the whole printable area and so cuts off whatever overhangs — which a
|
||||
teacher asks for by name, never by default; SIZE and POSITION do not
|
||||
apply to it.
|
||||
"""
|
||||
page_width, page_height = page
|
||||
width, height = picture
|
||||
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError("the picture has no size")
|
||||
|
||||
area_x, area_y = margin, margin
|
||||
area_width = max(1.0, page_width - 2 * margin)
|
||||
area_height = max(1.0, page_height - 2 * margin)
|
||||
|
||||
if fill:
|
||||
scale = max(area_width / width, area_height / height)
|
||||
drawn_width, drawn_height = width * scale, height * scale
|
||||
return (area_x + (area_width - drawn_width) / 2,
|
||||
area_y + (area_height - drawn_height) / 2,
|
||||
drawn_width, drawn_height)
|
||||
|
||||
size = max(1, min(100, int(size)))
|
||||
scale = min(area_width / width, area_height / height) * size / 100
|
||||
drawn_width, drawn_height = width * scale, height * scale
|
||||
column, row = PICTURE_POSITIONS.get(position, (1, 1))
|
||||
|
||||
return (area_x + (area_width - drawn_width) * column / 2,
|
||||
area_y + (area_height - drawn_height) * row / 2,
|
||||
drawn_width, drawn_height)
|
||||
|
||||
+229
-6
@@ -167,6 +167,7 @@ from PyQt6.QtCore import ( # noqa: E402
|
||||
QEvent,
|
||||
QKeyCombination,
|
||||
QMarginsF,
|
||||
QRectF,
|
||||
Qt,
|
||||
QTimer,
|
||||
QUrl,
|
||||
@@ -177,8 +178,11 @@ from PyQt6.QtGui import ( # noqa: E402
|
||||
QGuiApplication,
|
||||
QIcon,
|
||||
QKeySequence,
|
||||
QImage,
|
||||
QPageLayout,
|
||||
QPageSize,
|
||||
QPainter,
|
||||
QPdfWriter,
|
||||
QPixmap,
|
||||
)
|
||||
from PyQt6.QtNetwork import QLocalServer, QLocalSocket # noqa: E402
|
||||
@@ -191,9 +195,12 @@ from PyQt6.QtWebEngineCore import ( # noqa: E402
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView # noqa: E402
|
||||
from PyQt6.QtWidgets import ( # noqa: E402
|
||||
QApplication,
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
@@ -221,6 +228,36 @@ PRINT_MARGIN_MM = 10
|
||||
# and says so. Measured: a long lesson page takes well under a second.
|
||||
PREPARE_TIMEOUT_MS = 20_000
|
||||
|
||||
# A picture's page is drawn at printer resolution: 300 dpi is what the
|
||||
# picture itself is capped at, so nothing it has is thrown away.
|
||||
PICTURE_DPI = 300
|
||||
|
||||
# Is this tab a picture on its own, and if so, the picture.
|
||||
#
|
||||
# A PNG or JPEG a lesson opens by itself is shown by the engine on a
|
||||
# near-black viewer page, scaled to the window. Printed as a page, that is
|
||||
# the black bars above and below the artwork that teachers saw. So the
|
||||
# picture is taken as it is — drawn onto white, which is what transparent
|
||||
# parts become on paper — and laid out on the paper by the preview.
|
||||
# Capped at 3300 pixels on its longest side, which is 300 dpi across a
|
||||
# sheet of Letter; nothing a printer can show is lost. An SVG is a
|
||||
# document, not a picture on a viewer page, and prints as a page.
|
||||
PICTURE = """(function(){
|
||||
var type = document.contentType || '';
|
||||
if (type.indexOf('image/') !== 0 || type === 'image/svg+xml') return null;
|
||||
var img = document.images[0];
|
||||
if (!img || !img.naturalWidth || document.images.length !== 1) return null;
|
||||
var scale = Math.min(1, 3300 / Math.max(img.naturalWidth, img.naturalHeight));
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(img.naturalWidth * scale));
|
||||
canvas.height = Math.max(1, Math.round(img.naturalHeight * scale));
|
||||
var g = canvas.getContext('2d');
|
||||
g.fillStyle = '#ffffff';
|
||||
g.fillRect(0, 0, canvas.width, canvas.height);
|
||||
g.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
try { return canvas.toDataURL('image/png'); } catch (e) { return null; }
|
||||
})()"""
|
||||
|
||||
# How much of a page title fits on a tab before it stops being a label
|
||||
# and starts being a paragraph.
|
||||
TAB_TITLE = 22
|
||||
@@ -936,8 +973,31 @@ class WonderInk(QMainWindow):
|
||||
self._preview = None
|
||||
|
||||
def _render(self, view, dialog, paper):
|
||||
"""Ask the engine for this page as a PDF on PAPER, for DIALOG."""
|
||||
"""Ask the engine for this page as a PDF on PAPER, for DIALOG.
|
||||
|
||||
Unless the tab is a picture on its own, in which case the picture
|
||||
itself is handed to the preview to be laid out on the paper.
|
||||
"""
|
||||
attempt = dialog.preparing()
|
||||
|
||||
def picture(answer):
|
||||
if not attempt.current():
|
||||
return
|
||||
|
||||
if isinstance(answer, str) and answer.startswith("data:image/png;base64,"):
|
||||
import base64
|
||||
|
||||
image = QImage.fromData(base64.b64decode(answer.split(",", 1)[1]))
|
||||
|
||||
if not image.isNull():
|
||||
dialog.show_picture(image)
|
||||
return
|
||||
|
||||
self._render_page(view, dialog, paper, attempt)
|
||||
|
||||
view.page().runJavaScript(PICTURE, picture)
|
||||
|
||||
def _render_page(self, view, dialog, paper, attempt):
|
||||
size = QPageSize(getattr(QPageSize.PageSizeId, paper))
|
||||
layout = QPageLayout(
|
||||
size, QPageLayout.Orientation.Portrait,
|
||||
@@ -1173,6 +1233,14 @@ QDialog#preview QPushButton#print {{
|
||||
QDialog#preview QPushButton#print:disabled {{ background: {c['line']}; color: {c['dim']}; }}
|
||||
QDialog#preview QPushButton#turn {{ min-width: 44px; padding: 4px 8px; font-size: 18px; }}
|
||||
QDialog#preview QLabel#status[error="true"] {{ color: {c['negative']}; }}
|
||||
QDialog#preview QLabel#heading {{ font-weight: 600; font-size: 16px; }}
|
||||
QDialog#preview QToolButton#place {{
|
||||
border: 1px solid {c['dim']};
|
||||
border-radius: 4px;
|
||||
background: {c['view']};
|
||||
}}
|
||||
QDialog#preview QToolButton#place:checked {{ background: {c['accent']}; border-color: {c['accent']}; }}
|
||||
QDialog#preview QToolButton#place:disabled {{ border-color: {c['line']}; background: {c['window']}; }}
|
||||
"""
|
||||
|
||||
|
||||
@@ -1191,6 +1259,45 @@ class _Attempt:
|
||||
return self.live
|
||||
|
||||
|
||||
class PositionGrid(QWidget):
|
||||
"""Where on the paper: nine squares, one chosen, like the page itself."""
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid = QGridLayout(self)
|
||||
grid.setContentsMargins(0, 0, 0, 0)
|
||||
grid.setSpacing(4)
|
||||
self._group = QButtonGroup(self)
|
||||
self._names = {}
|
||||
|
||||
for name, (column, row) in classroom_printing.PICTURE_POSITIONS.items():
|
||||
button = QToolButton(self)
|
||||
button.setCheckable(True)
|
||||
button.setObjectName("place")
|
||||
button.setToolTip(name.capitalize())
|
||||
button.setAccessibleName(name.capitalize())
|
||||
button.setFixedSize(30, 24)
|
||||
grid.addWidget(button, row, column)
|
||||
self._group.addButton(button)
|
||||
self._names[button] = name
|
||||
button.setChecked(name == "centre")
|
||||
|
||||
self._group.buttonToggled.connect(
|
||||
lambda button, checked: self.changed.emit() if checked else None
|
||||
)
|
||||
|
||||
def position(self):
|
||||
checked = self._group.checkedButton()
|
||||
return self._names.get(checked, "centre")
|
||||
|
||||
def choose(self, name):
|
||||
for button, known in self._names.items():
|
||||
if known == name:
|
||||
button.setChecked(True)
|
||||
|
||||
|
||||
class PageView(QWidget):
|
||||
"""One page of the document, whole, as it will come out of the printer."""
|
||||
|
||||
@@ -1241,8 +1348,9 @@ class PageView(QWidget):
|
||||
class PrintPreview(QDialog):
|
||||
"""The document as it will print, and the few choices a teacher has.
|
||||
|
||||
Printer, which pages, how many copies, and colour or black and white
|
||||
where the printer's driver can do both. No paper tray, no scaling, no
|
||||
Printer, which pages, how many copies, and color or black and white
|
||||
where the printer's driver can do both — and, for a picture, how large
|
||||
and where on the paper. No paper tray, no scaling, no
|
||||
"print to file" — there is no file chooser anywhere in this room, and
|
||||
a print dialog is the usual way one gets in.
|
||||
"""
|
||||
@@ -1269,6 +1377,7 @@ class PrintPreview(QDialog):
|
||||
self._attempt = None
|
||||
self._paper = None
|
||||
self._options = {}
|
||||
self._serial = iter(range(1, 1 << 30))
|
||||
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setSingleShot(True)
|
||||
@@ -1309,8 +1418,8 @@ class PrintPreview(QDialog):
|
||||
self._copies = QSpinBox(self)
|
||||
self._copies.setRange(1, classroom_printing.MAX_COPIES)
|
||||
self._colour = QComboBox(self)
|
||||
self._colour.addItems(["Colour", "Black and white"])
|
||||
self._colour_label = QLabel("Colour", self)
|
||||
self._colour.addItems(["Color", "Black and white"])
|
||||
self._colour_label = QLabel("Color", self)
|
||||
|
||||
form = QFormLayout()
|
||||
form.addRow("Printer", self._printer)
|
||||
@@ -1319,6 +1428,40 @@ class PrintPreview(QDialog):
|
||||
form.addRow("Copies", self._copies)
|
||||
form.addRow(self._colour_label, self._colour)
|
||||
|
||||
# -- a picture: how large, and where ---------------------------------
|
||||
self._picture = None
|
||||
self._compose_timer = QTimer(self)
|
||||
self._compose_timer.setSingleShot(True)
|
||||
self._compose_timer.timeout.connect(self._compose)
|
||||
|
||||
self._size = QComboBox(self)
|
||||
for percent in classroom_printing.PICTURE_SIZES:
|
||||
self._size.addItem(
|
||||
"As large as fits (100%)" if percent == 100 else f"{percent}%", percent
|
||||
)
|
||||
self._place = PositionGrid(self)
|
||||
self._turned = QComboBox(self)
|
||||
self._turned.addItem("Automatic", "auto")
|
||||
self._turned.addItem("Upright (portrait)", "portrait")
|
||||
self._turned.addItem("Sideways (landscape)", "landscape")
|
||||
self._fill = QCheckBox("Fill the whole page\n(the edges are cut off)", self)
|
||||
|
||||
for signal in (self._size.currentIndexChanged, self._turned.currentIndexChanged,
|
||||
self._place.changed, self._fill.toggled):
|
||||
signal.connect(self._picture_changed)
|
||||
|
||||
self._picture_box = QWidget(self)
|
||||
picture_form = QFormLayout(self._picture_box)
|
||||
picture_form.setContentsMargins(0, 12, 0, 0)
|
||||
heading = QLabel("Picture", self)
|
||||
heading.setObjectName("heading")
|
||||
picture_form.addRow(heading)
|
||||
picture_form.addRow("Size", self._size)
|
||||
picture_form.addRow("Position", self._place)
|
||||
picture_form.addRow("Paper", self._turned)
|
||||
picture_form.addRow("", self._fill)
|
||||
self._picture_box.setVisible(False)
|
||||
|
||||
self._status = QLabel(self)
|
||||
self._status.setObjectName("status")
|
||||
self._status.setWordWrap(True)
|
||||
@@ -1337,6 +1480,7 @@ class PrintPreview(QDialog):
|
||||
|
||||
right = QVBoxLayout()
|
||||
right.addLayout(form)
|
||||
right.addWidget(self._picture_box)
|
||||
right.addStretch(1)
|
||||
right.addWidget(self._status)
|
||||
right.addLayout(buttons)
|
||||
@@ -1410,6 +1554,74 @@ class PrintPreview(QDialog):
|
||||
path.write_bytes(data)
|
||||
self.show_document(path)
|
||||
|
||||
def show_picture(self, image):
|
||||
"""A picture on its own, to be laid out on the paper here."""
|
||||
self._timer.stop()
|
||||
|
||||
if self._attempt is not None:
|
||||
self._attempt.live = False
|
||||
|
||||
self._picture = image
|
||||
self._picture_box.setVisible(True)
|
||||
self._compose()
|
||||
|
||||
def _picture_changed(self, *_):
|
||||
filled = self._fill.isChecked()
|
||||
self._size.setEnabled(not filled)
|
||||
self._place.setEnabled(not filled)
|
||||
|
||||
if self._picture is not None:
|
||||
# A moment's pause, so turning through the sizes does not lay
|
||||
# the page out once for every step.
|
||||
self._compose_timer.start(120)
|
||||
|
||||
def _compose(self):
|
||||
"""Lay the picture out on one page, as the PDF that will print."""
|
||||
import tempfile
|
||||
|
||||
if self._picture is None:
|
||||
return
|
||||
|
||||
if self._scratch is None:
|
||||
self._scratch = tempfile.mkdtemp(
|
||||
prefix="wonderink-print-", dir=os.environ.get("XDG_RUNTIME_DIR")
|
||||
)
|
||||
|
||||
image = self._picture
|
||||
landscape = classroom_printing.picture_landscape(
|
||||
self._turned.currentData(), image.width(), image.height()
|
||||
)
|
||||
path = Path(self._scratch) / f"picture-{next(self._serial)}.pdf"
|
||||
|
||||
writer = QPdfWriter(str(path))
|
||||
writer.setTitle(self._title)
|
||||
writer.setResolution(PICTURE_DPI)
|
||||
writer.setPageLayout(QPageLayout(
|
||||
QPageSize(getattr(QPageSize.PageSizeId, self.paper())),
|
||||
QPageLayout.Orientation.Landscape if landscape else QPageLayout.Orientation.Portrait,
|
||||
QMarginsF(0, 0, 0, 0), QPageLayout.Unit.Millimeter,
|
||||
))
|
||||
|
||||
page = (writer.width(), writer.height())
|
||||
margin = PRINT_MARGIN_MM / 25.4 * PICTURE_DPI
|
||||
x, y, width, height = classroom_printing.picture_rect(
|
||||
page, margin, (image.width(), image.height()),
|
||||
self._size.currentData(), self._place.position(), self._fill.isChecked(),
|
||||
)
|
||||
|
||||
painter = QPainter(writer)
|
||||
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
# Filling overhangs the margins by design; nothing is drawn in them.
|
||||
painter.setClipRect(QRectF(margin, margin, page[0] - 2 * margin, page[1] - 2 * margin))
|
||||
painter.drawImage(QRectF(x, y, width, height), image)
|
||||
painter.end()
|
||||
|
||||
previous = self._pdf
|
||||
self.show_document(path)
|
||||
|
||||
if previous is not None and previous != path and previous.parent == path.parent:
|
||||
previous.unlink(missing_ok=True)
|
||||
|
||||
def show_document(self, pdf):
|
||||
self._pdf = Path(pdf)
|
||||
self._pages = classroom_printing.page_count(self._pdf)
|
||||
@@ -1482,7 +1694,12 @@ class PrintPreview(QDialog):
|
||||
if found["paper"] != self._paper:
|
||||
self._paper = found["paper"]
|
||||
|
||||
if not initial:
|
||||
if initial:
|
||||
return
|
||||
|
||||
if self._picture is not None:
|
||||
self._compose()
|
||||
else:
|
||||
self.paper_wanted.emit(self._paper)
|
||||
|
||||
def _turn(self, step):
|
||||
@@ -1683,6 +1900,12 @@ def build_profile(app, name="wonderink"):
|
||||
# loads file: URLs at all, a default is not a good enough answer.
|
||||
# Both are named here so that the answer is in the file rather than in
|
||||
# a Qt release note.
|
||||
# Page backgrounds are not printed, as in Chrome, where "Background
|
||||
# graphics" is off unless a teacher asks for it. Qt's default is the
|
||||
# opposite, and a dark page background printed as solid black.
|
||||
settings.setAttribute(
|
||||
QWebEngineSettings.WebAttribute.PrintElementBackgrounds, False
|
||||
)
|
||||
settings.setAttribute(
|
||||
QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False
|
||||
)
|
||||
|
||||
@@ -177,6 +177,59 @@ 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("5. Wonder Ink prints through this, and only this")
|
||||
ink = (root / "scripts/classroom-wonderink").read_text()
|
||||
@@ -193,6 +246,13 @@ 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)
|
||||
check("the module is packaged",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Make the test pictures, and measure where a printed picture really is.
|
||||
|
||||
wonderink-picture-measure.py make DIRECTORY
|
||||
wonderink-picture-measure.py measure PDF WIDTHxHEIGHT SIZE POSITION PAPER FILL
|
||||
wonderink-picture-measure.py darkness PDF
|
||||
|
||||
Run inside the lab by tools/lab/wonderink-print-check. The pictures are
|
||||
white with a black border drawn on their own edges and a pink mark near
|
||||
one corner, so the ink on a printed page says exactly where the picture
|
||||
landed — and any other dark ink on the page is something that was added,
|
||||
which is what the black bars were.
|
||||
|
||||
The page is rendered at 72 dpi, so one pixel is one PostScript point, and
|
||||
compared against classroom_printing.picture_rect: the same arithmetic
|
||||
Wonder Ink lays the page out with, asked independently of the preview.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
|
||||
|
||||
import classroom_printing # noqa: E402
|
||||
|
||||
MARGIN_POINTS = 10 / 25.4 * 72
|
||||
|
||||
|
||||
def make(directory):
|
||||
from PyQt6.QtCore import QRectF, Qt
|
||||
from PyQt6.QtGui import QColor, QFont, QImage, QPainter, QPen
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv[:1]) # noqa: F841 — kept, or Qt has no application
|
||||
|
||||
def art(width, height, name, label, transparent=False):
|
||||
image = QImage(width, height, QImage.Format.Format_ARGB32)
|
||||
image.fill(QColor(0, 0, 0, 0) if transparent else QColor("white"))
|
||||
painter = QPainter(image)
|
||||
painter.setPen(QPen(QColor("black"), 8))
|
||||
painter.drawRect(QRectF(4, 4, width - 8, height - 8))
|
||||
painter.setPen(QPen(QColor("black"), 6))
|
||||
painter.drawEllipse(QRectF(width * 0.2, height * 0.25, width * 0.6, height * 0.5))
|
||||
painter.setFont(QFont("DejaVu Sans", int(min(width, height) * 0.05)))
|
||||
painter.drawText(QRectF(0, height * 0.05, width, height * 0.15),
|
||||
Qt.AlignmentFlag.AlignCenter, label)
|
||||
painter.fillRect(QRectF(width * 0.05, height * 0.8, width * 0.1, height * 0.1),
|
||||
QColor("#e0006c"))
|
||||
painter.end()
|
||||
image.save(f"{directory}/{name}")
|
||||
|
||||
art(1600, 1000, "ark-wide.png", "Colour the Ark")
|
||||
art(900, 1400, "ark-tall.png", "Noah")
|
||||
art(1600, 1000, "ark-wide.jpg", "Colour the Ark")
|
||||
# Transparent around the drawing: on paper that has to be white, not
|
||||
# black, which is what a careless composite would make of it.
|
||||
art(1200, 1200, "ark-clear.png", "Clear", transparent=True)
|
||||
|
||||
|
||||
def page_pixels(pdf):
|
||||
ppm = subprocess.run(["pdftoppm", "-r", "72", "-f", "1", "-l", "1", "-singlefile", pdf],
|
||||
capture_output=True, check=True).stdout
|
||||
# P6 header: magic, width, height, maxval, then raw RGB.
|
||||
fields, index = [], 0
|
||||
while len(fields) < 4:
|
||||
while ppm[index:index + 1].isspace():
|
||||
index += 1
|
||||
start = index
|
||||
while not ppm[index:index + 1].isspace():
|
||||
index += 1
|
||||
fields.append(ppm[start:index])
|
||||
width, height = int(fields[1]), int(fields[2])
|
||||
return width, height, ppm[index + 1:]
|
||||
|
||||
|
||||
def measure(pdf, picture, size, position, paper, fill):
|
||||
width, height, rgb = page_pixels(pdf)
|
||||
picture_w, picture_h = (int(n) for n in picture.split("x"))
|
||||
landscape = classroom_printing.picture_landscape(paper, picture_w, picture_h)
|
||||
x, y, w, h = classroom_printing.picture_rect(
|
||||
(width, height), MARGIN_POINTS, (picture_w, picture_h),
|
||||
int(size), position.replace("-", " "), fill == "fill")
|
||||
|
||||
# The expected ink, clipped to the printable area as the page is.
|
||||
left = max(x, MARGIN_POINTS)
|
||||
top = max(y, MARGIN_POINTS)
|
||||
right = min(x + w, width - MARGIN_POINTS)
|
||||
bottom = min(y + h, height - MARGIN_POINTS)
|
||||
|
||||
xs, ys, outside, pink = [], [], 0, 0
|
||||
|
||||
for row in range(height):
|
||||
for column in range(width):
|
||||
i = (row * width + column) * 3
|
||||
r, g, b = rgb[i], rgb[i + 1], rgb[i + 2]
|
||||
dark = r + g + b < 3 * 110
|
||||
magenta = r > 150 and g < 90 and b > 60
|
||||
|
||||
if dark or magenta:
|
||||
xs.append(column)
|
||||
ys.append(row)
|
||||
|
||||
if not (left - 2 <= column <= right + 2 and top - 2 <= row <= bottom + 2):
|
||||
outside += 1
|
||||
|
||||
if magenta:
|
||||
pink += 1
|
||||
|
||||
found = (min(xs), min(ys), max(xs) + 1, max(ys) + 1) if xs else None
|
||||
expected = (round(left), round(top), round(right), round(bottom))
|
||||
off = max(abs(a - b) for a, b in zip(found, expected)) if found else None
|
||||
|
||||
print(json.dumps({
|
||||
"page": [width, height],
|
||||
"page_landscape": width > height,
|
||||
"wanted_landscape": landscape,
|
||||
"found": found,
|
||||
"expected": expected,
|
||||
"off_by_points": off,
|
||||
"ink_outside_picture": outside,
|
||||
"pink_pixels": pink,
|
||||
}))
|
||||
|
||||
|
||||
def darkness(pdf):
|
||||
"""How much of the first page is dark, 0 to 1."""
|
||||
width, height, rgb = page_pixels(pdf)
|
||||
sums = [rgb[i] + rgb[i + 1] + rgb[i + 2] for i in range(0, len(rgb) - 2, 3)]
|
||||
dark = sum(1 for total in sums if total < 3 * 110)
|
||||
# Anything printed at all. Blink darkens light text when a page's
|
||||
# background is not printed, so white-on-black lesson text comes out
|
||||
# grey on white rather than vanishing; this is what says it did.
|
||||
ink = sum(1 for total in sums if total < 3 * 200)
|
||||
print(json.dumps({"dark_fraction": round(dark / (width * height), 4),
|
||||
"ink_fraction": round(ink / (width * height), 4)}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "make":
|
||||
make(sys.argv[2])
|
||||
elif sys.argv[1] == "darkness":
|
||||
darkness(sys.argv[2])
|
||||
else:
|
||||
measure(*sys.argv[2:])
|
||||
@@ -25,6 +25,8 @@ lab() { docker exec -i "$NAME" bash -c "$1"; }
|
||||
RIG=/opt/wonderink-check/wonderink-print-rig.py
|
||||
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
|
||||
lab "chmod -R a+rX /opt/wonderink-check && test -r $RIG" || {
|
||||
echo "could not put the rig in the lab" >&2
|
||||
exit 1
|
||||
@@ -63,8 +65,18 @@ install -d -o teacher -m 700 /tmp/teacher-rt
|
||||
# An inline PDF, served the way a site shows one rather than sends it. Its
|
||||
# own port and directory: a lab often has another test server on 8088.
|
||||
mkdir -p /opt/wonderink-check/www
|
||||
# CUPS keeps what it was given, so a check can compare the submitted
|
||||
# file with the one the preview showed, byte for byte.
|
||||
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" || {
|
||||
echo "could not make the test pictures" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Started detached. A background process started inside a docker exec
|
||||
# dies with it in the Plasma lab, whose PID 1 is systemd.
|
||||
lab 'pgrep -f "[h]ttp.server 8089" >/dev/null' || \
|
||||
@@ -97,12 +109,28 @@ run() {
|
||||
}
|
||||
|
||||
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"; }
|
||||
exited_cleanly() { said 'exit=0' && ! said 'Traceback' && ! said 'Segmentation' && ! said 'Aborted'; }
|
||||
|
||||
# Where a picture goes, measured on the page CUPS was given:
|
||||
# picture:FILE:SIZE:POSITION:PAPER:FILL[:gray]
|
||||
PICTURES=(
|
||||
picture:ark-wide.png:100:centre:auto:fit
|
||||
picture:ark-wide.png:100:centre:portrait:fit
|
||||
picture:ark-wide.png:50:top-left:auto:fit
|
||||
picture:ark-tall.png:100:centre:auto:fit
|
||||
picture:ark-tall.png:33:bottom-right:portrait:fit
|
||||
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
|
||||
)
|
||||
|
||||
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)
|
||||
options bad-range broken cancel page-closes-itself close-window page-prints-again repeat
|
||||
dark-page "${PICTURES[@]}")
|
||||
|
||||
# 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'; }
|
||||
@@ -162,6 +190,45 @@ for scenario in "${scenarios[@]}"; do
|
||||
judge "one job" '[[ $(count) -eq 1 ]]' ;;
|
||||
repeat)
|
||||
judge "five prints in a row, five jobs" '[[ $(count) -eq 5 ]]' ;;
|
||||
dark-page)
|
||||
judge "one job" '[[ $(count) -eq 1 ]]'
|
||||
spool=$(spool_file "$jobs")
|
||||
answer=$(lab "python3 $MEASURE darkness $spool")
|
||||
dark=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["dark_fraction"])' "$answer")
|
||||
ink=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["ink_fraction"])' "$answer")
|
||||
judge "a black page background does not print (dark: $dark of the page)" \
|
||||
"python3 -c 'import sys; sys.exit(0 if $dark < 0.05 else 1)'"
|
||||
judge "and its white text still does, darkened ($ink of the page)" \
|
||||
"python3 -c 'import sys; sys.exit(0 if $ink > 0.005 else 1)'" ;;
|
||||
picture:*)
|
||||
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* ]]'
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
[[ "$failed" -eq 0 ]] || { echo "$out" | tail -15 | sed 's/^/ /'; }
|
||||
|
||||
@@ -23,9 +23,8 @@ from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings
|
||||
notes = []
|
||||
wi.notify = lambda s, b: (notes.append(s), print("NOTIFY", s, "|", b, flush=True))
|
||||
app = QApplication(sys.argv)
|
||||
profile = QWebEngineProfile("rig", app)
|
||||
for a in ("PdfViewerEnabled", "PluginsEnabled"):
|
||||
profile.settings().setAttribute(getattr(QWebEngineSettings.WebAttribute, a), True)
|
||||
# Wonder Ink's own profile, settings and all — not one of the rig's.
|
||||
profile = wi.build_profile(app, "rig")
|
||||
window = wi.WonderInk(profile)
|
||||
profile.downloadRequested.connect(window.on_download)
|
||||
window.resize(1280, 800); window.show()
|
||||
@@ -60,7 +59,7 @@ 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"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._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 finish(code=0):
|
||||
stamp(f"RESULT notes={notes} preview_open={preview() is not None} tabs={window._tabs.count()}")
|
||||
@@ -101,7 +100,49 @@ actions = {
|
||||
QTimer.singleShot(800, lambda: (stamp(f"second request while open: still one preview={preview() is not None}"), press_print()))), "preview"),
|
||||
}
|
||||
|
||||
if scenario == "repeat":
|
||||
if 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.
|
||||
_, name, size, position, paper, fill, *rest = scenario.split(":")
|
||||
view = window.open_tab(); view.load(QUrl(f"http://127.0.0.1:8089/{name}"))
|
||||
def ready_picture():
|
||||
d = preview()
|
||||
return d is not None and d._picture is not None and d._pdf is not None
|
||||
def choose():
|
||||
d = preview()
|
||||
d._size.setCurrentIndex(d._size.findData(int(size)))
|
||||
d._place.choose(position.replace("-", " "))
|
||||
d._turned.setCurrentIndex(d._turned.findData(paper))
|
||||
d._fill.setChecked(fill == "fill")
|
||||
if rest and rest[0] == "gray":
|
||||
d._colour.setCurrentIndex(1)
|
||||
QTimer.singleShot(900, keep_and_print)
|
||||
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}")
|
||||
press_print()
|
||||
QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
||||
when(ready_picture, choose, "picture preview")))
|
||||
elif scenario == "dark-page":
|
||||
# A lesson page with a black background. Chrome does not print page
|
||||
# backgrounds unless asked, and neither may Wonder Ink.
|
||||
view = window.open_tab()
|
||||
view.setHtml("<html><head><title>Night sky</title></head><body style='background:#000;color:#fff'>"
|
||||
+ "<h1>Stars</h1>" + "<p>God made the stars. </p>" * 40 + "</body></html>",
|
||||
QUrl("https://app.wonderink.org/night"))
|
||||
def keep_dark():
|
||||
import shutil
|
||||
out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True)
|
||||
shutil.copy(preview()._pdf, out / "preview.pdf")
|
||||
press_print()
|
||||
view.loadFinished.connect(lambda ok: (QTimer.singleShot(0, lambda: window.print_view(view)),
|
||||
when(ready, keep_dark, "dark preview")))
|
||||
elif scenario == "repeat":
|
||||
view = window.open_tab(); lesson(view); n = [0]
|
||||
def once():
|
||||
if n[0] == 5: return finish()
|
||||
|
||||
Reference in New Issue
Block a user