fix: the figures carried a chunk WebKit refuses, which is why a page never loaded
The user's report was precise and I misread it twice: opening trigger.page as a *file* rendered the page with no image, while opening it through the app's `help:` URI spun forever. Both are one cause. The three figures carried a `bKGD` chunk -- and the PNG specification does not permit `bKGD` on an image with an alpha channel, which all three are (colour type 6). ImageMagick both writes and reads it, so my "decodes fully" check proved nothing: I tested with a decoder more permissive than the one that has to load the file. WebKit rejects it, so the image never appears; over `help:` the fetch never completes either, so the page's load event never fires and Yelp's spinner never stops. It came from `import`, which records the root window's background. Every figure now carries only the chunks a decoder is guaranteed to accept (IHDR/IDAT/IEND), and `tools/make-help-shots.sh` normalises its output at the end so a re-shoot cannot reintroduce it -- with the reason written where the next reader will be, since "ImageMagick reads it" is exactly the trap. `tools/check_help.sh` gains the rule, because the four yelp-check subcommands and the structural rules are all blind to a figure no decoder will accept: a `bKGD` on colour type 4 or 6 is a finding, checked directly rather than through a permissive tool. It is a whitelist because a decoder is one. Verified: the rule fires on the committed figures (colour type 6, bKGD present) and passes on the fixed ones; the figures still decode and are still 1280x800 / 938x420; check_help.sh is green on the source and installed sets; make -C build test-help passes.
This commit is contained in:
1 parent
fe499bfe41
commit
6138e6bdce
5 files changed
+60
-9
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 146 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 123 KiB |
+48
-9
@@ -3,9 +3,9 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Martino Ferrari
|
# SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
#
|
#
|
||||||
# The manual's check, the way tools/check_ui.py is the .ui files' check: it runs the four
|
# The manual's check, the way tools/check_ui.py is the .ui files' check: it runs the four
|
||||||
# yelp-check subcommands that need no network, checks the structural rules below, then renders
|
# yelp-check subcommands that need no network, checks the structural rules below, checks the
|
||||||
# the whole set to HTML with yelp-build so a page that only fails at the transform step is
|
# figures, then renders the whole set to HTML with yelp-build so a page that only fails at the
|
||||||
# caught too.
|
# transform step is caught too.
|
||||||
#
|
#
|
||||||
# `yelp-check validate` is deliberately absent, and it is worth being exact about why: it
|
# `yelp-check validate` is deliberately absent, and it is worth being exact about why: it
|
||||||
# builds the RelaxNG schema locally with `xsltproc` and `mal-rng.xsl`, but the schema it
|
# builds the RelaxNG schema locally with `xsltproc` and `mal-rng.xsl`, but the schema it
|
||||||
@@ -15,11 +15,10 @@
|
|||||||
# What that leaves unverified is *structure*, and that is not a small gap: three pages
|
# What that leaves unverified is *structure*, and that is not a small gap: three pages
|
||||||
# carrying a `<figure>` whose `<title>` was a sentence and which had no `<desc>` opened in
|
# carrying a `<figure>` whose `<title>` was a sentence and which had no `<desc>` opened in
|
||||||
# nothing, while links, media, orphans, ids and the HTML render all passed. So the rules this
|
# nothing, while links, media, orphans, ids and the HTML render all passed. So the rules this
|
||||||
# project has actually needed are checked here, with xmllint's XPath and no schema at all.
|
# project has actually needed are checked here. Add a rule when a page *teaches* one -- and do
|
||||||
# Add a rule when a page *teaches* one -- and do not add one you have not seen broken: a rule
|
# not add one you have not seen broken: a rule that fails a legal page is worse than no rule.
|
||||||
# that fails a legal page is worse than no rule.
|
|
||||||
#
|
#
|
||||||
# Every expression uses local-name() on purpose. A Mallard page declares
|
# Every XPath expression uses local-name() on purpose. A Mallard page declares
|
||||||
# `xmlns="http://projectmallard.org/1.0/"`, and XPath 1.0 has no default namespace, so
|
# `xmlns="http://projectmallard.org/1.0/"`, and XPath 1.0 has no default namespace, so
|
||||||
# `//figure` matches *nothing* and every rule below silently passes. Written naively, this
|
# `//figure` matches *nothing* and every rule below silently passes. Written naively, this
|
||||||
# check reported "ok" for the very defect it was written for.
|
# check reported "ok" for the very defect it was written for.
|
||||||
@@ -58,11 +57,51 @@ no text sits directly under page|count(/*[local-name()='page']/text()[normalize-
|
|||||||
a media names its src|count(//*[local-name()='media'][not(@src)])
|
a media names its src|count(//*[local-name()='media'][not(@src)])
|
||||||
RULES
|
RULES
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# The figures, and this one is a *decoder's* rule rather than ours: a `bKGD` chunk is forbidden
|
||||||
|
# by the PNG specification on an image with an alpha channel, and `import` writes one because it
|
||||||
|
# records the root window's background. ImageMagick both writes and reads it, so a round trip
|
||||||
|
# through it proves nothing -- which is how three figures passed every check while WebKit
|
||||||
|
# refused them, showing a figure that never loaded and, over the `help:` URI, a page that spun
|
||||||
|
# forever. Only the chunks PNG allows for the colour type are accepted; a whitelist, because a
|
||||||
|
# decoder is one.
|
||||||
|
python3 - "$dir" <<'PY' || failed=1
|
||||||
|
import glob, os, struct, sys
|
||||||
|
seen = 0
|
||||||
|
for path in sorted(glob.glob(os.path.join(sys.argv[1], 'figures', '*.png'))):
|
||||||
|
seen += 1
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
if f.read(8) != b'\x89PNG\r\n\x1a\n':
|
||||||
|
print(f" {os.path.basename(path)}: not a PNG", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
chunks = []
|
||||||
|
while True:
|
||||||
|
head = f.read(8)
|
||||||
|
if len(head) < 8:
|
||||||
|
break
|
||||||
|
length, kind = struct.unpack('>I4s', head)
|
||||||
|
f.read(length)
|
||||||
|
f.read(4)
|
||||||
|
chunks.append(kind)
|
||||||
|
# Colour type 4 (grey+alpha) and 6 (RGB+alpha) have an alpha channel; bKGD is not permitted
|
||||||
|
# on either. Anything else is left alone: this checks the rule a page was broken by.
|
||||||
|
colour_type = None
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
f.read(16)
|
||||||
|
colour_type = f.read(1)[0]
|
||||||
|
if colour_type in (4, 6) and b'bKGD' in chunks:
|
||||||
|
print(f" {os.path.basename(path)}: bKGD on an image with alpha (colour type {colour_type})"
|
||||||
|
" -- PNG forbids it and a strict decoder refuses the file", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if seen == 0:
|
||||||
|
print(" no figures to check", file=sys.stderr)
|
||||||
|
PY
|
||||||
|
|
||||||
if [ "$failed" != 0 ]; then
|
if [ "$failed" != 0 ]; then
|
||||||
echo "check_help: structural findings above" >&2
|
echo "check_help: findings above" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf 'structure %s: ok\n' "$dir"
|
printf 'structure %s: ok; figures: ok\n' "$dir"
|
||||||
|
|
||||||
mkdir -p "$tmp/html"
|
mkdir -p "$tmp/html"
|
||||||
yelp-build html -o "$tmp/html" "$dir"
|
yelp-build html -o "$tmp/html" "$dir"
|
||||||
|
|||||||
@@ -221,4 +221,16 @@ APP_ARGS="--plots 1" start_app "$tmp/math-editor.log" \
|
|||||||
sleep 10
|
sleep 10
|
||||||
grab math-editor "$tmp/math-editor.log" "$boxes_math" pick_other_window
|
grab math-editor "$tmp/math-editor.log" "$boxes_math" pick_other_window
|
||||||
|
|
||||||
|
# The last step, and it is not cosmetic: `import` records the root window's background, so the
|
||||||
|
# PNG leaves ImageMagick with a `bKGD` chunk -- forbidden by the PNG specification on an image
|
||||||
|
# with an alpha channel (colour types 4 and 6), which these are. ImageMagick writes it and reads
|
||||||
|
# it back happily, so a round-trip through `magick` proves nothing; a stricter decoder refuses
|
||||||
|
# the file. In Yelp that showed as a figure that never loaded, and over the `help:` URI as a page
|
||||||
|
# that spun forever, because the load event never fired. Normalise every figure to the chunks a
|
||||||
|
# decoder is guaranteed to accept, whatever the grab produced.
|
||||||
|
for f in "$out"/*.png; do
|
||||||
|
magick "$f" -strip -define png:exclude-chunk=bKGD,cHRM,tIME,tEXt,tXMP,exif \
|
||||||
|
-define png:compression-level=9 "$f"
|
||||||
|
done
|
||||||
|
|
||||||
echo "make-help-shots: done"
|
echo "make-help-shots: done"
|
||||||
Reference in new issue
Block a user