diff --git a/.github/workflows/build_n_deploy_docs.yaml b/.github/workflows/build_n_deploy_docs.yaml
index d09c60b6..3c6c98b3 100644
--- a/.github/workflows/build_n_deploy_docs.yaml
+++ b/.github/workflows/build_n_deploy_docs.yaml
@@ -38,7 +38,7 @@ jobs:
run: pip install poetry==1.6.1
- name: Install dependencies
- run: poetry install --with=docs --no-cache --sync
+ run: poetry install --with=docs --no-cache --sync --extras=all
- name: Configure Git
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfee770d..b5137c6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,15 +7,26 @@ this project adheres to [Semantic Versioning 2.0.0](https://semver.org/).
## Pre-Release 3.0.0a3
-- Restored `pygerber_language_server` command.
+- Removed legacy error types from `pygerber.gerber.api._errors`.
+- Removed `pygerber.common.general_model` module.
+- Removed `pygerber.common.immutable_map_model` module.
+- Removed `pygerber.common.rgba` module.
- Rename `Project` class from `pygerber.gerber.api` to `CompositeView`.
- Changed `source_code` and `file_type` attributes of `GerberFile` to be read-only.
- Changed return type of `CompositeView.render_with_pillow` to `CompositePillowImage`.
Interface of `CompositePillowImage` is the same as previously `CompositeView`.
+- Changed miniatures displayed by language server to be fixed size due to repeating
+ problems with apertures being too small or too large.
- Added custom `__str__` to `CompositeView` and `GerberFile` classes.
- Added `GerberJobFile` class for handling `.gbrjob` files.
- Added `Project` class for grouping multiple `CompositeView` objects.
- Added documentation for `GerberJobFile` and `Project` classes.
+- Added `pygerber.vm.shapely` package containing implementation of Gerber vm (renderer)
+ using shapely library.
+- Added `render_with_shapely` to `GerberFile` class.
+- Updated `Quick start` guide.
+- Updated many of docstrings in `pygerber.gerber.api` package.
+- Restored `pygerber_language_server` command.
## Pre-Release 3.0.0a2
diff --git a/docs/70_gerber/20_quick_start/00_introduction.md b/docs/70_gerber/20_quick_start/00_introduction.md
index 086d5d0f..adf71eeb 100644
--- a/docs/70_gerber/20_quick_start/00_introduction.md
+++ b/docs/70_gerber/20_quick_start/00_introduction.md
@@ -25,7 +25,7 @@ For guide on how to use `GerberFile` class, check out
[Single file guide](./01_single_file.md).
For guide on how to use `Project` class, check out
-[Multi file project guide](./02_multi_file_project.md).
+[Multi file project guide](./02_multi_file.md).
Most of code examples (those with file name at the top of code frame) can be directly
copied and pasted into Python file, interactive shell or Jupyter notebook and executed.
diff --git a/docs/70_gerber/20_quick_start/01_single_file.md b/docs/70_gerber/20_quick_start/01_single_file.md
index 6b78e1fd..5b3dae95 100644
--- a/docs/70_gerber/20_quick_start/01_single_file.md
+++ b/docs/70_gerber/20_quick_start/01_single_file.md
@@ -25,6 +25,17 @@ file location.
{{ include_code("test/examples/gerberx3/api/_62_quick_start_from_buffer.quickstart.py", "docspygerberlexer", title="example_from_buffer.py", linenums="1", hl_lines="5") }}
+Each of the factory methods listed above accept optional `file_type`
+([`FileTypeEnum`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.FileTypeEnum)
+)parameter which can be used to explicitly set file type (e.g. copper, silkscreen). If
+this parameter is not set, by default PyGerber will try to guess file type based on file
+extension and/or
+[file attributes](https://www.ucamco.com/files/downloads/file_en/456/gerber-layer-format-specification-revision-2022-02_en.pdf#page=125).
+
+!!! info
+
+ File type affects color of rendered image if color scheme is not explicitly set.
+
## Configuring `GerberFile` object
Once you have `GerberFile` object created, you can use PyGerber features exposed as
@@ -32,6 +43,15 @@ methods on this object. `GerberFile` allows you to customize behavior of some of
underlying implementation parts. Those methods mutate `GerberFile` object and
consecutive calls to those methods override previous configuration in its **entirety**.
+[`set_color_map()`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.GerberFile.set_color_map)
+can be used to override default color map.
+
+Color map is used to map file type to predefined color style. PyGerber provides simple
+color schema but it is useful mostly for final renders as colors used were chosen to
+resemble final look of average PCB. Therefore you can easily provide your own color map.
+
+Check out [Custom color maps](./10_custom_color_maps.md) for more details.
+
[`set_parser_options()`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.GerberFile.set_parser_options)
allows you to modify advanced parser settings. It is available to allow tweaking
predefined parser behavior options. If you need more control than provided here, please
@@ -40,8 +60,6 @@ intentionally not precisely defined here, as they are different for different pa
implementations, only way to use this method is to already understand what you are
doing.
-`TODO: Add example`
-
[`set_compiler_options()`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.GerberFile.set_compiler_options)
allows you to modify advanced compiler settings. It is available to allow tweaking
predefined compiler behavior options. If you need more control than provided here,
@@ -50,24 +68,16 @@ are intentionally not precisely defined here, as they are different for differen
compiler implementations, only way to use this method is to already understand what you
are doing.
-`TODO: Add example`
-
-[`set_color_map()`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.GerberFile.set_color_map)
-can be used to override default color map.
-
-Color map is used to map file type to predefined color style. PyGerber provides simple
-color schema but it is useful mostly for final renders as colors used were chosen to
-resemble final look of average PCB. Therefore you can easily provide your own color map.
-
-`TODO: Add example`
-
-Check out [Custom color maps](./10_custom_color_maps.md) for more details.
-
## Rendering Gerber file
+### Raster images
+
`GerberFile` object exposes
[`render_with_pillow()`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.GerberFile.render_with_pillow)
-method which renders Gerber file into Pillow image object.
+method which uses Gerber file renderer implemented with
+[Pillow](https://pillow.readthedocs.io/en/stable) (Python Imaging Library (PIL) fork) to
+generate raster images. Those images can be saved afterwards in PNG, JPEG etc. image
+formats.
{{ include_code("test/examples/gerberx3/api/_00_single_file_render_with_pillow_defaults_str.example.py", "docspygerberlexer", title="render_with_pillow.py", linenums="1") }}
@@ -78,6 +88,10 @@ value is set to 20, which is a safe default, but quite low for small PCBs.
`render_with_pillow()` returns `PillowImage` object which wraps actual image
(`PIL.Image.Image` object) and additional information about image coordinate space.
+
+
+
+
To retrieve image object, you can use `get_image()` method. Afterwards you can save it
with
[`save()`](https://pillow.readthedocs.io/en/stable/reference/Image.md#PIL.Image.Image.save)
@@ -93,6 +107,47 @@ image size, etc, as presented below:
{{ run_capture_stdout("python test/examples/gerberx3/api/_50_show_image_info.singlefile.py", "python show_image_space.py") }}
+### Vector images
+
+`GerberFile` object exposes
+[`render_with_shapely()`](../../reference/pygerber/gerber/api/__init__.md#pygerber.gerber.api.GerberFile.render_with_shapely)
+method which uses Gerber file renderer implemented with
+[shapely](https://shapely.readthedocs.io/en/stable/manual.html) library to generate
+vector images. Those images can be saved afterwards in SVG format.
+
+`render_with_shapely()` returns `ShapelyImage` object which wraps output geometry and
+additional information about image coordinate space. You can save the image in SVG
+format using `save()` method
+
+{{ include_code("test/examples/gerberx3/api/_05_single_file_render_with_shapely_defaults_file.example_svg.py", "docspygerberlexer", title="example.py", linenums="1") }}
+
+
+
+
+
+!!! success "Improvements in PyGerber 3.0.0"
+
+ In comparison to SVG rendering engine present in PyGerber 2.4.x,
+ [shapely](https://shapely.readthedocs.io/en/stable/manual.html) based
+ implementation performs actual boolean operations on geometry, therefore resulting in
+ cleaner geometry which can be used to create 3D models, for example in blender.
+
+### Image colors
+
+There are three ways to change color of image created with `render_with_*()` methods.
+
+First way is to explicitly pass `Style` instance as `style` parameter to
+`render_with_*()` method. Style class offers predefined styles which can be accessed by
+`Style.presets.`.
+
+Second way is to change Gerber file type. File type can be deduced automatically but
+also can be explicitly set in `GerberFile` factory method (`from_str`, `from_file`
+etc.). File type will be mapped to specific `Style` based on color map.
+
+Third way is to change what color map is used to convert file types to styles. This can
+be achieved with `set_color_map()`, but for more details on how to specify custom color
+maps see [Custom color maps](./10_custom_color_maps.md).
+
## Formatting Gerber file
`GerberFile` object exposes
diff --git a/docs/70_gerber/20_quick_start/single_file_pillow.png b/docs/70_gerber/20_quick_start/single_file_pillow.png
new file mode 100644
index 00000000..5fb0d6b3
Binary files /dev/null and b/docs/70_gerber/20_quick_start/single_file_pillow.png differ
diff --git a/docs/70_gerber/20_quick_start/single_file_shapely.svg b/docs/70_gerber/20_quick_start/single_file_shapely.svg
new file mode 100644
index 00000000..38b05ae1
--- /dev/null
+++ b/docs/70_gerber/20_quick_start/single_file_shapely.svg
@@ -0,0 +1 @@
+
diff --git a/src/pygerber/gerber/api/__init__.py b/src/pygerber/gerber/api/__init__.py
index 19b8e496..6e2f01a7 100644
--- a/src/pygerber/gerber/api/__init__.py
+++ b/src/pygerber/gerber/api/__init__.py
@@ -14,11 +14,13 @@
DEFAULT_COLOR_MAP,
FileTypeEnum,
)
+from pygerber.gerber.api._errors import PathToGerberJobProjectNotDefinedError
from pygerber.gerber.api._gerber_file import (
GerberFile,
Image,
ImageSpace,
PillowImage,
+ ShapelyImage,
Units,
)
from pygerber.gerber.api._gerber_job_file import (
@@ -33,27 +35,31 @@
Size,
)
from pygerber.gerber.formatter.options import Options
+from pygerber.vm.types.style import Style
__all__ = [
- "FileTypeEnum",
- "GerberFile",
- "CompositeView",
- "CompositePillowImage",
- "Units",
- "ImageSpace",
- "Image",
- "PillowImage",
"CompositeImage",
- "DEFAULT_COLOR_MAP",
+ "CompositePillowImage",
+ "CompositeView",
"DEFAULT_ALPHA_COLOR_MAP",
- "Options",
- "GerberJobFile",
+ "DEFAULT_COLOR_MAP",
"DesignRules",
"FilesAttributes",
+ "FileTypeEnum",
"GeneralSpecs",
"GenerationSoftware",
+ "GerberFile",
+ "GerberJobFile",
"Header",
+ "Image",
+ "ImageSpace",
"MaterialStackup",
+ "PathToGerberJobProjectNotDefinedError",
+ "PillowImage",
"ProjectId",
+ "ShapelyImage",
"Size",
+ "Style",
+ "Units",
+ "Options",
]
diff --git a/src/pygerber/gerber/api/_gerber_file.py b/src/pygerber/gerber/api/_gerber_file.py
index 1893a639..7f88d401 100644
--- a/src/pygerber/gerber/api/_gerber_file.py
+++ b/src/pygerber/gerber/api/_gerber_file.py
@@ -21,6 +21,7 @@
from pygerber.gerber.parser import parse
from pygerber.vm import render
from pygerber.vm.pillow.vm import PillowResult
+from pygerber.vm.shapely.vm import ShapelyResult
from pygerber.vm.types.box import Box
from pygerber.vm.types.style import Style
@@ -194,6 +195,31 @@ def get_image(self) -> PIL.Image.Image:
return self._image
+class ShapelyImage(Image):
+ """The `ShapelyImage` class is a rendered image returned by
+ `GerberFile.render_with_shapely` method.
+ """
+
+ def __init__(
+ self, image_space: ImageSpace, result: ShapelyResult, style: Style
+ ) -> None:
+ super().__init__(image_space=image_space)
+ self._result = result
+ self._style = style
+
+ def save(self, destination: str | Path | TextIO) -> None:
+ """Write rendered image as SVG to location or buffer.
+
+ Parameters
+ ----------
+ destination : str | Path
+ `str` and `Path` objects are interpreted as file paths and opened with
+ truncation. `TextIO`-like (files, StringIO) objects are written to directly.
+
+ """
+ self._result.save_svg(destination, self._style)
+
+
class GerberFile:
"""Generic representation of Gerber file.
@@ -405,19 +431,14 @@ def render_with_pillow(
Parameters
----------
style : Style, optional
- Style (color scheme) of rendered image, if value is None, style will be
- inferred from file_type if it possible to determine file_type
- (for FileTypeEnum.INFER*) or specific file_type was specified in
- constructor, by default None
+ Style (color scheme) of rendered image, if value is None, `file_type` will
+ be used. `file_type` was one of `FileTypeEnum.INFER*` attempt will be done
+ to guess `file_type` based on extension and/or attributes, by default None
dpmm : int, optional
Resolution of image in dots per millimeter, by default 20
"""
- if self._file_type in (FileTypeEnum.INFER_FROM_ATTRIBUTES, FileTypeEnum.INFER):
- style = self._get_style_from_file_function()
-
- if style is None:
- style = self._color_map[self._file_type]
+ style = self._dispatch_style(style)
rvmc = self._get_rvmc()
result = render(
@@ -435,20 +456,81 @@ def render_with_pillow(
image=result.get_image(style=style),
)
- def _get_style_from_file_function(self) -> Style:
+ def _dispatch_style(self, style: Optional[Style]) -> Style:
+ if style is not None:
+ return style
+
+ if self._file_type in (FileTypeEnum.INFER_FROM_EXTENSION, FileTypeEnum.INFER):
+ self._file_type = self._get_file_type_from_extension()
+
+ if self._file_type in (FileTypeEnum.INFER_FROM_ATTRIBUTES, FileTypeEnum.INFER):
+ self._file_type = self._get_file_type_from_attributes()
+
+ return self._color_map[self._file_type]
+
+ def _get_file_type_from_extension(self) -> FileTypeEnum:
+ if not isinstance(self._source_type_or_path, Path):
+ if self._file_type == FileTypeEnum.INFER_FROM_EXTENSION:
+ return FileTypeEnum.UNDEFINED
+
+ if self._file_type == FileTypeEnum.INFER:
+ return FileTypeEnum.INFER_FROM_ATTRIBUTES
+
+ raise NotImplementedError(self._file_type)
+
+ file_type = FileTypeEnum.infer_from_extension(self._source_type_or_path.suffix)
+
+ if file_type == FileTypeEnum.UNDEFINED:
+ if self._file_type == FileTypeEnum.INFER_FROM_EXTENSION:
+ return file_type
+
+ if self._file_type == FileTypeEnum.INFER:
+ return FileTypeEnum.INFER_FROM_ATTRIBUTES
+
+ raise NotImplementedError(self._file_type)
+
+ return file_type
+
+ def _get_file_type_from_attributes(self) -> FileTypeEnum:
file_function_node = self._get_final_state().attributes.file_attributes.get(
".FileFunction"
)
if file_function_node is None:
- self._file_type = FileTypeEnum.UNDEFINED
+ return FileTypeEnum.UNDEFINED
- else:
- assert isinstance(file_function_node, TF_FileFunction)
- self._file_type = FileTypeEnum.infer_from_attributes(
- file_function_node.file_function.value
- )
+ assert isinstance(file_function_node, TF_FileFunction)
+ return FileTypeEnum.infer_from_attributes(
+ file_function_node.file_function.value
+ )
- return self._color_map[self._file_type]
+ def render_with_shapely(self, style: Optional[Style] = None) -> ShapelyImage:
+ """Render Gerber file to vector image using rendering backend based on Shapely
+ library.
+
+ Parameters
+ ----------
+ style : Style, optional
+ Style (color scheme) of rendered image, if value is None, `file_type` will
+ be used. `file_type` was one of `FileTypeEnum.INFER*` attempt will be done
+ to guess `file_type` based on extension and/or attributes, by default None
+ Only foreground color is used, as all operations with clear polarity
+ are performing actual boolean difference operations on geometry.
+
+ """
+ style = self._dispatch_style(style)
+
+ rvmc = self._get_rvmc()
+ result = render(rvmc, backend="shapely")
+ assert isinstance(result, ShapelyResult)
+ return ShapelyImage(
+ image_space=ImageSpace(
+ units=self._get_final_state().unit_mode,
+ box=result.main_box,
+ dpmm=0,
+ ),
+ result=result,
+ style=style,
+ )
def format(self, output: TextIO, options: Optional[formatter.Options]) -> None:
"""Format Gerber code and write it to `output` stream."""
diff --git a/test/examples/gerberx3/api/_05_single_file_render_with_shapely_defaults_file.example_svg.py b/test/examples/gerberx3/api/_05_single_file_render_with_shapely_defaults_file.example_svg.py
new file mode 100644
index 00000000..e627e749
--- /dev/null
+++ b/test/examples/gerberx3/api/_05_single_file_render_with_shapely_defaults_file.example_svg.py
@@ -0,0 +1,10 @@
+from pygerber.gerber.api import GerberFile, Style
+
+from pygerber.examples import ExamplesEnum, get_example_path
+
+path_to_gerber_file = get_example_path(ExamplesEnum.UCAMCO_2_11_2)
+
+image = GerberFile.from_file(path_to_gerber_file).render_with_shapely(
+ Style.presets.BLACK_WHITE
+)
+image.save("output.svg")
diff --git a/test/examples/gerberx3/api/test_examples.py b/test/examples/gerberx3/api/test_examples.py
index bc609c53..669b966e 100644
--- a/test/examples/gerberx3/api/test_examples.py
+++ b/test/examples/gerberx3/api/test_examples.py
@@ -19,10 +19,23 @@
],
ids=lambda path: path.name,
)
-def test_examples_with_output_image(example_path: Path) -> None:
- with cd_to_tempdir():
- exec(example_path.read_text(encoding="utf-8")) # noqa: S102
- assert (Path.cwd() / "output.png").exists()
+def test_examples_with_output_png_image(example_path: Path) -> None:
+ # with cd_to_tempdir():
+ exec(example_path.read_text(encoding="utf-8")) # noqa: S102
+ assert (Path.cwd() / "output.png").exists()
+
+
+@pytest.mark.parametrize(
+ "example_path",
+ [
+ *THIS_DIRECTORY.glob("*.example_svg.py"),
+ ],
+ ids=lambda path: path.name,
+)
+def test_examples_with_output_svg_image(example_path: Path) -> None:
+ # with cd_to_tempdir():
+ exec(example_path.read_text(encoding="utf-8")) # noqa: S102
+ assert (Path.cwd() / "output.svg").exists()
@pytest.mark.parametrize(