Skip to content

Commit

Permalink
Add Gerber to PNG and JPEG command line
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Nov 20, 2024
1 parent 6a9d420 commit e829ef3
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 426 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ lint.ignore = [
"COM812", # Checks for implicitly concatenated strings on a single line. Conflicts with ruff format.
"S101", # Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
"EXE002", # Breaks on file systems which do not support executable permissions.
"EM101", # Stupid errors when exceptions are parametrized by short strings.
]
show-fixes = true
target-version = "py38"
Expand Down
116 changes: 2 additions & 114 deletions src/pygerber/console/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import click

import pygerber
from pygerber.gerber.api import FileTypeEnum
from pygerber.console.gerber import gerber


@click.group("pygerber")
Expand All @@ -26,116 +26,4 @@ def _is_language_server_available() -> None:
click.echo("Language server is not available.")


@main.group("render")
def _render() -> None:
"""Render Gerber file with API V2."""


@_render.command("raster")
@click.argument(
"source",
type=click.Path(file_okay=True, dir_okay=False, exists=True, readable=True),
)
@click.option(
"-t",
"--file-type",
"file_type",
type=click.Choice(list(FileTypeEnum.__members__.keys()), case_sensitive=False),
default=FileTypeEnum.INFER.name,
help="Type of the Gerber file. Affects what colors are used for rendering. If not "
"specified, file type will be inferred from extension or .FileFunction attribute.",
)
@click.option(
"-o",
"--output",
type=click.Path(dir_okay=False),
default="output.png",
help="Path to output file.",
)
@click.option(
"-i",
"--image-format",
type=click.Choice(["JPEG", "PNG", "AUTO"], case_sensitive=False),
default="AUTO",
)
@click.option(
"-p",
"--pixel-format",
type=click.Choice(["RGBA", "RGB"], case_sensitive=False),
default="RGB",
)
@click.option("-d", "--dpmm", type=int, default=20, help="Dots per millimeter.")
@click.option(
"-q",
"--quality",
type=int,
default=95,
help="Compression algorithm quality control parameter.",
)
def _raster(
source: str,
file_type: str,
output: str,
pixel_format: str,
image_format: str,
dpmm: int,
quality: int,
) -> None:
"""Render Gerber file with API V2 as raster (PNG/JPEG) image."""
raise NotImplementedError


@_render.command("vector")
@click.argument(
"source",
type=click.Path(file_okay=True, dir_okay=False, exists=True, readable=True),
)
@click.option(
"-t",
"--file-type",
"file_type",
type=click.Choice(list(FileTypeEnum.__members__.keys()), case_sensitive=False),
default=FileTypeEnum.INFER.name,
help="Type of the Gerber file. Affects what colors are used for rendering. If not "
"specified, file type will be inferred from extension or .FileFunction attribute.",
)
@click.option(
"-o",
"--output",
type=click.Path(dir_okay=False),
default="output.svg",
help="Path to output file.",
)
@click.option(
"-s",
"--scale",
type=float,
default=1.0,
help="Scaling factor applied to the output image.",
)
def _vector(
source: str,
file_type: str,
output: str,
scale: float,
) -> None:
"""Render Gerber file with API V2 as vector (SVG) image."""
raise NotImplementedError


@_render.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.
Layers are merged from first to last, thus last layer will be on top.
"""
raise NotImplementedError
main.add_command(gerber)
245 changes: 245 additions & 0 deletions src/pygerber/console/gerber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
"""Command line commands of PyGerber."""

from __future__ import annotations

from typing import Callable, Optional

import click

from pygerber.gerber.api import Color, FileTypeEnum, GerberFile, Style
from pygerber.vm.types.color import InvalidHexColorLiteralError


@click.group("gerber")
def gerber() -> None:
"""Gerber format related commands."""


@gerber.group("convert")
def convert() -> None:
"""Convert Gerber image to different image format."""


def _get_file_type_option() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.option(
"-t",
"--file-type",
"file_type",
type=click.Choice(list(FileTypeEnum.__members__.keys()), case_sensitive=False),
default=FileTypeEnum.INFER.name,
help=(
"Type (function) of the Gerber file. Affects what colors are used for "
"rendering. If not specified, file type will be inferred from extension "
"or .FileFunction attribute."
),
)


def _get_style_option() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.option(
"-s",
"--style",
type=click.Choice(
[*Style.presets.get_styles(), "CUSTOM"], case_sensitive=False
),
default=None,
help=(
"Direct style override. When specified, this will override style implied "
"by file type. When set to CUSTOM, you have to use -f/--foreground and "
"-b/--background to specify foreground and background colors."
),
)


def _get_foreground_option() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.option(
"-f",
"--foreground",
type=str,
default=None,
help=(
"Foreground color in hex format. Only used when --style is set to CUSTOM. "
"Example: `#FF0000`, `#` is accepted but not mandatory."
),
)


def _get_background_option() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.option(
"-b",
"--background",
type=str,
default=None,
help=(
"Background color in hex format. Only used when --style is set to CUSTOM. "
"Example: `#FF0000`, `#` is accepted but not mandatory."
),
)


def _get_raster_implementation_option() -> (
Callable[[click.decorators.FC], click.decorators.FC]
):
return click.option(
"-i",
"--implementation",
type=click.Choice(["pillow"], case_sensitive=False),
default="pillow",
help="Name of rendering Virtual Machine to be used.",
)


def _get_output_file_option() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.option(
"-o",
"--output",
type=click.Path(dir_okay=False),
default="output.png",
help="Path to output file.",
)


def _get_dpmm_option() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.option(
"-d",
"--dpmm",
type=int,
default=20,
help="Dots per millimeter.",
)


def _get_source_file_argument() -> Callable[[click.decorators.FC], click.decorators.FC]:
return click.argument(
"source",
type=click.Path(file_okay=True, dir_okay=False, exists=True, readable=True),
)


@convert.command("png")
@_get_source_file_argument()
@_get_output_file_option()
@_get_dpmm_option()
@_get_file_type_option()
@_get_style_option()
@_get_foreground_option()
@_get_background_option()
@_get_raster_implementation_option()
def png(
source: str,
output: str,
file_type: str,
style: Optional[str],
foreground: Optional[str],
background: Optional[str],
dpmm: int,
implementation: str,
) -> None:
"""Convert Gerber image file to PNG image."""
style_obj = _sanitize_style(style, foreground, background)
file = GerberFile.from_file(source, file_type=FileTypeEnum(file_type.upper()))

if implementation.lower() == "pillow":
result = file.render_with_pillow(style_obj, dpmm)
result.save_png(output)

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


def _sanitize_style(
style: Optional[str],
foreground: Optional[str],
background: Optional[str],
) -> Optional[Style]:
style_obj: Optional[Style] = None

if style is not None:
style = style.upper()

if style == "CUSTOM":
if foreground is None or background is None:
option_name = "foreground" if foreground is None else "background"
msg = (
"When using CUSTOM style, both foreground and background must be "
"specified."
)
raise click.BadOptionUsage(option_name, msg)

try:
background_color = Color.from_hex(background)
except InvalidHexColorLiteralError as e:
msg = f"Invalid hex color literal: {e!r}"
raise click.BadOptionUsage("background", msg) from e

try:
foreground_color = Color.from_hex(foreground)
except InvalidHexColorLiteralError as e:
msg = f"Invalid hex color literal: {e!r}"
raise click.BadOptionUsage("background", msg) from e

style_obj = Style(
background=background_color,
foreground=foreground_color,
)

else:
style_obj = Style.presets.get_styles().get(style)

if style_obj is None:
option_name = "style"
msg = f"Style {style!r} is not recognized."
raise click.BadOptionUsage(option_name, msg)

return style_obj


@convert.command("jpeg")
@_get_source_file_argument()
@_get_output_file_option()
@_get_dpmm_option()
@_get_file_type_option()
@_get_style_option()
@_get_foreground_option()
@_get_background_option()
@_get_raster_implementation_option()
def jpeg(
source: str,
output: str,
file_type: str,
style: Optional[str],
foreground: Optional[str],
background: Optional[str],
dpmm: int,
implementation: str,
) -> None:
"""Convert Gerber image file to JPEG image."""
style_obj = _sanitize_style(style, foreground, background)
file = GerberFile.from_file(source, file_type=FileTypeEnum(file_type.upper()))

if implementation.lower() == "pillow":
result = file.render_with_pillow(style_obj, dpmm)
result.save_jpeg(output)

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


@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.
Layers are merged from first to last, thus last layer will be on top.
"""
raise NotImplementedError
3 changes: 2 additions & 1 deletion src/pygerber/gerber/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
Size,
)
from pygerber.gerber.formatter.options import Options
from pygerber.vm.types.style import Style
from pygerber.vm.types import Color, Style

__all__ = [
"CompositeImage",
Expand All @@ -60,6 +60,7 @@
"ShapelyImage",
"Size",
"Style",
"Color",
"Units",
"Options",
]
Loading

0 comments on commit e829ef3

Please sign in to comment.