Spaces:
Paused
Paused
File size: 5,101 Bytes
f66643d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 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)
|