Skip to content

Commit

Permalink
Add rendering tests with reference assets
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Nov 21, 2024
1 parent e9557d5 commit 0cb0ed3
Show file tree
Hide file tree
Showing 9 changed files with 672 additions and 98 deletions.
357 changes: 356 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ scikit-image = [
scipy = [
{ version = ">=1.10.0,<1.11.0", python = ">=3.8,<3.9" }, # Pillow dropped support for 3.8 in 8.0
{ version = ">=1.11.0,<2.0.0", python = ">=3.9" },
{ version = ">=1.14.1,<2.0.0", python = ">=3.13,<4.0" },
]
svglib = "^1.5.1"
reportlab = {extras = ["pycairo"], version = "^4.2.5"}

[tool.poetry.group.style]
optional = true
Expand Down
56 changes: 47 additions & 9 deletions test/assets/assetlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from attr import dataclass
from filelock import FileLock
from PIL import Image
from pydantic import BaseModel, Field, FilePath, HttpUrl
from pydantic import BaseModel, DirectoryPath, Field, HttpUrl
from pydantic_core import Url

if TYPE_CHECKING:
Expand Down Expand Up @@ -181,17 +181,50 @@ def update(self, content: bytes) -> None:
)


class File(Source):
"""Asset source representing a file."""
class Directory(BaseModel):
"""Asset source representing a file system location."""

file_path: FilePath
directory: DirectoryPath

@classmethod
def new(cls, file_path: Path | str) -> Self:
"""Create a new FilePath instance."""
if isinstance(file_path, str):
file_path = Path(file_path)
return cls(file_path=file_path)
def new(cls, directory: Path | str) -> Self:
"""Create a new Directory instance."""
if isinstance(directory, str):
directory = Path(directory)
return cls(directory=directory.resolve())

def file(self, path: str | Path) -> File:
"""Get a File instance for the directory."""
return File(
directory=self, file_path=Path(path) if isinstance(path, str) else path
)


class File(Source):
"""Asset source representing a file system location."""

directory: Directory
"""Directory information."""

file_path: Path
"""Path to the file within the directory, relative to directory root."""

def load(self) -> bytes:
"""Load the asset from the source."""
src = self.absolute_path
return src.read_bytes()

def update(self, content: bytes) -> None:
"""Update the asset from the source."""
dest = self.absolute_path
dest.parent.mkdir(0o777, parents=True, exist_ok=True)
dest.touch(0o777, exist_ok=True)
dest.write_bytes(content)

@property
def absolute_path(self) -> Path:
"""Get the absolute path to the file."""
return self.directory.directory / self.file_path


SourceT = TypeVar("SourceT", bound=Source)
Expand All @@ -217,6 +250,11 @@ def update(self, content: str) -> None:
"""Update the asset from the source."""
self.src.update(content.encode(encoding=self.encoding))

@classmethod
def new(cls, src: SourceT) -> TextAsset[SourceT]:
"""Create a new TextAsset instance."""
return cls(src=src)


class ImageFormat(Enum):
"""Enumeration of image formats."""
Expand Down
152 changes: 103 additions & 49 deletions test/assets/gerberx3/A64_OLinuXino_rev_G/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,111 @@

from pathlib import Path

from pygerber.common.namespace import Namespace
from test.assets.asset import ExcellonAsset, GerberX3Asset
from test.assets.assetlib import (
Directory,
File,
GitFile,
ImageAsset,
ImageFormat,
TextAsset,
)
from test.assets.reference import REFERENCE_REPOSITORY

THIS_FILE = Path(__file__)
THIS_DIRECTORY = THIS_FILE.parent

THIS_DIRECTORY_SRC = Directory.new(THIS_DIRECTORY)
NAMESPACE = "gerber/A64_OLinuXino_rev_G"

class A64_OlinuXino_Rev_G(Namespace): # noqa: N801
A64_OlinuXino_Rev_G_B_Cu = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_Cu.gbr"
)
A64_OlinuXino_Rev_G_B_Mask = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_Mask.gbr"
)
A64_OlinuXino_Rev_G_B_Paste = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_Paste.gbr"
)
A64_OlinuXino_Rev_G_B_SilkS = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_SilkS.gbr"
)
A64_OlinuXino_Rev_G_Edge_Cuts = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-Edge_Cuts.gbr"
)
A64_OlinuXino_Rev_G_F_Cu = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_Cu.gbr"
)
A64_OlinuXino_Rev_G_F_Mask = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_Mask.gbr"
)
A64_OlinuXino_Rev_G_F_Paste = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_Paste.gbr"
)
A64_OlinuXino_Rev_G_F_SilkS = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_SilkS.gbr"
)
A64_OlinuXino_Rev_G_In1_Cu = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In1_Cu.gbr"
)
A64_OlinuXino_Rev_G_In2_Cu = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In2_Cu.gbr"
)
A64_OlinuXino_Rev_G_In3_Cu = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In3_Cu.gbr"
)
A64_OlinuXino_Rev_G_In4_Cu = GerberX3Asset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In4_Cu.gbr"
)
A64_OlinuXino_Rev_G_NPTH = ExcellonAsset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-NPTH.drl"
)
A64_OlinuXino_Rev_G_PTH = ExcellonAsset(
THIS_DIRECTORY / "A64-OlinuXino_Rev_G-PTH.drl"
)
gbrjob = GerberX3Asset(THIS_DIRECTORY / "A64-OlinuXino_Rev_G-job.gbrjob")

A64_OlinuXino_Rev_G_B_Cu = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_Cu.gbr")
)
A64_OlinuXino_Rev_G_B_Cu_Formatted = TextAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Cu.formatted.gbr")
)
A64_OlinuXino_Rev_G_B_Cu_png = ImageAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Cu.png"),
ImageFormat.PNG,
)
A64_OlinuXino_Rev_G_B_Cu_png_shapely = ImageAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Cu.shapely.png"),
ImageFormat.PNG,
)

A64_OlinuXino_Rev_G_B_Mask = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_Mask.gbr")
)
A64_OlinuXino_Rev_G_B_Mask_Formatted = TextAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Mask.formatted.gbr")
)
A64_OlinuXino_Rev_G_B_Mask_png = ImageAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Mask.png"),
ImageFormat.PNG,
)


A64_OlinuXino_Rev_G_B_Paste = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_Paste.gbr")
)

A64_OlinuXino_Rev_G_B_SilkS = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_SilkS.gbr")
)

A64_OlinuXino_Rev_G_Edge_Cuts = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-Edge_Cuts.gbr")
)

A64_OlinuXino_Rev_G_F_Cu = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_Cu.gbr")
)
A64_OlinuXino_Rev_G_F_Cu_Formatted = TextAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-F_Cu.formatted.gbr")
)
A64_OlinuXino_Rev_G_F_Cu_png = ImageAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-F_Cu.png"),
ImageFormat.PNG,
)
A64_OlinuXino_Rev_G_F_Cu_png_shapely = ImageAsset[GitFile].new(
REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-F_Cu.shapely.png"),
ImageFormat.PNG,
)

A64_OlinuXino_Rev_G_F_Mask = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_Mask.gbr")
)

A64_OlinuXino_Rev_G_F_Paste = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_Paste.gbr")
)

A64_OlinuXino_Rev_G_F_SilkS = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_SilkS.gbr")
)

A64_OlinuXino_Rev_G_In1_Cu = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In1_Cu.gbr")
)

A64_OlinuXino_Rev_G_In2_Cu = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In2_Cu.gbr")
)

A64_OlinuXino_Rev_G_In3_Cu = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In3_Cu.gbr")
)

A64_OlinuXino_Rev_G_In4_Cu = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In4_Cu.gbr")
)

A64_OlinuXino_Rev_G_NPTH = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-NPTH.drl")
)

A64_OlinuXino_Rev_G_PTH = TextAsset[File].new(
THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-PTH.drl")
)

gbrjob = TextAsset[File].new(THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-job.gbrjob"))
2 changes: 1 addition & 1 deletion test/benchmark/a64_olinuxino_rev_g_bottom_copper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from pathlib import Path
from typing import cast

import test.assets.gerberx3.A64_OLinuXino_rev_G as A64_OlinuXino_Rev_G
from pygerber.gerber.compiler import compile
from pygerber.gerber.parser import parse
from pygerber.vm import render
from pygerber.vm.pillow.vm import PillowResult
from test.assets.gerberx3.A64_OLinuXino_rev_G import A64_OlinuXino_Rev_G

THIS_FILE = Path(__file__)
THIS_DIRECTORY = THIS_FILE.parent
Expand Down
87 changes: 70 additions & 17 deletions test/e2e/test_gerber/test_pillow.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
from __future__ import annotations

import inspect
from io import BytesIO
from pathlib import Path
from typing import ClassVar, Type
from typing import ClassVar, Literal, Type

import pytest
from PIL import Image

import test.assets.gerberx3.A64_OLinuXino_rev_G as A64_OlinuXino_Rev_G
from pygerber.gerber.ast.nodes import File
from pygerber.gerber.compiler import Compiler
from pygerber.gerber.compiler import compile
from pygerber.gerber.parser import parse
from pygerber.gerber.parser.pyparsing.parser import Parser
from pygerber.vm import render
from pygerber.vm.pillow.vm import PillowResult, PillowVirtualMachine
from test.assets.asset import GerberX3Asset
from test.assets.assetlib import ImageAnalyzer, ImageAsset, TextAsset
from test.assets.generated.macro import (
get_custom_circle_local_2_0,
get_custom_circle_local_2_0_ring_rot_30,
get_custom_circle_local_2_0_rot_30,
)
from test.assets.gerberx3.A64_OLinuXino_rev_G import A64_OlinuXino_Rev_G
from test.assets.gerberx3.arc.clockwise import ClockwiseArcAssets
from test.assets.gerberx3.arc.counterclockwise import CounterClockwiseArcAssets
from test.assets.gerberx3.ATMEGA328 import ATMEGA328Assets
Expand All @@ -41,7 +48,7 @@ def _render(self, source: GerberX3Asset, dpmm: int = 10) -> PillowResult:
return self._render_ast(ast, dpmm=dpmm)

def _render_ast(self, ast: File, dpmm: int = 10) -> PillowResult:
rvmc = Compiler().compile(ast)
rvmc = compile(ast)
return PillowVirtualMachine(dpmm=dpmm).run(rvmc)

def _save(self, result: PillowResult) -> None:
Expand All @@ -58,21 +65,67 @@ def _save(self, result: PillowResult) -> None:
result.get_image_no_style().save(dump_directory / f"{caller_function_name}.png")


class TestOLinuXinoRevG(PillowRenderE2E):
@tag(Tag.PILLOW)
def test_bottom_copper(self) -> None:
result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu, dpmm=100)
self._save(result)
class PillowRender:
parser: Literal["pyparsing"] = "pyparsing"
dpmm: int = 20

@tag(Tag.PILLOW)
def test_bottom_mask(self) -> None:
result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Mask, dpmm=100)
self._save(result)
def create_image(self, source: str) -> Image.Image:
ast = parse(source, parser=self.parser)
rvmc = compile(ast)
result = render(rvmc, dpmm=self.dpmm)

@tag(Tag.PILLOW)
def test_bottom_paste(self) -> None:
result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Paste, dpmm=100)
self._save(result)
buffer = BytesIO()
result.save(buffer, file_format="PNG")

buffer.seek(0)
return Image.open(buffer, formats=["png"])

def compare_with_reference(
self, reference: ImageAsset, ssim_threshold: float, image: Image.Image
) -> None:
ia = ImageAnalyzer(reference.load())
ia.assert_same_size(image)
(
ia.histogram_compare_color(image)
.assert_channel_count(4)
.assert_greater_or_equal_values(0.99)
)
assert ia.structural_similarity(image) > ssim_threshold


class TestOLinuXinoRevG(PillowRender):
dpmm: int = 40

@tag(Tag.PILLOW, Tag.OPENCV, Tag.SKIMAGE)
@pytest.mark.parametrize(
("asset", "reference", "ssim_threshold"),
[
(
A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu,
A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu_png,
0.99,
),
(
A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_F_Cu,
A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_F_Cu_png,
0.99,
),
],
ids=["B_Cu", "F_Cu"],
)
def test_render_pillow(
self,
asset: TextAsset,
reference: ImageAsset,
ssim_threshold: float,
*,
is_regeneration_enabled: bool,
) -> None:
image = self.create_image(asset.load())
if is_regeneration_enabled:
reference.update(image)
else:
self.compare_with_reference(reference, ssim_threshold, image)


class TestFcPolyTest(PillowRenderE2E):
Expand Down
Loading

0 comments on commit 0cb0ed3

Please sign in to comment.