Skip to content

Commit

Permalink
Merge pull request #1155 from mbway/pdf_layers
Browse files Browse the repository at this point in the history
Optional content groups in muPDF backend
  • Loading branch information
mozman authored Aug 31, 2024
2 parents 1b7a647 + b00b250 commit 28b99dc
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 41 deletions.
16 changes: 9 additions & 7 deletions src/ezdxf/addons/drawing/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def __init__(
# RenderContext contains all information to resolve resources for a
# specific DXF document.
self.ctx = ctx

# the render pipeline is the connection between frontend and backend
self.pipeline = pipeline
pipeline.set_draw_entities_callback(self.draw_entities_callback)
Expand Down Expand Up @@ -227,8 +227,8 @@ def override_properties(self, entity: DXFGraphic, properties: Properties) -> Non
.. versionchanged:: 1.3.0
This method is the first function in the stack of new property override
functions. It is possible to push additional override functions onto this
This method is the first function in the stack of new property override
functions. It is possible to push additional override functions onto this
stack, see also :meth:`push_property_override_function`.
"""
Expand All @@ -241,7 +241,7 @@ def push_property_override_function(self, override_fn: TEntityFunc) -> None:
function, because the DXF entities are not copies - except for virtual entities.
The override functions are called after resolving the DXF attributes of an entity
and before the :meth:`Frontend.draw_entity` method in the order from first to
and before the :meth:`Frontend.draw_entity` method in the order from first to
last.
.. versionadded:: 1.3.0
Expand Down Expand Up @@ -566,8 +566,9 @@ def timeout() -> bool:
line_pattern = baseline.pattern_renderer(line.distance)
for s, e in line_pattern.render(line.start, line.end):
if ocs.transform:
s, e = ocs.to_wcs((s.x, s.y, elevation)), ocs.to_wcs(
(e.x, e.y, elevation)
s, e = (
ocs.to_wcs((s.x, s.y, elevation)),
ocs.to_wcs((e.x, e.y, elevation)),
)
lines.append((s, e))
self.pipeline.draw_solid_lines(lines, properties)
Expand Down Expand Up @@ -750,7 +751,8 @@ def draw_image_entity(self, entity: DXFGraphic, properties: Properties) -> None:

if image.transparency != 0.0:
loaded_image = _multiply_alpha(
loaded_image, 1.0 - image.transparency # type: ignore
loaded_image,
1.0 - image.transparency, # type: ignore
)
image_data = ImageData(
image=np.array(loaded_image),
Expand Down
2 changes: 2 additions & 0 deletions src/ezdxf/addons/drawing/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ class Settings:
e.g. 0.15 is 15% of :attr:`max_stroke_width`
output_coordinate_space: expert feature to map the DXF coordinates to the
output coordinate system [0, output_coordinate_space]
output_layers: For supported backends, separate the entities into 'layers' in the output
"""

Expand All @@ -321,6 +322,7 @@ class Settings:
# dimension - aspect ratio is always preserved - these are CAD drawings!
# The SVGBackend uses this feature to map all coordinates to integer values:
output_coordinate_space: float = 1_000_000 # e.g. for SVGBackend
output_layers: bool = True

def __post_init__(self) -> None:
if self.content_rotation not in (0, 90, 180, 270):
Expand Down
16 changes: 9 additions & 7 deletions src/ezdxf/addons/drawing/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,16 @@ def set_colors(self, bg: Color, fg: Optional[Color] = None) -> None:


class RenderContext:
"""The render context for the given DXF document. The :class:`RenderContext`
resolves the properties of DXF entities from the context they reside in to actual
"""The render context for the given DXF document. The :class:`RenderContext`
resolves the properties of DXF entities from the context they reside in to actual
values like RGB colors, transparency, linewidth and so on.
A given `ctb` file (plot style file) overrides the default properties for all
A given `ctb` file (plot style file) overrides the default properties for all
layouts, which means the plot style table stored in the layout is always ignored.
Args:
doc: DXF document
ctb: path to a plot style table or a :class:`~ezdxf.addons.acadctb.ColorDependentPlotStyles`
ctb: path to a plot style table or a :class:`~ezdxf.addons.acadctb.ColorDependentPlotStyles`
instance
export_mode: Whether to render the document as it would look when
exported (plotted) by a CAD application to a file such as pdf,
Expand Down Expand Up @@ -389,10 +389,10 @@ def _override_layer_properties(self, layers: Sequence[LayerProperties]):

def set_current_layout(self, layout: Layout, ctb: str | CTB = ""):
"""Set the current layout and update layout specific properties.
Args:
layout: modelspace or a paperspace layout
ctb: path to a plot style table or a :class:`~ezdxf.addons.acadctb.ColorDependentPlotStyles`
ctb: path to a plot style table or a :class:`~ezdxf.addons.acadctb.ColorDependentPlotStyles`
instance
"""
Expand All @@ -405,7 +405,9 @@ def set_current_layout(self, layout: Layout, ctb: str | CTB = ""):
# last is the ctb stored in the layout
ctb = layout.get_plot_style_filename()
elif not isinstance(ctb, CTB):
raise TypeError(f"expected argument ctb of type str or {CTB.__name__}, got {type(ctb)}")
raise TypeError(
f"expected argument ctb of type str or {CTB.__name__}, got {type(ctb)}"
)
self.current_layout_properties = LayoutProperties.from_layout(layout)
self.plot_styles = self._load_plot_style_table(ctb)
self.layers = dict()
Expand Down
45 changes: 33 additions & 12 deletions src/ezdxf/addons/drawing/pymupdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
from __future__ import annotations

import math
from typing import Iterable, no_type_check
from typing import Iterable, no_type_check, Any
import copy

import PIL.Image
import numpy as np

from ezdxf.math import Vec2, BoundingBox2d, Matrix44
from ezdxf.math import Vec2, BoundingBox2d
from ezdxf.colors import RGB
from ezdxf.path import Command
from ezdxf.version import __version__
from ezdxf.lldxf.validator import make_table_key as layer_key

from .type_hints import Color
from .backend import BackendInterface, BkPath2d, BkPoints2d, ImageData
Expand All @@ -21,13 +22,13 @@
from . import layout, recorder

is_pymupdf_installed = True
pymupdf: Any = None
try:
import fitz
import pymupdf # type: ignore[import-untyped, no-redef]
except ImportError:
print(
"Python module PyMuPDF (AGPL!) is required: https://pypi.org/project/PyMuPDF/"
)
fitz = None
is_pymupdf_installed = False
# PyMuPDF docs: https://pymupdf.readthedocs.io/en/latest/

Expand Down Expand Up @@ -194,16 +195,17 @@ def __init__(self, page: layout.Page, settings: layout.Settings) -> None:
assert (
is_pymupdf_installed
), "Python module PyMuPDF is required: https://pypi.org/project/PyMuPDF/"
self.doc = fitz.open()
self.doc = pymupdf.open()
self.doc.set_metadata(
{
"producer": f"PyMuPDF {fitz.version[0]}",
"producer": f"PyMuPDF {pymupdf.version[0]}",
"creator": f"ezdxf {__version__}",
}
)
self.settings = settings
self._stroke_width_cache: dict[float, float] = dict()
self._color_cache: dict[str, tuple[float, float, float]] = dict()
self._optional_content_groups: dict[str, int] = {}
self._stroke_width_cache: dict[float, float] = {}
self._color_cache: dict[str, tuple[float, float, float]] = {}
self.page_width_in_pt = int(page.width_in_mm * MM_TO_POINTS)
self.page_height_in_pt = int(page.height_in_mm * MM_TO_POINTS)
# LineweightPolicy.ABSOLUTE:
Expand Down Expand Up @@ -255,6 +257,18 @@ def set_background(self, color: Color) -> None:
def new_shape(self):
return self.page.new_shape()

def get_optional_content_group(self, layer_name: str) -> int:
if not self.settings.output_layers:
return 0 # the default value of `oc` when not provided
layer_name = layer_key(layer_name)
if layer_name not in self._optional_content_groups:
self._optional_content_groups[layer_name] = self.doc.add_ocg(
name=layer_name,
config=-1,
on=True,
)
return self._optional_content_groups[layer_name]

def finish_line(self, shape, properties: BackendProperties, close: bool) -> None:
color = self.resolve_color(properties.color)
width = self.resolve_stroke_width(properties.lineweight)
Expand All @@ -266,6 +280,7 @@ def finish_line(self, shape, properties: BackendProperties, close: bool) -> None
lineCap=1,
stroke_opacity=alpha_to_opacity(properties.color[7:9]),
closePath=close,
oc=self.get_optional_content_group(properties.layer),
)

def finish_filling(self, shape, properties: BackendProperties) -> None:
Expand All @@ -278,6 +293,7 @@ def finish_filling(self, shape, properties: BackendProperties) -> None:
lineCap=1,
closePath=True,
even_odd=True,
oc=self.get_optional_content_group(properties.layer),
)

def resolve_color(self, color: Color) -> tuple[float, float, float]:
Expand Down Expand Up @@ -373,7 +389,7 @@ def draw_image(self, image_data: ImageData, properties: BackendProperties) -> No
)
xs = [p.x for p in corners]
ys = [p.y for p in corners]
r = fitz.Rect((min(xs), min(ys)), (max(xs), max(ys)))
r = pymupdf.Rect((min(xs), min(ys)), (max(xs), max(ys)))

# translation and non-uniform scale are handled by having the image stretch to fill the given rect.
angle = (corners[1] - corners[0]).angle_deg
Expand All @@ -396,12 +412,17 @@ def draw_image(self, image_data: ImageData, properties: BackendProperties) -> No
image = np.asarray(pil_image)
height, width, depth = image.shape

pixmap = fitz.Pixmap(
fitz.Colorspace(fitz.CS_RGB), width, height, bytes(image.data), True
pixmap = pymupdf.Pixmap(
pymupdf.Colorspace(pymupdf.CS_RGB), width, height, bytes(image.data), True
)
# TODO: could improve by caching and re-using xrefs. If a document contains many
# identical images redundant copies will be stored for each one
self.page.insert_image(r, keep_proportion=False, pixmap=pixmap)
self.page.insert_image(
r,
keep_proportion=False,
pixmap=pixmap,
oc=self.get_optional_content_group(properties.layer),
)

def configure(self, config: Configuration) -> None:
self.lineweight_policy = config.lineweight_policy
Expand Down
30 changes: 15 additions & 15 deletions src/ezdxf/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pathlib
import tempfile
from typing import Callable, Optional, TYPE_CHECKING, Type
from typing import Callable, Optional, TYPE_CHECKING, Type, Sequence
import abc
import sys
import os
Expand All @@ -30,7 +30,7 @@

if TYPE_CHECKING:
from ezdxf.entities import DXFGraphic
from ezdxf.addons.drawing.properties import Properties
from ezdxf.addons.drawing.properties import Properties, LayerProperties

__all__ = ["get", "add_parsers"]

Expand Down Expand Up @@ -331,12 +331,12 @@ def run(args):
print(str(e))
sys.exit(1)

if args.backend == "matplotlib":
if args.backend == "matplotlib":
try:
file_output = MatplotlibFileOutput(args.dpi)
except ImportError as e:
print(str(e))
sys.exit(1)
sys.exit(1)
elif args.backend == "qt":
try:
file_output = PyQtFileOutput(args.dpi)
Expand All @@ -356,6 +356,8 @@ def run(args):
raise ValueError(args.backend)

verbose = args.verbose
if verbose:
logging.basicConfig(level=logging.INFO)

if args.formats:
print(f"formats supported by {args.backend}:")
Expand Down Expand Up @@ -388,20 +390,18 @@ def run(args):
out = file_output.backend()

if args.all_layers_visible:
for layer_properties in ctx.layers.values():
layer_properties.is_visible = True
def override_layer_properties(layer_properties: Sequence[LayerProperties]) -> None:
for properties in layer_properties:
properties.is_visible = True

if args.all_entities_visible:
ctx.set_layer_properties_override(override_layer_properties)

class AllVisibleFrontend(Frontend):
def override_properties(
self, entity: DXFGraphic, properties: Properties
) -> None:
properties.is_visible = True
frontend = Frontend(ctx, out, config=config)

frontend = AllVisibleFrontend(ctx, out, config=config)
else:
frontend = Frontend(ctx, out, config=config)
if args.all_entities_visible:
def override_entity_properties(entity: DXFGraphic, properties: Properties) -> None:
properties.is_visible = True
frontend.push_property_override_function(override_entity_properties)

t0 = time.perf_counter()
if verbose:
Expand Down

0 comments on commit 28b99dc

Please sign in to comment.