Skip to content

Commit

Permalink
Add merge-convert png command
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Feb 1, 2025
1 parent fb7c0ea commit 6d34c8f
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 26 deletions.
52 changes: 38 additions & 14 deletions src/pygerber/console/gerber.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from pygerber.gerber import formatter
from pygerber.gerber.api import Color, FileTypeEnum, GerberFile, Style
from pygerber.gerber.api._composite_view import CompositeView
from pygerber.vm.shapely.vm import is_shapely_available
from pygerber.vm.types.color import InvalidHexColorLiteralError

Expand Down Expand Up @@ -498,22 +499,45 @@ def format_cmd(
file.format(f, options=options)


@gerber.command("project")
@click.argument("files", nargs=-1)
@click.option(
"-o",
"--output",
type=click.Path(dir_okay=False),
default="output.png",
help="Path to output file.",
)
@click.option("-d", "--dpmm", type=int, default=20, help="Dots per millimeter.")
def _project(files: str, output: str, dpmm: int) -> None:
"""Render multiple Gerber files and merge them layer by layer.
@gerber.group("merge-convert")
def merge_convert() -> None:
"""Convert multiple Gerber images to different image format and merge them into one
image (stack on top of each other).
"""

Layers are merged from first to last, thus last layer will be on top.

@merge_convert.command("png")
@click.argument("sources", nargs=-1)
@_get_output_file_option()
@_get_dpmm_option()
@_get_file_type_option()
@_get_raster_implementation_option("pillow")
def merge_convert_png(
sources: str,
output: str,
file_type: str,
dpmm: int,
implementation: str,
) -> None:
"""Convert multiple Gerber images to PNG image and merge them into one image.
Images are merged from first to last, thus fist layer is bottom most, last layer
is topmost, unobstructed.
SOURCES - paths to files which are supposed to be rendered and merged.
"""
raise NotImplementedError
view = CompositeView(
GerberFile.from_file(source, file_type=FileTypeEnum(file_type.upper()))
for source in sources
)

if implementation.lower() == "pillow":
result = view.render_with_pillow(dpmm)
result.save_png(output)

else:
msg = f"Implementation {implementation!r} is not supported."
raise NotImplementedError(msg)


@gerber.command("lint")
Expand Down
1 change: 0 additions & 1 deletion src/pygerber/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class ExamplesEnum(Enum):

carte_test_B_Cu = "carte_test-B_Cu.gbr" # noqa: N815
carte_test_B_Mask = "carte_test-B_Mask.gbr" # noqa: N815
carte_test_B_Paste = "carte_test-B_Paste.gbr" # noqa: N815
carte_test_B_Silkscreen = "carte_test-B_Silkscreen.gbr" # noqa: N815

carte_test_Edges = "carte_test-Edge_Cuts.gbr" # noqa: N815
Expand Down
4 changes: 2 additions & 2 deletions src/pygerber/gerber/api/_composite_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from PIL import Image

from pygerber.gerber.api._enums import COLOR_MAP_T
from pygerber.gerber.api._gerber_file import GerberFile, PillowImage
from pygerber.gerber.api._gerber_file import GerberFile, PillowImage, _PillowSaveMixin

if TYPE_CHECKING:
from typing_extensions import Self
Expand All @@ -15,7 +15,7 @@ class CompositeImage:
"""Image composed of multiple sub-images."""


class CompositePillowImage(CompositeImage):
class CompositePillowImage(CompositeImage, _PillowSaveMixin):
"""Image composed of multiple sub-images."""

def __init__(self, sub_images: list[PillowImage], image: Image.Image) -> None:
Expand Down
24 changes: 16 additions & 8 deletions src/pygerber/gerber/api/_gerber_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,8 @@ def get_image_space(self) -> ImageSpace:
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
class _PillowSaveMixin:
"""Mixin providing common methods for saving Pillow images."""

def save(
self,
Expand Down Expand Up @@ -332,6 +326,20 @@ def save_webp(
"""
self.get_image().convert("RGB").save(fp=destination, format="WEBP", **kwargs)

def get_image(self) -> PIL.Image.Image:
"""Get image object."""
raise NotImplementedError


class PillowImage(Image, _PillowSaveMixin):
"""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
Expand Down
5 changes: 5 additions & 0 deletions test/assets/reference/pygerber/console/gerber.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@
"reference/pygerber/console/test_gerber_format.formatted.gbr"
)
)

MERGE_CONVERT_PNG_REFERENCE_IMAGE = ImageAsset[GitFile].new(
REFERENCE_REPOSITORY.file("reference/pygerber/console/merge_convert_png.png"),
ImageFormat.PNG,
)
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
from click.testing import CliRunner
from PIL import Image

from pygerber.console.gerber import bmp, format_cmd, jpeg, png, tiff, webp
from pygerber.console.gerber import (
bmp,
format_cmd,
jpeg,
merge_convert_png,
png,
tiff,
webp,
)
from pygerber.examples import ExamplesEnum, get_example_path
from pygerber.vm.shapely.vm import is_shapely_available
from test.assets.assetlib import ImageAnalyzer, SvgImageAsset
Expand All @@ -19,6 +27,7 @@
CONVERT_WEBP_LOSSLESS_REFERENCE_IMAGE,
CONVERT_WEBP_LOSSY_REFERENCE_IMAGE,
FORMAT_CMD_REFERENCE_CONTENT,
MERGE_CONVERT_PNG_REFERENCE_IMAGE,
)
from test.conftest import cd_to_tempdir
from test.tags import Tag, tag
Expand Down Expand Up @@ -313,3 +322,40 @@ def test_gerber_format_cmd(*, is_regeneration_enabled: bool) -> None:
pytest.skip("Reference updated")
else:
assert FORMAT_CMD_REFERENCE_CONTENT.load() == file_path.read_text()


MIN_MERGE_PNG_SSIM = 0.99


@tag(Tag.PILLOW, Tag.OPENCV, Tag.SKIMAGE)
def test_gerber_merge_convert_png(*, is_regeneration_enabled: bool) -> None:
runner = CliRunner()
with cd_to_tempdir() as temp_path:
result = runner.invoke(
merge_convert_png,
[
get_example_path(ExamplesEnum.carte_test_B_Cu).as_posix(),
get_example_path(ExamplesEnum.carte_test_B_Mask).as_posix(),
get_example_path(ExamplesEnum.carte_test_B_Silkscreen).as_posix(),
"-o",
"output.png",
"-d",
"20",
],
)
logging.debug(result.output)
assert result.exit_code == 0
assert (temp_path / "output.png").exists()

image = Image.open(temp_path / "output.png")
if is_regeneration_enabled:
MERGE_CONVERT_PNG_REFERENCE_IMAGE.update(image) # pragma: no cover
else:
ia = ImageAnalyzer(MERGE_CONVERT_PNG_REFERENCE_IMAGE.load())
assert ia.structural_similarity(image) > MIN_MERGE_PNG_SSIM
ia.assert_same_size(image)
(
ia.histogram_compare_color(image)
.assert_channel_count(4)
.assert_greater_or_equal_values(0.99)
)
File renamed without changes.

0 comments on commit 6d34c8f

Please sign in to comment.