Initial Blockminers printer
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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"
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user