PDFTranslator / pdf2zh /render /config.py
hoang.nguyen6
deploy
f66643d unverified
Raw
History Blame Contribute Delete
5.1 kB
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class StyleSpec:
weight: str = "regular" # "regular" | "bold"
style_: str = "normal" # "normal" | "italic"
align: str = "left" # "left" | "center" | "right"
@dataclass
class SizingConfig:
detect: bool = True
cluster_eps_pt: float = 2.0
# Maps group name → list of labels belonging to that group
cluster_groups: dict[str, list[str]] = field(
default_factory=lambda: {
"body": [
"Text",
"ListItem",
"Footnote",
"Handwriting",
"TextInlineMath",
],
"toc": ["TableOfContents"],
"equation": ["Equation"],
"headings": ["SectionHeader"],
"header_footer": ["PageHeader", "PageFooter"],
"caption": ["Caption"],
}
)
# "document" = cluster across all pages; "page" = cluster per page
cluster_scope_by_group: dict[str, str] = field(
default_factory=lambda: {
"body": "page",
"toc": "document",
"equation": "page",
"headings": "page",
"header_footer": "page",
"caption": "page",
}
)
cap_height_ratio: float = 0.8
fallback_size: float = 11.0
# Used by _estimate_fit_size: avg char width / font_size and line-height / font_size
char_width_ratio: float = 0.55
leading_ratio: float = 1.25
# Table cell tweaks: slightly smaller font + inset to avoid border overlap
cell_font_scale: float = 0.88
cell_bbox_inset_pt: float = 2.0
@dataclass
class BackgroundConfig:
enabled: bool = True
sample_margin_pt: float = 6.0
dpi_scale: float = 2.0
complexity_brightness_spread: float = 72.0
text_contamination_dark_value: int = 220
text_contamination_dark_ratio: float = 0.015
min_sample_pixels: int = 24
eraser_padding_pt: float = 1.5
fallback_bg: tuple[int, int, int] = (255, 255, 255)
@dataclass
class TextColorConfig:
enabled: bool = True
center_fraction: float = 0.6
fallback: tuple[int, int, int] = (0, 0, 0)
@dataclass
class CompressConfig:
subset_fonts: bool = True
deflate: bool = True
pikepdf_image_recompress: bool = False
target_dpi: int = 200
jpeg_quality: int = 78
@dataclass
class RenderConfig:
# Typst font configuration
typst_font_paths: list[str] = field(default_factory=list)
# Single name or fallback chain. Typst tries each in order when a glyph
# is missing — useful for mixed-script content (Vietnamese, Greek, etc.).
font_family: str | list[str] = "Helvetica"
# Optional per-label style overrides
styles: dict[str, StyleSpec] = field(
default_factory=lambda: {
"SectionHeader": StyleSpec(weight="bold"),
"PageHeader": StyleSpec(align="center"),
"PageFooter": StyleSpec(align="center"),
"Caption": StyleSpec(style_="italic", align="center"),
"TableOfContents": StyleSpec(),
}
)
default_style: StyleSpec = field(default_factory=StyleSpec)
cell_style: StyleSpec = field(default_factory=StyleSpec)
sizing: SizingConfig = field(default_factory=SizingConfig)
background: BackgroundConfig = field(default_factory=BackgroundConfig)
text_color: TextColorConfig = field(default_factory=TextColorConfig)
compress: CompressConfig = field(default_factory=CompressConfig)
min_font_size_pt: float = 7.0
expand_downward: bool = True
max_expand_pt: float = 80.0
# Remove native text layer in translatable regions (needed for non-scanned PDFs)
redact_native_text: bool = True
pages: list[int] | None = None
typst_binary: str = "typst"
keep_typst_source: bool = False
# Legacy PyMuPDF fallback fields (kept for the fallback renderer)
font_path: str = ""
font_name: str = "Body"
@classmethod
def from_json(cls, path: str | Path) -> "RenderConfig":
import json
data = json.loads(Path(path).read_text(encoding="utf-8"))
cfg = cls()
if "font_family" in data:
cfg.font_family = data["font_family"]
if "typst_font_paths" in data:
cfg.typst_font_paths = data["typst_font_paths"]
if "typst_binary" in data:
cfg.typst_binary = data["typst_binary"]
if "font_path" in data:
cfg.font_path = data["font_path"]
if "min_font_size_pt" in data:
cfg.min_font_size_pt = float(data["min_font_size_pt"])
if "pages" in data:
cfg.pages = data["pages"]
_load_nested(cfg.sizing, data.get("sizing", {}))
_load_nested(cfg.background, data.get("background", {}))
_load_nested(cfg.text_color, data.get("text_color", {}))
_load_nested(cfg.compress, data.get("compress", {}))
return cfg
def _load_nested(obj: Any, d: dict) -> None:
for k, v in d.items():
if hasattr(obj, k):
setattr(obj, k, v)