Files
2026-05-30 16:54:49 +00:00

93 lines
3.7 KiB
Python

import zlib
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from PIL import Image
from .settings import BuildConfig, PAGES_DIR, PDF_COMPRESSION_LEVEL, PRINT_DIR, worker_count
def write_pdf(config: BuildConfig) -> Path | None:
pages = sorted(PAGES_DIR.glob("page_*.png"))
if not pages:
return None
out_name = config.output_pdf_name.strip() or "blockminers_printable.pdf"
if not out_name.lower().endswith(".pdf"):
out_name += ".pdf"
PRINT_DIR.mkdir(parents=True, exist_ok=True)
out = PRINT_DIR / Path(out_name).name
write_lossless_image_pdf(pages, out, config.dpi)
return out
def write_lossless_image_pdf(page_paths: list[Path], out_path: Path, dpi: int) -> None:
max_obj = 2 + len(page_paths) * 3
offsets = [0] * (max_obj + 1)
page_objects: list[int] = []
def pdf_num(value: float) -> str:
return f"{value:.4f}".rstrip("0").rstrip(".")
def write_obj(f, obj_id: int, body: bytes) -> None:
offsets[obj_id] = f.tell()
f.write(f"{obj_id} 0 obj\n".encode("ascii"))
f.write(body)
f.write(b"\nendobj\n")
def write_stream_obj(f, obj_id: int, dictionary: str, stream: bytes) -> None:
header = f"<< {dictionary} /Length {len(stream)} >>\nstream\n".encode("ascii")
write_obj(f, obj_id, header + stream + b"\nendstream")
def compress_page(path: Path) -> tuple[int, int, float, float, bytes]:
with Image.open(path).convert("RGB") as img:
width_px, height_px = img.size
width_pt = width_px / dpi * 72.0
height_pt = height_px / dpi * 72.0
compressed = zlib.compress(img.tobytes(), level=PDF_COMPRESSION_LEVEL)
return width_px, height_px, width_pt, height_pt, compressed
with ThreadPoolExecutor(max_workers=worker_count(len(page_paths))) as executor:
compressed_pages = list(executor.map(compress_page, page_paths))
with out_path.open("wb") as f:
f.write(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
for index, (width_px, height_px, width_pt, height_pt, compressed) in enumerate(compressed_pages):
page_obj = 3 + index * 3
image_obj = page_obj + 1
content_obj = page_obj + 2
page_objects.append(page_obj)
image_dict = (
f"/Type /XObject /Subtype /Image /Width {width_px} /Height {height_px} "
"/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode"
)
write_stream_obj(f, image_obj, image_dict, compressed)
content = (
f"q\n{pdf_num(width_pt)} 0 0 {pdf_num(height_pt)} 0 0 cm\n/Im0 Do\nQ\n"
).encode("ascii")
write_stream_obj(f, content_obj, "", content)
page_body = (
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {pdf_num(width_pt)} {pdf_num(height_pt)}] "
f"/Resources << /XObject << /Im0 {image_obj} 0 R >> >> "
f"/Contents {content_obj} 0 R >>"
).encode("ascii")
write_obj(f, page_obj, page_body)
kids = " ".join(f"{obj_id} 0 R" for obj_id in page_objects)
write_obj(f, 2, f"<< /Type /Pages /Kids [{kids}] /Count {len(page_objects)} >>".encode("ascii"))
write_obj(f, 1, b"<< /Type /Catalog /Pages 2 0 R >>")
xref_offset = f.tell()
f.write(f"xref\n0 {max_obj + 1}\n".encode("ascii"))
f.write(b"0000000000 65535 f \n")
for obj_id in range(1, max_obj + 1):
f.write(f"{offsets[obj_id]:010d} 00000 n \n".encode("ascii"))
f.write(
f"trailer\n<< /Size {max_obj + 1} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n".encode(
"ascii"
)
)