Buckets:

glennmatlin's picture
download
raw
4.24 kB
#!/usr/bin/env python3
"""Audit rendered manuscript PDFs for figure-quality problems.
Checks two common submission failures:
1. Type 3 fonts embedded by stale matplotlib/plotly figure PDFs.
2. Raster images rendered below a target effective PPI.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class FontFinding:
page: int
line: str
@dataclass(frozen=True)
class ImageFinding:
page: int
image: str
x_ppi: int
y_ppi: int
width: int
height: int
def _run(command: list[str]) -> str:
completed = subprocess.run(
command,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return completed.stdout
def _page_count(pdf_path: Path) -> int:
out = _run(["pdfinfo", str(pdf_path)])
for line in out.splitlines():
if line.startswith("Pages:"):
return int(line.split(":", 1)[1].strip())
raise RuntimeError(f"Could not read page count from {pdf_path}")
def _parse_pages(raw: str | None, max_page: int) -> list[int]:
if not raw:
return list(range(1, max_page + 1))
pages: set[int] = set()
for chunk in raw.split(","):
chunk = chunk.strip()
if not chunk:
continue
if "-" in chunk:
start_s, end_s = chunk.split("-", 1)
start, end = int(start_s), int(end_s)
pages.update(range(start, end + 1))
else:
pages.add(int(chunk))
return [p for p in sorted(pages) if 1 <= p <= max_page]
def type3_fonts(pdf_path: Path, pages: list[int]) -> list[FontFinding]:
findings: list[FontFinding] = []
for page in pages:
out = _run(["pdffonts", "-f", str(page), "-l", str(page), str(pdf_path)])
for line in out.splitlines()[2:]:
if "Type 3" in line:
findings.append(FontFinding(page=page, line=line.strip()))
return findings
def low_ppi_images(pdf_path: Path, min_ppi: int) -> list[ImageFinding]:
out = _run(["pdfimages", "-list", str(pdf_path)])
findings: list[ImageFinding] = []
for line in out.splitlines():
if not re.match(r"\s*\d+\s+\d+\s+", line):
continue
parts = line.split()
if len(parts) < 14:
continue
page = int(parts[0])
image = parts[1]
width = int(parts[3])
height = int(parts[4])
x_ppi = int(parts[12])
y_ppi = int(parts[13])
if x_ppi < min_ppi or y_ppi < min_ppi:
findings.append(
ImageFinding(
page=page,
image=image,
x_ppi=x_ppi,
y_ppi=y_ppi,
width=width,
height=height,
)
)
return findings
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("pdf", type=Path)
parser.add_argument("--pages", help="Comma/range list, e.g. 2,7-10,47-49")
parser.add_argument("--min-ppi", type=int, default=300)
parser.add_argument("--warn-only", action="store_true")
args = parser.parse_args()
page_count = _page_count(args.pdf)
pages = _parse_pages(args.pages, page_count)
font_findings = type3_fonts(args.pdf, pages)
image_findings = low_ppi_images(args.pdf, args.min_ppi)
if font_findings:
print("Type 3 fonts:")
for finding in font_findings:
print(f" page {finding.page}: {finding.line}")
else:
print("Type 3 fonts: none on checked pages")
if image_findings:
print(f"Raster images below {args.min_ppi} PPI:")
for finding in image_findings:
print(
f" page {finding.page} image {finding.image}: "
f"{finding.width}x{finding.height} at "
f"{finding.x_ppi}x{finding.y_ppi} PPI"
)
else:
print(f"Raster images below {args.min_ppi} PPI: none")
has_findings = bool(font_findings or image_findings)
return 0 if args.warn_only or not has_findings else 1
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
4.24 kB
·
Xet hash:
503cb412510d9ce3d671e9e4d2d409b6c347a51386789cc8f709adf7fbc85bbc

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.