Skip to content

Commit

Permalink
Update pygerber.gerberx3.api to use new toolchain (#302)
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster authored Sep 24, 2024
2 parents a2a4918 + 3632ce7 commit 5198825
Show file tree
Hide file tree
Showing 23 changed files with 581 additions and 111 deletions.
26 changes: 23 additions & 3 deletions src/pygerber/gerberx3/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,28 @@

from __future__ import annotations

from pygerber.gerberx3.api._enums import FileTypeEnum
from pygerber.gerberx3.api._gerber_file import GerberFile, GerberFileInfo
from pygerber.gerberx3.api._enums import (
DEFAULT_ALPHA_COLOR_MAP,
DEFAULT_COLOR_MAP,
FileTypeEnum,
)
from pygerber.gerberx3.api._gerber_file import (
GerberFile,
Image,
ImageSpace,
PillowImage,
Units,
)
from pygerber.gerberx3.api._project import Project

__all__ = ["FileTypeEnum", "GerberFile", "Project", "GerberFileInfo"]
__all__ = [
"FileTypeEnum",
"GerberFile",
"Project",
"Units",
"ImageSpace",
"Image",
"PillowImage",
"DEFAULT_COLOR_MAP",
"DEFAULT_ALPHA_COLOR_MAP",
]
204 changes: 177 additions & 27 deletions src/pygerber/gerberx3/api/_gerber_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, TextIO

from pygerber.gerberx3.api._enums import DEFAULT_COLOR_MAP, FileTypeEnum
import pyparsing as pp

from pygerber.gerberx3.api._enums import (
COLOR_MAP_T,
DEFAULT_ALPHA_COLOR_MAP,
FileTypeEnum,
)
from pygerber.gerberx3.ast import State, get_final_state
from pygerber.gerberx3.ast.nodes.attribute.TF import TF_FileFunction
from pygerber.gerberx3.ast.nodes.enums import UnitMode
from pygerber.gerberx3.compiler import compile
from pygerber.gerberx3.parser import parse
from pygerber.vm import render
from pygerber.vm.pillow import PillowResult
from pygerber.vm.pillow.vm import PillowResult
from pygerber.vm.types.box import Box
from pygerber.vm.types.style import Style

if TYPE_CHECKING:
Expand All @@ -25,6 +33,154 @@
from typing_extensions import Self


class Units(Enum):
"""The `Units` enum contains possible Gerber file units."""

Millimeters = "MM"
Inches = "IN"


class ImageSpace:
"""Container for information about Gerber image space."""

def __init__(self, units: UnitMode, box: Box, dpmm: int) -> None:
self._units = Units(units.value)
self._min_x = box.min_x
self._min_y = box.min_y
self._max_x = box.max_x
self._max_y = box.max_y
self._dpmm = dpmm

@property
def units(self) -> Units:
"""Units of image space."""
return self._units

@property
def min_x(self) -> float:
"""Minimum X coordinate in image in file defined unit."""
return self._min_x

@property
def min_y(self) -> float:
"""Minimum Y coordinate in image in file defined unit."""
return self._min_y

@property
def max_x(self) -> float:
"""Maximum X coordinate in image in file defined unit."""
return self._max_x

@property
def max_y(self) -> float:
"""Maximum T coordinate in image in file defined unit."""
return self._max_y

@property
def dpmm(self) -> int:
"""Resolution of image in dots per millimeter."""
return self._dpmm

@pp.cached_property
def min_x_mm(self) -> float:
"""Minimum X coordinate of image in millimeters."""
return self.min_x if self.units == Units.Millimeters else self.min_x * 25.4

@pp.cached_property
def min_y_mm(self) -> float:
"""Minimum Y coordinate of image in millimeters."""
return self.min_y if self.units == Units.Millimeters else self.min_y * 25.4

@pp.cached_property
def max_x_mm(self) -> float:
"""Maximum X coordinate of image in millimeters."""
return self.max_x if self.units == Units.Millimeters else self.max_x * 25.4

@pp.cached_property
def max_y_mm(self) -> float:
"""Maximum Y coordinate of image in millimeters."""
return self.max_y if self.units == Units.Millimeters else self.max_y * 25.4

@pp.cached_property
def width_mm(self) -> float:
"""Width of image in millimeters."""
return self.max_x_mm - self.min_x_mm

@pp.cached_property
def height_mm(self) -> float:
"""Height of image in millimeters."""
return self.max_y_mm - self.min_y_mm

@pp.cached_property
def center_x_mm(self) -> float:
"""Center X coordinate of image in millimeters.
This value can be negative.
"""
return (self.min_x_mm + self.max_x_mm) / 2

@pp.cached_property
def center_y_mm(self) -> float:
"""Center Y coordinate of image in millimeters.
This value can be negative.
"""
return (self.min_y_mm + self.max_y_mm) / 2

@pp.cached_property
def min_x_pixels(self) -> int:
"""Minimum X coordinate of image in pixels.
This value can be negative.
"""
return int(self.min_x_mm * self.dpmm)

@pp.cached_property
def min_y_pixels(self) -> int:
"""Minimum Y coordinate of image in pixels.
This value can be negative.
"""
return int(self.min_y_mm * self.dpmm)

@pp.cached_property
def max_x_pixels(self) -> int:
"""Maximum X coordinate of image in pixels."""
return int(self.max_x_mm * self.dpmm)

@pp.cached_property
def max_y_pixels(self) -> int:
"""Maximum Y coordinate of image in pixels."""
return int(self.max_y_mm * self.dpmm)


class Image:
"""The `Image` class is a base class for all rendered images returned by
`GerberFile.render_with_*` methods.
"""

def __init__(self, image_space: ImageSpace) -> None:
self._image_space = image_space

def get_image_space(self) -> ImageSpace:
"""Get information about image space."""
return self._image_space


class PillowImage(Image):
"""The `PillowImage` class is a rendered image returned by
`GerberFile.render_with_pillow` method.
"""

def __init__(self, image_space: ImageSpace, image: PIL.Image.Image) -> None:
super().__init__(image_space=image_space)
self._image = image

def get_image(self) -> PIL.Image.Image:
"""Get image object."""
return self._image


class GerberFile:
"""Generic representation of Gerber file.
Expand All @@ -39,6 +195,7 @@ def __init__(self, source_code: str, file_type: FileTypeEnum) -> None:
self._cached_ast: Optional[File] = None
self._cached_rvmc: Optional[RVMC] = None
self._cached_final_state: Optional[State] = None
self._color_map = DEFAULT_ALPHA_COLOR_MAP

def _flush_cached(self) -> None:
self._cached_ast = None
Expand Down Expand Up @@ -97,6 +254,11 @@ def set_compiler_options(self, **options: Any) -> Self:
self._compiler_options = options
return self

def set_color_map(self, color_map: COLOR_MAP_T) -> Self:
"""Set color map for this Gerber file."""
self._color_map = color_map
return self

def _get_final_state(self) -> State:
if self._cached_final_state is None:
self._cached_final_state = get_final_state(self._get_ast())
Expand All @@ -123,7 +285,7 @@ def render_with_pillow(
self,
style: Optional[Style] = None,
dpmm: int = 20,
) -> PIL.Image.Image:
) -> PillowImage:
"""Render Gerber file to raster image.
Parameters
Expand All @@ -137,11 +299,11 @@ def render_with_pillow(
Resolution of image in dots per millimeter, by default 20
"""
if self.file_type == FileTypeEnum.INFER_FROM_ATTRIBUTES:
if self.file_type in (FileTypeEnum.INFER_FROM_ATTRIBUTES, FileTypeEnum.INFER):
style = self._get_style_from_file_function()

if style is None:
style = DEFAULT_COLOR_MAP[FileTypeEnum.UNDEFINED]
style = self._color_map[self.file_type]

rvmc = self._get_rvmc()
result = render(
Expand All @@ -150,7 +312,14 @@ def render_with_pillow(
dpmm=dpmm,
)
assert isinstance(result, PillowResult)
return result.get_image(style=style)
return PillowImage(
image_space=ImageSpace(
units=self._get_final_state().unit_mode,
box=result.main_box,
dpmm=dpmm,
),
image=result.get_image(style=style),
)

def _get_style_from_file_function(self) -> Style:
file_function_node = self._get_final_state().attributes.file_attributes.get(
Expand All @@ -165,23 +334,4 @@ def _get_style_from_file_function(self) -> Style:
file_function_node.file_function.value
)

return DEFAULT_COLOR_MAP[self.file_type]


@dataclass
class GerberFileInfo:
"""Container for information about Gerber file."""

min_x_mm: float
"""Minimum X coordinate in file in millimeters."""
min_y_mm: float
"""Minimum Y coordinate in file in millimeters."""
max_x_mm: float
"""Maximum X coordinate in file in millimeters."""
max_y_mm: float
"""Maximum T coordinate in file in millimeters."""

width_mm: float
"""Width of image in millimeters."""
height_mm: float
"""Height of image in millimeters."""
return self._color_map[self.file_type]
66 changes: 60 additions & 6 deletions src/pygerber/gerberx3/api/_project.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Iterable
from typing import Iterable, Sequence

from pygerber.gerberx3.api._gerber_file import GerberFile
from PIL import Image

if TYPE_CHECKING:
import PIL.Image
from pygerber.gerberx3.api._gerber_file import GerberFile, PillowImage


class CompositeImage:
"""Image composed of multiple sub-images."""

def __init__(self, sub_images: list[PillowImage], image: Image.Image) -> None:
self._sub_images = sub_images
self._image = image

def get_sub_images(self) -> Sequence[PillowImage]:
"""Get sequence containing sub-images."""
return self._sub_images

def get_image(self) -> Image.Image:
"""Get image composed out of sub-images."""
return self._image


class Project:
Expand All @@ -20,6 +35,45 @@ class Project:
def __init__(self, files: Iterable[GerberFile]) -> None:
self.files = list(files)

def render_with_pillow(self) -> PIL.Image.Image:
def render_with_pillow(
self,
dpmm: int = 20,
) -> CompositeImage:
"""Render project to raster image using Pillow."""
raise NotImplementedError
sub_images: list[PillowImage] = [
file.render_with_pillow(dpmm=dpmm) for file in self.files
]

max_x_image = max(sub_images, key=lambda x: x.get_image_space().max_x)
max_y_image = max(sub_images, key=lambda x: x.get_image_space().max_y)
min_x_image = min(sub_images, key=lambda x: x.get_image_space().min_x)
min_y_image = min(sub_images, key=lambda x: x.get_image_space().min_y)

width_pixels = (
max_x_image.get_image_space().max_x_pixels
- min_x_image.get_image_space().min_x_pixels
)
height_pixels = (
max_y_image.get_image_space().max_y_pixels
- min_y_image.get_image_space().min_y_pixels
)

image = Image.new("RGBA", (width_pixels, height_pixels))

for sub_image in sub_images:
image.paste(
sub_image.get_image(),
(
abs(
min_x_image.get_image_space().min_x_pixels
- sub_image.get_image_space().min_x_pixels
),
abs(
max_y_image.get_image_space().max_y_pixels
- sub_image.get_image_space().max_y_pixels
),
),
mask=sub_image.get_image().getchannel("A"),
)

return CompositeImage(sub_images, image)
Loading

0 comments on commit 5198825

Please sign in to comment.