add USB Type-C Spec R2.5
This commit is contained in:
parent
a1c36ccc90
commit
7956e6ca25
281 changed files with 13432 additions and 7 deletions
|
|
@ -33,10 +33,19 @@ from dataclasses import dataclass
|
|||
from pathlib import Path
|
||||
|
||||
# --- page geometry, in PDF points (origin top-left, y increasing downward) ---
|
||||
# The first five are template-specific; each has a command-line override of the
|
||||
# same name (--header-bottom, --footer-top, ...) so a different vendor template
|
||||
# can be tuned without editing the defaults.
|
||||
HEADER_BOTTOM = 62.0 # running head + its horizontal rule (ends ~60pt)
|
||||
FOOTER_TOP = 740.0 # doc id / revision / page number sit below this
|
||||
BODY_MIN_SIZE = 9.5 # body text and section headings are >= 10pt
|
||||
BODY_MAX_X0 = 60.0 # ...and start at the left margin (~47.5pt)
|
||||
BODY_MIN_WIDTH = float("inf") # ...or, for indented prose (bullets, numbered
|
||||
# lists), are at least this wide. Off by default; set
|
||||
# it between the widest *in-figure* label and the
|
||||
# narrowest full line of prose. A right-edge test is
|
||||
# not enough: a right-aligned figure label also ends
|
||||
# at the right margin.
|
||||
CAPTION_MAX_SIZE = 13.0 # captions are bold, 9-12pt depending on template
|
||||
CAPTION_LINE_GAP = 5.0 # max vertical gap between wrapped caption lines
|
||||
ROW_TOLERANCE = 6.0 # captions starting within this many points of each
|
||||
|
|
@ -45,7 +54,30 @@ BODY_GAP = 5.0 # clearance below body text; stext line bboxes clip
|
|||
# descenders, which would otherwise leak into a crop
|
||||
PAD = 6.0 # padding around the ink bbox, in points
|
||||
|
||||
CAPTION_RE = re.compile(r"^\s*Figure\s+(\d+(?:\.\d+)?)\s*\.\s*(.*)$")
|
||||
# Two printed caption conventions are supported:
|
||||
# "Figure 12. Title" / "Figure 4.1. Title" -- number, period, title
|
||||
# "Figure 3-8 Title" / "Figure A-14 Title" -- chapter/appendix-hyphen-number,
|
||||
# no period (USB Type-C spec)
|
||||
# Group "num" is the figure number as printed, and is used verbatim in the
|
||||
# output filename ("figure-4.1.png", "figure-3-8.png", "figure-A-14.png").
|
||||
CAPTION_RE = re.compile(
|
||||
r"^\s*Figure\s+(?:"
|
||||
r"(?P<dotted>\d+(?:\.\d+)?)\s*\.\s*" # Figure 12. / Figure 4.1.
|
||||
r"|"
|
||||
r"(?P<hyphen>(?:\d+|[A-Z])-\d+)\s*\.?(?:\s+|$)" # Figure 3-8 / Figure A-14
|
||||
r")(?P<title>.*)$"
|
||||
)
|
||||
|
||||
|
||||
# A bold "Table N ..." caption is body furniture, not figure ink, and bounds a
|
||||
# figure band exactly like a body-text line does. Same two number conventions.
|
||||
TABLE_CAPTION_RE = re.compile(
|
||||
r"^\s*Table\s+(?:\d+(?:\.\d+)?\s*\.|(?:\d+|[A-Z])-\d+)(?:\s|$)"
|
||||
)
|
||||
|
||||
|
||||
def caption_number(m: re.Match) -> str:
|
||||
return m.group("dotted") or m.group("hyphen")
|
||||
|
||||
INK_THRESHOLD = 200 # 0-255 grey; anything darker counts as ink
|
||||
ANALYSIS_DPI = 100 # cheap render used only for ink detection
|
||||
|
|
@ -81,6 +113,7 @@ class Figure:
|
|||
bottom: float
|
||||
left: float
|
||||
right: float
|
||||
name: str = "" # output filename, assigned during extraction
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> str:
|
||||
|
|
@ -123,7 +156,7 @@ def find_captions(lines: list[Line]) -> list[Caption]:
|
|||
m = CAPTION_RE.match(line.text)
|
||||
if not m or line.size > CAPTION_MAX_SIZE or not line.bold:
|
||||
continue
|
||||
title = m.group(2).strip()
|
||||
title = m.group("title").strip()
|
||||
# Absorb continuation lines: same size, directly below, horizontally
|
||||
# overlapping (a side-by-side caption is not a continuation), and not
|
||||
# itself a new caption.
|
||||
|
|
@ -143,7 +176,7 @@ def find_captions(lines: list[Line]) -> list[Caption]:
|
|||
title = f"{title} {nxt.text.strip()}".strip()
|
||||
x0, x1 = min(x0, nxt.x0), max(x1, nxt.x1)
|
||||
prev = nxt
|
||||
out.append(Caption(m.group(1), title, line.y0, prev.y1, x0, x1))
|
||||
out.append(Caption(caption_number(m), title, line.y0, prev.y1, x0, x1))
|
||||
out.sort(key=lambda c: (c.top, c.x0))
|
||||
return out
|
||||
|
||||
|
|
@ -161,6 +194,25 @@ def caption_rows(captions: list[Caption]) -> list[list[Caption]]:
|
|||
return rows
|
||||
|
||||
|
||||
def is_body_line(line: Line) -> bool:
|
||||
"""Body text / heading / table caption, i.e. page furniture that bounds a
|
||||
figure band -- as opposed to a label drawn inside the figure itself.
|
||||
|
||||
Three shapes count:
|
||||
* a line at the left margin (plain paragraphs, section headings);
|
||||
* an indented but full-width line (bullets and numbered lists) -- only
|
||||
recognised once --body-min-width is set, because the cut-off has to sit
|
||||
above the widest label inside any figure. A right-edge test would not
|
||||
do: a right-aligned label inside a figure also ends at the margin;
|
||||
* a bold "Table N ..." caption, which is often centred and short.
|
||||
"""
|
||||
if line.size < BODY_MIN_SIZE:
|
||||
return False
|
||||
if line.x0 <= BODY_MAX_X0 or line.x1 - line.x0 >= BODY_MIN_WIDTH:
|
||||
return True
|
||||
return bool(line.bold and TABLE_CAPTION_RE.match(line.text))
|
||||
|
||||
|
||||
def figures_on_page(pdf: Path, page: int) -> list[Figure]:
|
||||
width, _, lines = page_lines(pdf, page)
|
||||
captions = find_captions(lines)
|
||||
|
|
@ -182,7 +234,7 @@ def figures_on_page(pdf: Path, page: int) -> list[Figure]:
|
|||
continue
|
||||
if line.y0 >= FOOTER_TOP:
|
||||
continue
|
||||
is_body = line.size >= BODY_MIN_SIZE and line.x0 <= BODY_MAX_X0
|
||||
is_body = is_body_line(line)
|
||||
if is_body and line.y1 + BODY_GAP > upper:
|
||||
# Do not step over an earlier row's figures.
|
||||
if any(upper < c.top < line.y1 for pr in rows[:r] for c in pr):
|
||||
|
|
@ -225,7 +277,7 @@ def figures_on_page_above(pdf: Path, page: int) -> list[Figure]:
|
|||
continue
|
||||
if line.bold and CAPTION_RE.match(line.text):
|
||||
continue # a real next caption; already bounded via rows[r+1]
|
||||
is_body = line.size >= BODY_MIN_SIZE and line.x0 <= BODY_MAX_X0
|
||||
is_body = is_body_line(line)
|
||||
if is_body and line.y0 - BODY_GAP < lower:
|
||||
lower = line.y0 - BODY_GAP
|
||||
# Split the row horizontally at the midpoints between its captions.
|
||||
|
|
@ -339,6 +391,10 @@ def extract(pdf: Path, outdir: Path, dpi: int, dry_run: bool,
|
|||
re.search(r"Pages:\s+(\d+)", run(["pdfinfo", str(pdf)])).group(1)
|
||||
)
|
||||
results: list[tuple[Figure, str]] = []
|
||||
# Multi-page drawings repeat the same caption verbatim (with ", cont."
|
||||
# appended) on every sheet. Keep them all: the first sheet keeps the plain
|
||||
# name, later ones get -cont1, -cont2, ... in page order.
|
||||
seen: dict[str, int] = {}
|
||||
tmp = Path(tempfile.mkdtemp(prefix="figextract-"))
|
||||
try:
|
||||
for page in range(1, pages + 1):
|
||||
|
|
@ -358,6 +414,9 @@ def extract(pdf: Path, outdir: Path, dpi: int, dry_run: bool,
|
|||
"-l", str(page), "-singlefile", str(pdf), str(full)])
|
||||
|
||||
for fig in figs:
|
||||
n = seen.get(fig.number, 0)
|
||||
seen[fig.number] = n + 1
|
||||
fig.name = f"figure-{fig.number}{'' if n == 0 else f'-cont{n}'}.png"
|
||||
box = ink_bbox(pgm, fig.top, fig.bottom, scale,
|
||||
fig.left, fig.right)
|
||||
if box is None:
|
||||
|
|
@ -369,7 +428,7 @@ def extract(pdf: Path, outdir: Path, dpi: int, dry_run: bool,
|
|||
x0, x1 = max(fig.left, x0 - PAD), min(fig.right, x1 + PAD)
|
||||
y0 = max(y0 - PAD, fig.top)
|
||||
y1 = min(y1 + PAD, fig.bottom - 2.0)
|
||||
name = f"figure-{fig.number}.png"
|
||||
name = fig.name
|
||||
note = (f"p{fig.page} band {fig.top:.0f}-{fig.bottom:.0f}pt "
|
||||
f"x {fig.left:.0f}-{fig.right:.0f}pt "
|
||||
f"-> ink {x0:.0f},{y0:.0f} {x1 - x0:.0f}x{y1 - y0:.0f}pt")
|
||||
|
|
@ -387,6 +446,7 @@ def extract(pdf: Path, outdir: Path, dpi: int, dry_run: bool,
|
|||
|
||||
|
||||
def main() -> int:
|
||||
global HEADER_BOTTOM, FOOTER_TOP, BODY_MAX_X0, BODY_MIN_WIDTH, BODY_GAP, PAD
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("pdfs", nargs="+", type=Path)
|
||||
|
|
@ -402,8 +462,37 @@ def main() -> int:
|
|||
ap.add_argument("--caption-position", choices=["below", "above"], default="below",
|
||||
help="whether 'Figure N. Title' sits below the figure "
|
||||
"(Renesas-style, default) or above it (e.g. USB PD spec)")
|
||||
# Page-layout tuning. Defaults match the Renesas template; override rather
|
||||
# than edit for a different vendor's page furniture.
|
||||
ap.add_argument("--header-bottom", type=float, default=HEADER_BOTTOM,
|
||||
metavar="PT", help=f"y below the running head "
|
||||
f"(default {HEADER_BOTTOM:g})")
|
||||
ap.add_argument("--footer-top", type=float, default=FOOTER_TOP,
|
||||
metavar="PT", help=f"y above the page footer "
|
||||
f"(default {FOOTER_TOP:g})")
|
||||
ap.add_argument("--body-max-x0", type=float, default=BODY_MAX_X0,
|
||||
metavar="PT", help="max left edge of a body-text/heading "
|
||||
f"line (default {BODY_MAX_X0:g}; the USB "
|
||||
"Type-C template indents to 72)")
|
||||
ap.add_argument("--body-min-width", type=float, default=BODY_MIN_WIDTH,
|
||||
metavar="PT", help="treat an indented line (bullet, numbered "
|
||||
"list) as body text when it is at least "
|
||||
"this wide (default: off)")
|
||||
ap.add_argument("--body-gap", type=float, default=BODY_GAP,
|
||||
metavar="PT", help=f"clearance around body text "
|
||||
f"(default {BODY_GAP:g})")
|
||||
ap.add_argument("--pad", type=float, default=PAD,
|
||||
metavar="PT", help=f"padding around the ink bbox "
|
||||
f"(default {PAD:g})")
|
||||
args = ap.parse_args()
|
||||
|
||||
HEADER_BOTTOM = args.header_bottom
|
||||
FOOTER_TOP = args.footer_top
|
||||
BODY_MAX_X0 = args.body_max_x0
|
||||
BODY_MIN_WIDTH = args.body_min_width
|
||||
BODY_GAP = args.body_gap
|
||||
PAD = args.pad
|
||||
|
||||
if args.region and len(args.pdfs) != 1:
|
||||
ap.error("--region applies to exactly one PDF")
|
||||
|
||||
|
|
@ -412,7 +501,7 @@ def main() -> int:
|
|||
outdir = root / pdf.stem
|
||||
print(f"\n=== {pdf.name} -> {outdir}/")
|
||||
for fig, note in extract(pdf, outdir, args.dpi, args.dry_run, args.caption_position):
|
||||
print(f" figure-{fig.number} {note}")
|
||||
print(f" {fig.name[:-4]} {note}")
|
||||
print(f" {fig.title[:88]}")
|
||||
for spec in args.region:
|
||||
note = crop_region(pdf, outdir, args.dpi, spec, args.dry_run)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue