Skip to content

Commit

Permalink
Add render_with_shapely to GerberFile with docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Oct 13, 2024
1 parent 0c1efad commit 1975b81
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build_n_deploy_docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/70_gerber/20_quick_start/00_introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
87 changes: 71 additions & 16 deletions docs/70_gerber/20_quick_start/01_single_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,33 @@ 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
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
Expand All @@ -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,
Expand All @@ -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") }}

Expand All @@ -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.

<p align="center">
<img src="single_file_pillow.png" alt="render_project" width="400" />
</p>

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)
Expand All @@ -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") }}

<p align="center">
<img src="single_file_shapely.svg" alt="render_project" width="400" />
</p>

!!! 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.<preset-name>`.

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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/70_gerber/20_quick_start/single_file_shapely.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 17 additions & 11 deletions src/pygerber/gerber/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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",
]
116 changes: 99 additions & 17 deletions src/pygerber/gerber/api/_gerber_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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."""
Expand Down
Loading

0 comments on commit 1975b81

Please sign in to comment.