This commit is contained in:
Nicolas Predella 2026-07-31 15:15:31 +03:00
parent 5b38682b93
commit 9b3b2cca94
4 changed files with 26 additions and 49 deletions

View file

@ -31,6 +31,7 @@ from .sa2_measures import (
from .writer import ( from .writer import (
MM_TO_PT, MM_TO_PT,
_format_pdf_lines, _format_pdf_lines,
escape_pdf_text,
export_mesh_to_pdf, export_mesh_to_pdf,
export_wireframe_to_pdf, export_wireframe_to_pdf,
) )
@ -38,6 +39,7 @@ from .writer import (
__all__ = [ __all__ = [
"export_mesh_to_pdf", "export_mesh_to_pdf",
"export_wireframe_to_pdf", "export_wireframe_to_pdf",
"escape_pdf_text",
"deduplicate_2d_edges", "deduplicate_2d_edges",
"extract_unique_top_view_edges", "extract_unique_top_view_edges",
"extract_lineart_edges", "extract_lineart_edges",

View file

@ -19,28 +19,10 @@ from .sizing import SA2Sizing, compute_effective_stroke_and_font, MM_TO_PT
def escape_pdf_text(text: str) -> str: def escape_pdf_text(text: str) -> str:
""" # For PDF text strings, escape only the PDF special characters: \, (, )
Escapes special PDF characters (parentheses, backslashes) and converts # Non-ASCII characters will be encoded as cp1252 bytes in the stream
WinAnsiEncoding symbols (Ø -> \330, ° -> \260, × -> \327, ± -> \261, µ -> \265) # which matches the WinAnsiEncoding of the font
into PDF octal escape codes. return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
"""
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)
@ -379,10 +361,10 @@ def render_sa2_measures_pdf(
if abs(h_z_depth - total_z_span) < 0.2: if abs(h_z_depth - total_z_span) < 0.2:
depth_str = " THRU" depth_str = " THRU"
elif h_z_depth > 0.1: elif h_z_depth > 0.1:
depth_str = f" \u2334{h_z_depth:.1f}mm" depth_str = f" x {h_z_depth:.1f} DP"
dia_val = round(hdia, 1) dia_val = round(hdia, 1)
dia_str = f"Ø{dia_val:g}mm{depth_str}" dia_str = f"Ø {dia_val:g}{depth_str}"
y_text_offset = ys1 + 2.0 - (line_idx * font_size_pt * 1.1) y_text_offset = ys1 + 2.0 - (line_idx * font_size_pt * 1.1)
stream_cmds.append("BT") stream_cmds.append("BT")
@ -417,7 +399,7 @@ def render_sa2_measures_pdf(
stream_cmds.append(f"{p_e2[0,0]:.2f} {p_e2[0,1]:.2f} m {p_h2[0,0]:.2f} {p_h2[0,1]:.2f} l S") stream_cmds.append(f"{p_e2[0,0]:.2f} {p_e2[0,1]:.2f} m {p_h2[0,0]:.2f} {p_h2[0,1]:.2f} l S")
stream_cmds.append(f"{p_h1[0,0]:.2f} {p_h1[0,1]:.2f} m {p_h2[0,0]:.2f} {p_h2[0,1]:.2f} l S") stream_cmds.append(f"{p_h1[0,0]:.2f} {p_h1[0,1]:.2f} m {p_h2[0,0]:.2f} {p_h2[0,1]:.2f} l S")
pitch_str = f"PITCH X: {dx_mm:.1f}mm" pitch_str = f"{dx_mm:.1f} C-C"
stream_cmds.append("BT") stream_cmds.append("BT")
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf") stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
stream_cmds.append(f"1 0 0 1 {(p_h1[0,0]+p_h2[0,0])/2.0:.2f} {p_h1[0,1] - font_size_pt * 0.9:.2f} Tm") stream_cmds.append(f"1 0 0 1 {(p_h1[0,0]+p_h2[0,0])/2.0:.2f} {p_h1[0,1] - font_size_pt * 0.9:.2f} Tm")
@ -436,7 +418,7 @@ def render_sa2_measures_pdf(
stream_cmds.append(f"{p_e2[0,0]:.2f} {p_e2[0,1]:.2f} m {p_v2[0,0]:.2f} {p_v2[0,1]:.2f} l S") stream_cmds.append(f"{p_e2[0,0]:.2f} {p_e2[0,1]:.2f} m {p_v2[0,0]:.2f} {p_v2[0,1]:.2f} l S")
stream_cmds.append(f"{p_v1[0,0]:.2f} {p_v1[0,1]:.2f} m {p_v2[0,0]:.2f} {p_v2[0,1]:.2f} l S") stream_cmds.append(f"{p_v1[0,0]:.2f} {p_v1[0,1]:.2f} m {p_v2[0,0]:.2f} {p_v2[0,1]:.2f} l S")
pitch_str = f"PITCH Y: {dy_mm:.1f}mm" pitch_str = f"{dy_mm:.1f} C-C"
stream_cmds.append("BT") stream_cmds.append("BT")
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf") stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
stream_cmds.append(f"1 0 0 1 {p_v1[0,0] + 2.0:.2f} {(p_v1[0,1]+p_v2[0,1])/2.0:.2f} Tm") stream_cmds.append(f"1 0 0 1 {p_v1[0,0] + 2.0:.2f} {(p_v1[0,1]+p_v2[0,1])/2.0:.2f} Tm")

View file

@ -31,28 +31,10 @@ from .sizing import (
def escape_pdf_text(text: str) -> str: def escape_pdf_text(text: str) -> str:
""" # For PDF text strings, escape only the PDF special characters: \, (, )
Escapes special PDF characters (parentheses, backslashes) and converts # Non-ASCII characters will be encoded as cp1252 bytes in the stream
WinAnsiEncoding symbols (Ø -> \330, ° -> \260, × -> \327, ± -> \261, µ -> \265) # which matches the WinAnsiEncoding of the font
into PDF octal escape codes. return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
"""
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]: def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]:
@ -496,7 +478,7 @@ def export_mesh_to_pdf(
stream_cmds.append("ET") stream_cmds.append("ET")
stream_content = "\n".join(stream_cmds) + "\n" stream_content = "\n".join(stream_cmds) + "\n"
stream_bytes = stream_content.encode("utf-8") stream_bytes = stream_content.encode("cp1252", errors="replace")
objects: list[bytes] = [] objects: list[bytes] = []
objects.append(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") objects.append(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")

View file

@ -50,12 +50,23 @@ from NodeSlicer.core.pdf_exporter import (
extract_slicing_object_features, extract_slicing_object_features,
export_wireframe_to_pdf, export_wireframe_to_pdf,
export_mesh_to_pdf, export_mesh_to_pdf,
escape_pdf_text,
MM_TO_PT MM_TO_PT
) )
class TestPdfExporter(unittest.TestCase): class TestPdfExporter(unittest.TestCase):
def test_escape_pdf_text_special_characters(self):
escaped = escape_pdf_text("Ø8mm (test) 45° × 2 ± 0.1 µm")
# With cp1252 encoding, special chars pass through directly
self.assertIn("Ø8mm", escaped)
self.assertIn("\\(test\\)", escaped)
self.assertIn("°", escaped)
self.assertIn("×", escaped)
self.assertIn("±", escaped)
self.assertIn("µ", escaped)
def test_extract_edges_from_slicing_object(self): def test_extract_edges_from_slicing_object(self):
so = SlicingObject() so = SlicingObject()
# Create a simple triangle in 3D: (0,0,0), (10,0,0), (0,10,0) with normal (0,0,1) # Create a simple triangle in 3D: (0,0,0), (10,0,0), (0,10,0) with normal (0,0,1)