Source code for declaracad.occ.impl.occ_pdf

"""
Copyright (c) 2022, CodeLV.

Distributed under the terms of the GPL v3 License.

The full license is in the file LICENSE, distributed with this software.

Created on Sep 19, 2022

@author: jrm
"""

import io
import os
import warnings
from typing import Optional

from atom.api import Instance, Value, set_default
from OCCT.BRep import BRep_Builder
from OCCT.BRepBuilderAPI import (
    BRepBuilderAPI_MakeEdge,
    BRepBuilderAPI_MakeFace,
    BRepBuilderAPI_MakeWire,
    BRepBuilderAPI_Transform,
)
from OCCT.Geom import Geom_BezierCurve
from OCCT.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCCT.TColgp import TColgp_Array1OfPnt
from OCCT.TopoDS import TopoDS_Compound, TopoDS_Shape

from declaracad.core.utils import log
from declaracad.occ.draw import ProxyPdf

from .occ_shape import OccShape

try:
    from pdf4py.parser import Parser, SequentialParser
    from pdf4py.types import PDFOperator
except ImportError as e:
    warnings.warn(f"pdf4py could not be imported: {e}")
    Parser = object
    SequentialParser = object
    PDFOperator = None


IGNORED_CMDS = {
    "w",  # line width
    "J",  # line cap
    "j",  # line join
    "M",  # miter limit
    "d",  # dash phase
    "ri",  # rendering intent
    "i",  # flatness
    "ds",  # dict name
    # color stuff
    "cs",
    "CS",
    "sc",
    "SC",
    "scn",
    "SCN",
    "g",
    "G",
    "rg",
    "RG",
    "k",
    "K",
}

CLOSE_CMDS = ("h", "s", "b", "b*")
ENDPATH_CMDS = ("h", "s", "S", "f", "b", "b*")


def is_valid_bezier(pts: TColgp_Array1OfPnt, tol=1e-6) -> bool:
    """Check if the bezier control points are all valid the same"""
    return not all(
        pts.Value(i).IsEqual(pts.Value(i + 1), tol) for i in range(1, pts.Size())
    )


def make_rect(x, y, w, h) -> tuple[gp_Pnt, BRepBuilderAPI_MakeWire]:
    """Create a wire rectangle."""
    c0 = gp_Pnt(x, y)
    c1 = gp_Pnt(x + w, y)
    c2 = gp_Pnt(x + w, y + h)
    c3 = gp_Pnt(x, y + h)
    path = BRepBuilderAPI_MakeWire(
        BRepBuilderAPI_MakeEdge(c0, c1).Edge(),
        BRepBuilderAPI_MakeEdge(c1, c2).Edge(),
        BRepBuilderAPI_MakeEdge(c2, c3).Edge(),
        BRepBuilderAPI_MakeEdge(c3, c0).Edge(),
    )
    return c0, path


[docs] class OccPdf(OccShape, ProxyPdf): #: Update the class reference reference = set_default( "https://dev.opencascade.org/doc/refman/html/" "class_b_rep_builder_a_p_i___make_wire.html" ) #: Make wire shape = Instance(TopoDS_Shape) #: The document doc = Instance(Parser) #: File f = Value() def create_shape(self): d = self.declaration if not d.source: return f = self.f if f is not None: f.close() self.f = None if os.path.exists(os.path.expanduser(d.source)): f = self.f = open(os.path.expanduser(d.source), "rb") else: f = self.f = io.BytesIO() f.write(d.source) f.seek(0) doc = self.doc = Parser(f) root = doc.parse_reference(doc.trailer["Root"]) pages = doc.parse_reference(root["Pages"]) builder = BRep_Builder() shape = TopoDS_Compound() builder.MakeCompound(shape) for item in pages["Kids"]: page = doc.parse_reference(item) for s in self.extract_shapes(doc, page): builder.Add(shape, s) if d.scale != 1: bbox = self.get_bounding_box(shape) t = gp_Trsf() t.SetScale(gp_Pnt(*bbox.center), d.scale) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() if d.center: bbox = self.get_bounding_box(shape) x, y, z = bbox.center t = gp_Trsf() t.SetTranslation(gp_Vec(-x, -y, 0)) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() if d.mirror: bbox = self.get_bounding_box(shape) t = gp_Trsf() t.SetMirror(gp_Ax2(gp_Pnt(*bbox.center), gp_Dir(0, 1, 0))) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() t = self.get_transform() shape = BRepBuilderAPI_Transform(shape, t, True).Shape() self.shape = shape def set_source(self, source): self.create_shape() def set_mirror(self, mirror): self.create_shape() def set_fill_mode(self, mode): self.create_shape() def set_center(self, center): self.create_shape() def fill_shape(self, wire): fill_mode = self.declaration.fill_mode if fill_mode == "always" or (fill_mode == "auto" and wire.Closed()): return BRepBuilderAPI_MakeFace(wire).Face() return wire def extract_shapes(self, doc, page): """Parse shapes from the pdf page""" assert page.get("Type", "Type") == "Page" # media_box = page["MediaBox"] contents = doc.parse_reference(page["Contents"]) data = contents.stream() path: Optional[BRepBuilderAPI_MakeWire] = None params = [] stack = [] d = self.declaration ctm = gp_Trsf() last_pnt = gp_Pnt() start_pnt: Optional[gp_Pnt] = None tol = self.declaration.tolerance for token in SequentialParser(data): if not isinstance(token, PDFOperator): params.append(token) continue cmd = token.value # log.debug(f"pdf command: {cmd} {params}") if cmd == "q": stack.append(ctm) ctm = gp_Trsf().Multiplied(ctm) elif cmd == "Q": ctm = stack.pop() # If this fails the pdf is invalid elif cmd in IGNORED_CMDS: pass # no-op elif cmd == "cm": a, b, c, d, e, f = params log.info(params) matrix = gp_Trsf() matrix.SetValues( a, b, 0, e, c, d, 0, f, 0, 0, 1, 0, ) ctm = ctm.Multiplied(matrix) elif cmd == "m": assert path is None path = BRepBuilderAPI_MakeWire() x, y = params last_pnt = start_pnt = gp_Pnt(x, y, 0) n = 0 elif cmd == "re": assert path is None last_pnt, path = make_rect(*params) shape = self.fill_shape(path.Wire()) yield BRepBuilderAPI_Transform(shape, ctm).Shape() path = None elif cmd in "l": # Line to x, y = params pnt = gp_Pnt(x, y, 0) if not last_pnt.IsEqual(pnt, tol): path.Add(BRepBuilderAPI_MakeEdge(last_pnt, pnt).Edge()) last_pnt = pnt elif cmd == "c": # Cubic to n += 1 pts = TColgp_Array1OfPnt(1, 4) pts.SetValue(1, last_pnt) pts.SetValue(2, gp_Pnt(params[0], params[1], 0)) pts.SetValue(3, gp_Pnt(params[2], params[3], 0)) last_pnt = gp_Pnt(params[4], params[5], 0) pts.SetValue(4, last_pnt) if is_valid_bezier(pts): curve = Geom_BezierCurve(pts) path.Add(BRepBuilderAPI_MakeEdge(curve).Edge()) else: log.warning(f"pdf command: {cmd} {params} is invalid") elif cmd in ("v", "y"): # Quad to pts = TColgp_Array1OfPnt(1, 3) pts.SetValue(1, last_pnt) pts.SetValue(2, gp_Pnt(params[0], params[1], 0)) last_pnt = gp_Pnt(params[2], params[3], 0) pts.SetValue(3, last_pnt) if is_valid_bezier(pts): curve = Geom_BezierCurve(pts) path.Add(BRepBuilderAPI_MakeEdge(curve).Edge()) else: log.warning(f"pdf command: {cmd} {params} is invalid") elif cmd == "n": path = None elif cmd in ENDPATH_CMDS: if path is not None: if cmd in CLOSE_CMDS and not last_pnt.IsEqual(start_pnt, tol): path.Add(BRepBuilderAPI_MakeEdge(last_pnt, start_pnt).Edge()) shape = self.fill_shape(path.Wire()) yield BRepBuilderAPI_Transform(shape, ctm).Shape() path = None start_pnt = None else: log.warning(f"pdf command ignored: {cmd} {params}") # Reset params = []