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" ) )