"""Regenerate the image-bearing fixtures for the asset path (0.10.0).
Five documents, one per reader, each carrying a KNOWN number of images so a
gate can state "carried N of M" with M read out of the source rather than out
of this package. Written in a second file rather than appended to
`make_fixtures.py` for one reason: every fixture that file emits is byte-pinned
by a test, and the object numbering of the PDF builders is part of those bytes.
Adding an XObject to a shared builder would regenerate files whose whole value
is that they have not moved.
The same policy holds here as there: no generator library. The PNG is written
out with `zlib` from the stdlib, the JPEG as a header sequence (the readers
copy JPEG bytes through and read nothing but the frame marker, so a decodable
photograph would test nothing extra and could not be hand-audited), and the
containers are assembled part by part.
Run from the repository root: python3 tests/fixtures/make_image_fixtures.py
"""
from __future__ import annotations
import io
import struct
import zipfile
import zlib
from pathlib import Path
HERE = Path(__file__).parent
IMAGES = HERE / "image-inbox"
_XML = ''
_ZIP_DATE = (2020, 1, 1, 0, 0, 0)
def png(width: int, height: int, value: int = 0x40) -> bytes:
"""A real, single-channel PNG of a flat grey."""
def chunk(kind: bytes, payload: bytes) -> bytes:
return (
len(payload).to_bytes(4, "big")
+ kind
+ payload
+ zlib.crc32(kind + payload).to_bytes(4, "big")
)
ihdr = struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0)
raw = b"".join(b"\x00" + bytes([value] * width) for _ in range(height))
return (
b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", ihdr)
+ chunk(b"IDAT", zlib.compress(raw, 9))
+ chunk(b"IEND", b"")
)
def jpeg_header(width: int, height: int) -> bytes:
"""A JPEG's marker sequence: SOI, JFIF, a baseline frame header, EOI.
Not a decodable photograph, and that is the point of it. The `pdf` reader
passes `DCTDecode` bytes through untouched and reads only the frame header
for the size, which is precisely what R761's own table images need -- 29 of
its 50 image objects are `DCTDecode`. A fixture that also carried entropy
data would exercise no additional line of this package and could not be read
byte by byte by a person.
"""
frame = bytes([8, height >> 8, height & 0xFF, width >> 8, width & 0xFF, 1, 1, 0x11, 0])
return (
b"\xff\xd8"
b"\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
+ b"\xff\xc0"
+ (len(frame) + 2).to_bytes(2, "big")
+ frame
+ b"\xff\xd9"
)
# --- pdf ---------------------------------------------------------------------
#
# TWO images on one page, and they are deliberately of the two kinds the
# measurement on R761 found: 29 `DCTDecode` objects, which arrive as a finished
# JPEG file, and 21 `FlateDecode` ones, which arrive as raw samples with the
# colour model in the dictionary beside them and have to be encoded to be
# carried at all. A fixture with only one kind would leave half the reader
# unexercised, and it is the encoded half that can be silently wrong.
PDF_GRAY_WIDTH, PDF_GRAY_HEIGHT = 4, 3
PDF_GRAY_SAMPLES = bytes([0, 60, 120, 180, 20, 80, 140, 200, 40, 100, 160, 255])
PDF_JPEG_WIDTH, PDF_JPEG_HEIGHT = 360, 269
PDF_CONTENT = (
b"BT /F1 12 Tf 20 170 Td (Toleranseklasse er gitt i tabell 84-2) Tj ET\n"
b"q 80 0 0 60 20 90 cm /ImFlate Do Q\n"
b"q 80 0 0 60 20 20 cm /ImJpeg Do Q\n"
)
def build_image_pdf() -> bytes:
"""A one-page PDF with a Flate image and a DCT image in its resources."""
flate = zlib.compress(PDF_GRAY_SAMPLES, 9)
jpeg = jpeg_header(PDF_JPEG_WIDTH, PDF_JPEG_HEIGHT)
objects = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R "
b"/Resources << /Font << /F1 5 0 R >> "
b"/XObject << /ImFlate 6 0 R /ImJpeg 7 0 R >> >> >>",
b"<< /Length "
+ str(len(PDF_CONTENT)).encode()
+ b" >>\nstream\n"
+ PDF_CONTENT
+ b"endstream",
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
b"<< /Type /XObject /Subtype /Image /Width "
+ str(PDF_GRAY_WIDTH).encode()
+ b" /Height "
+ str(PDF_GRAY_HEIGHT).encode()
+ b" /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length "
+ str(len(flate)).encode()
+ b" >>\nstream\n"
+ flate
+ b"\nendstream",
b"<< /Type /XObject /Subtype /Image /Width "
+ str(PDF_JPEG_WIDTH).encode()
+ b" /Height "
+ str(PDF_JPEG_HEIGHT).encode()
+ b" /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /DCTDecode /Length "
+ str(len(jpeg)).encode()
+ b" >>\nstream\n"
+ jpeg
+ b"\nendstream",
]
out = bytearray(b"%PDF-1.4\n")
offsets = []
for number, body in enumerate(objects, start=1):
offsets.append(len(out))
out += str(number).encode() + b" 0 obj\n" + body + b"\nendobj\n"
xref_at = len(out)
size = str(len(objects) + 1).encode()
out += b"xref\n0 " + size + b"\n0000000000 65535 f \n"
for offset in offsets:
out += ("%010d 00000 n \n" % offset).encode()
out += b"trailer\n<< /Size " + size + b" /Root 1 0 R >>\n"
out += b"startxref\n" + str(xref_at).encode() + b"\n%%EOF\n"
return bytes(out)
# --- html --------------------------------------------------------------------
#
# THREE `` and only two of them can be carried. The remote one is the
# boundary written as a fixture: this package never opens a socket during
# extraction (the network gate is an explicit per-run opt-in and extraction is
# not on that path), so a remote source becomes a pointer WITHOUT a file, and
# the gate counts it as found-and-not-carried rather than as absent.
HTML_DOCUMENT = """
Toleranseklasse for de enkelte konstruksjonsdeler er gitt i tabell 84-2.
Figuren under viser prinsippet.
Og en som ligger et annet sted:
"""
# --- niso-sts ----------------------------------------------------------------
#
# The shape the R761 delivery actually has, measured 2026-09-16: 50 `` the extractor already emits on its own line.
STS_DOCUMENT = """
Toleranseklasse er gitt i tabell 84-2. Figur 84-1 viser prinsippet.