up
This commit is contained in:
parent
71301f85f7
commit
7e43f42c1c
3 changed files with 95 additions and 39 deletions
|
|
@ -12,6 +12,31 @@ from .geometry import extract_lineart_edges, extract_unique_top_view_edges, dedu
|
|||
MM_TO_PT = 2.834645669291339
|
||||
|
||||
|
||||
def escape_pdf_text(text: str) -> str:
|
||||
"""
|
||||
Escapes special PDF characters (parentheses, backslashes) and converts
|
||||
WinAnsiEncoding symbols (Ø -> \330, ° -> \260, × -> \327, ± -> \261, µ -> \265)
|
||||
into PDF octal escape codes.
|
||||
"""
|
||||
res = []
|
||||
for ch in text:
|
||||
if ch == "(":
|
||||
res.append("\\(")
|
||||
elif ch == ")":
|
||||
res.append("\\)")
|
||||
elif ch == "\\":
|
||||
res.append("\\\\")
|
||||
else:
|
||||
code = ord(ch)
|
||||
if code < 128:
|
||||
res.append(ch)
|
||||
elif code <= 255:
|
||||
res.append(f"\\{code:03o}")
|
||||
else:
|
||||
res.append("?")
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def circumcircle_3pts(p1: NDArray, p2: NDArray, p3: NDArray) -> tuple[tuple[float, float] | None, float | None]:
|
||||
"""Calculate center and radius of circumcircle passing through 3 2D points."""
|
||||
ax, ay = float(p1[0]), float(p1[1])
|
||||
|
|
@ -349,13 +374,13 @@ def render_sa2_measures_pdf(
|
|||
x_w_text = (x_w1 + x_w2) / 2.0 - (len(w_str) * font_size_pt * 0.28)
|
||||
y_w_text = y_w - font_size_pt * 1.1
|
||||
stream_cmds.append(f"1 0 0 1 {x_w_text:.2f} {y_w_text:.2f} Tm")
|
||||
stream_cmds.append(f"({w_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(w_str)}) Tj")
|
||||
|
||||
h_str = f"{h_obj_mm/1000:g}m" if main_step >= 1000.0 else f"{h_obj_mm:g}mm"
|
||||
x_h_text = x_h - (len(h_str) * font_size_pt * 0.55) - (2.0 * MM_TO_PT)
|
||||
y_h_text = (y_h1 + y_h2) / 2.0 - (font_size_pt * 0.35)
|
||||
stream_cmds.append(f"1 0 0 1 {x_h_text:.2f} {y_h_text:.2f} Tm")
|
||||
stream_cmds.append(f"({h_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(h_str)}) Tj")
|
||||
|
||||
stream_cmds.append("ET")
|
||||
|
||||
|
|
@ -392,11 +417,11 @@ def render_sa2_measures_pdf(
|
|||
)
|
||||
|
||||
dia_val = round(dia_mm, 1)
|
||||
dia_str = f"DIA {dia_val:g}mm" if main_step < 1000.0 else f"DIA {dia_val/1000:g}m"
|
||||
dia_str = f"Ø{dia_val:g}mm" if main_step < 1000.0 else f"Ø{dia_val/1000:g}m"
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.9:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {x_s1 + 2.0:.2f} {y_s1 + 2.0:.2f} Tm")
|
||||
stream_cmds.append(f"({dia_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(dia_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# 2. ISO 129-1 Arc Cutouts & Corner Fillets (R - Amber Orange)
|
||||
|
|
@ -433,7 +458,7 @@ def render_sa2_measures_pdf(
|
|||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.9:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {xs1 + 2.0:.2f} {ys1 + 2.0:.2f} Tm")
|
||||
stream_cmds.append(f"({r_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(r_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# 3. ISO 129-1 Bevels & Chamfers (C / 45° - Violet Purple)
|
||||
|
|
@ -469,7 +494,7 @@ def render_sa2_measures_pdf(
|
|||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {x_cs1 + 2.0:.2f} {y_cs1 + 2.0:.2f} Tm")
|
||||
stream_cmds.append(f"({c_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(c_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# 4. Hole Center Pitch Distances (Teal Green)
|
||||
|
|
@ -494,7 +519,7 @@ def render_sa2_measures_pdf(
|
|||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {(x1+x2)/2.0:.2f} {(y1+y2)/2.0 + 3.0:.2f} Tm")
|
||||
stream_cmds.append(f"({pitch_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(pitch_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# 5. Interior Step / Notch Feature Segment Lengths (Deep Purple)
|
||||
|
|
@ -514,5 +539,5 @@ def render_sa2_measures_pdf(
|
|||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {(xs1+xs2)/2.0:.2f} {(ys1+ys2)/2.0 + 2.0:.2f} Tm")
|
||||
stream_cmds.append(f"({step_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(step_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,31 @@ from .sa2_measures import render_sa2_measures_pdf
|
|||
MM_TO_PT = 2.834645669291339
|
||||
|
||||
|
||||
def escape_pdf_text(text: str) -> str:
|
||||
"""
|
||||
Escapes special PDF characters (parentheses, backslashes) and converts
|
||||
WinAnsiEncoding symbols (Ø -> \330, ° -> \260, × -> \327, ± -> \261, µ -> \265)
|
||||
into PDF octal escape codes.
|
||||
"""
|
||||
res = []
|
||||
for ch in text:
|
||||
if ch == "(":
|
||||
res.append("\\(")
|
||||
elif ch == ")":
|
||||
res.append("\\)")
|
||||
elif ch == "\\":
|
||||
res.append("\\\\")
|
||||
else:
|
||||
code = ord(ch)
|
||||
if code < 128:
|
||||
res.append(ch)
|
||||
elif code <= 255:
|
||||
res.append(f"\\{code:03o}")
|
||||
else:
|
||||
res.append("?")
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]:
|
||||
"""
|
||||
Formats line segment coordinates (N, 2, 2) in PDF points into raw PDF operators.
|
||||
|
|
@ -296,6 +321,8 @@ def export_mesh_to_pdf(
|
|||
|
||||
# 6. Size Scale Legend (Bottom Left, aligned directly on bottom-left main grid lines with arrows and X/Y labels)
|
||||
stream_cmds.append("/GS100 gs")
|
||||
stream_cmds.append("0.000 0.000 0.000 RG")
|
||||
stream_cmds.append("0.000 0.000 0.000 rg")
|
||||
x_grid_start_mm = grid_min_x
|
||||
x_grid_end_mm = min(grid_max_x, grid_min_x + 10.0 * main_step)
|
||||
num_main_steps = int(np.round((x_grid_end_mm - x_grid_start_mm) / main_step))
|
||||
|
|
@ -361,7 +388,7 @@ def export_mesh_to_pdf(
|
|||
)
|
||||
|
||||
# 6c. Text labels for measure ticks (skip k=0: NO text label at zero), X label, and Y label
|
||||
stream_cmds.append(f"{sr:.3f} {sg:.3f} {sb:.3f} rg")
|
||||
stream_cmds.append("0.000 0.000 0.000 rg")
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt:.1f} Tf")
|
||||
|
||||
|
|
@ -380,10 +407,11 @@ def export_mesh_to_pdf(
|
|||
unit = " mm" if k == num_main_steps else ""
|
||||
tick_str = f"{val_mm:g}{unit}"
|
||||
|
||||
x_text_pt = xk_pt - (len(tick_str) * font_size_pt * 0.28)
|
||||
y_text_pt = y_bar_pt + tick_h_pt + (2.5 * MM_TO_PT)
|
||||
tw_pt = len(tick_str) * font_size_pt * 0.60
|
||||
x_text_pt = xk_pt - (tw_pt / 2.0)
|
||||
y_text_pt = y_bar_pt + tick_h_pt + (2.0 * MM_TO_PT)
|
||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_text_pt:.2f} Tm")
|
||||
stream_cmds.append(f"({tick_str}) Tj")
|
||||
stream_cmds.append(f"({escape_pdf_text(tick_str)}) Tj")
|
||||
|
||||
# X axis label next to right arrowhead
|
||||
x_x_label_pt = x_arrow_tip_pt + (2.0 * MM_TO_PT)
|
||||
|
|
@ -404,22 +432,19 @@ def export_mesh_to_pdf(
|
|||
if (show_name or show_bbox_size or settings_items) and raw_geometry is not None:
|
||||
label_infos = extract_object_label_info(raw_geometry)
|
||||
if label_infos:
|
||||
font_size_pt = max(8.0, line_width_pt * 4.0)
|
||||
line_spacing_pt = font_size_pt * 1.2
|
||||
eff_font_size_pt = font_size_pt if font_size_pt is not None and font_size_pt > 0 else max(8.0, line_width_pt * 4.0)
|
||||
line_spacing_pt = eff_font_size_pt * 1.25
|
||||
stream_cmds.append("/GS100 gs")
|
||||
stream_cmds.append(f"{sr:.3f} {sg:.3f} {sb:.3f} rg")
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt:.1f} Tf")
|
||||
|
||||
for so_item, obj_name, max_x_mm, y_val_mm, w_obj_mm, h_obj_mm in label_infos:
|
||||
p_pt = to_pdf_pt(np.array([[max_x_mm, y_val_mm]]))
|
||||
x_text_pt = p_pt[0, 0] + (2.5 * MM_TO_PT)
|
||||
y_text_pt = p_pt[0, 1] - (font_size_pt * 0.35)
|
||||
y_text_pt = p_pt[0, 1] - (eff_font_size_pt * 0.35)
|
||||
|
||||
lines_to_draw: list[str] = []
|
||||
lines_to_draw: list[tuple[str, tuple[float, float, float]]] = []
|
||||
|
||||
if show_name:
|
||||
lines_to_draw.append(str(obj_name))
|
||||
lines_to_draw.append((str(obj_name), stroke_color))
|
||||
|
||||
if show_bbox_size:
|
||||
if main_step >= 1000.0:
|
||||
|
|
@ -428,7 +453,7 @@ def export_mesh_to_pdf(
|
|||
size_str = f"(X: {w_val:g}m Y: {h_val:g}m)"
|
||||
else:
|
||||
size_str = f"(X: {w_obj_mm:g}mm Y: {h_obj_mm:g}mm)"
|
||||
lines_to_draw.append(size_str)
|
||||
lines_to_draw.append((size_str, bbox_color))
|
||||
|
||||
for key_name, suffix in settings_items:
|
||||
attr_val = get_first_attribute_value(so_item, key_name)
|
||||
|
|
@ -439,10 +464,16 @@ def export_mesh_to_pdf(
|
|||
val_str = str(attr_val)
|
||||
if suffix:
|
||||
val_str = f"{val_str}{suffix}"
|
||||
lines_to_draw.append(f"{key_name}: {val_str}")
|
||||
lines_to_draw.append((f"{key_name}: {val_str}", stroke_color))
|
||||
|
||||
for line_idx, line_str in enumerate(lines_to_draw):
|
||||
clean_str = line_str.replace("(", "\\(").replace(")", "\\)")
|
||||
if lines_to_draw:
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {eff_font_size_pt:.1f} Tf")
|
||||
|
||||
for line_idx, (line_str, col) in enumerate(lines_to_draw):
|
||||
cr, cg, cb = col
|
||||
stream_cmds.append(f"{cr:.3f} {cg:.3f} {cb:.3f} rg")
|
||||
clean_str = escape_pdf_text(line_str)
|
||||
curr_y_pt = y_text_pt - (line_idx * line_spacing_pt)
|
||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {curr_y_pt:.2f} Tm")
|
||||
stream_cmds.append(f"({clean_str}) Tj")
|
||||
|
|
@ -470,10 +501,10 @@ def export_mesh_to_pdf(
|
|||
objects.append(b"6 0 obj\n<< /Type /ExtGState /CA 0.5 /ca 0.5 >>\nendobj\n")
|
||||
objects.append(b"7 0 obj\n<< /Type /ExtGState /CA 1.0 /ca 1.0 >>\nendobj\n")
|
||||
|
||||
# Embed Open Gorton Typeface
|
||||
font_file_path = Path(__file__).parent / "fonts" / "OpenGorton-Regular.otf"
|
||||
# Embed League Mono Typeface
|
||||
font_file_path = Path(__file__).parent / "fonts" / "LeagueMono-Regular.otf"
|
||||
if not font_file_path.exists():
|
||||
font_file_path = Path(__file__).parents[2] / "fonts" / "OpenGorton-Regular.otf"
|
||||
font_file_path = Path(__file__).parents[2] / "fonts" / "LeagueMono-Regular.otf"
|
||||
|
||||
font_bytes: bytes | None = None
|
||||
if font_file_path.exists():
|
||||
|
|
@ -487,22 +518,22 @@ def export_mesh_to_pdf(
|
|||
widths_str = " ".join(["600"] * 224)
|
||||
objects.append(
|
||||
f"8 0 obj\n"
|
||||
f"<< /Type /Font /Subtype /Type1 /BaseFont /OpenGorton-Regular "
|
||||
f"<< /Type /Font /Subtype /Type1 /BaseFont /LeagueMono-Regular "
|
||||
f"/FirstChar 32 /LastChar 255 "
|
||||
f"/Widths [ {widths_str} ] "
|
||||
f"/FontDescriptor 9 0 R /Encoding /WinAnsiEncoding >>\nendobj\n".encode("utf-8")
|
||||
)
|
||||
objects.append(
|
||||
b"9 0 obj\n"
|
||||
b"<< /Type /FontDescriptor /FontName /OpenGorton-Regular /Flags 32 "
|
||||
b"/FontBBox [-500 -300 1500 1000] /ItalicAngle 0 /Ascent 800 /Descent -200 "
|
||||
b"<< /Type /FontDescriptor /FontName /LeagueMono-Regular /Flags 33 "
|
||||
b"/FontBBox [-200 -200 1200 1000] /ItalicAngle 0 /Ascent 1000 /Descent -200 "
|
||||
b"/CapHeight 700 /StemV 80 /FontFile3 10 0 R >>\nendobj\n"
|
||||
)
|
||||
font_stream_header = f"10 0 obj\n<< /Length {len(font_bytes)} /Subtype /OpenType >>\nstream\n".encode("utf-8")
|
||||
font_stream_footer = b"\nendstream\nendobj\n"
|
||||
objects.append(font_stream_header + font_bytes + font_stream_footer)
|
||||
else:
|
||||
objects.append(b"8 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n")
|
||||
objects.append(b"8 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>\nendobj\n")
|
||||
|
||||
header = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"
|
||||
|
||||
|
|
|
|||
|
|
@ -678,9 +678,9 @@ class TestPdfExporter(unittest.TestCase):
|
|||
|
||||
self.assertIn("/F1 14.5 Tf", content)
|
||||
|
||||
def test_export_pdf_open_gorton_font_embedded(self):
|
||||
def test_export_pdf_league_mono_font_embedded(self):
|
||||
so = SlicingObject()
|
||||
so.name = "GortonTestPart"
|
||||
so.name = "LeagueMonoTestPart"
|
||||
so.mesh = np.array([
|
||||
[[0, 0, 0], [10, 0, 0], [10, 10, 0]],
|
||||
[[0, 0, 0], [10, 10, 0], [0, 10, 0]]
|
||||
|
|
@ -689,7 +689,7 @@ class TestPdfExporter(unittest.TestCase):
|
|||
edges_2d = extract_unique_top_view_edges(so)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
pdf_path = os.path.join(tmpdir, "test_open_gorton.pdf")
|
||||
pdf_path = os.path.join(tmpdir, "test_league_mono.pdf")
|
||||
export_mesh_to_pdf(
|
||||
edges_2d,
|
||||
pdf_path,
|
||||
|
|
@ -700,7 +700,7 @@ class TestPdfExporter(unittest.TestCase):
|
|||
with open(pdf_path, "rb") as f:
|
||||
content = f.read().decode("utf-8", errors="ignore")
|
||||
|
||||
self.assertIn("OpenGorton-Regular", content)
|
||||
self.assertIn("LeagueMono-Regular", content)
|
||||
self.assertIn("/Subtype /OpenType", content)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue