From 73602352de47b1a07395520f16580d17b5665aff Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 8 Jan 2022 17:53:07 +0100 Subject: [PATCH] - added commands `propset`, `propget`, `propdel`, `proplist`, `propclear` - removed commands `metadata`, `clearprops` - added global metadata by refactoring metadata-related implementation into a _MetadataMixin class - vp.Document's page_size is now stored as a global property - added a warning when a layer passed to `--layer` doesn't exist - renamed metadata to properties in `stat` (and other user facing texts) --- docs/reference.rst | 28 +++-- tests/test_commands.py | 107 ++++++++++++++++- tests/test_model.py | 4 +- tests/test_viewer.py | 8 +- vpype/layers.py | 9 +- vpype/metadata.py | 10 ++ vpype/model.py | 120 +++++++++++-------- vpype_cli/debug.py | 6 +- vpype_cli/metadata.py | 260 +++++++++++++++++++++++++++++++++++++---- 9 files changed, 462 insertions(+), 90 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index d30c22c1..06a93567 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -23,10 +23,6 @@ CLI reference .. click:: vpype_cli:circle :prog: circle -.. _cmd_clearprops: -.. click:: vpype_cli:clearprops - :prog: clearprops - .. _cmd_color: .. click:: vpype_cli:color :prog: color @@ -95,10 +91,6 @@ CLI reference .. click:: vpype_cli:lswap :prog: lswap -.. _cmd_metadata: -.. click:: vpype_cli:metadata - :prog: metadata - .. _cmd_multipass: .. click:: vpype_cli:multipass :prog: multipass @@ -119,6 +111,26 @@ CLI reference .. click:: vpype_cli:penwidth :prog: penwidth +.. _cmd_propclear: +.. click:: vpype_cli:propclear + :prog: propclear + +.. _cmd_propdel: +.. click:: vpype_cli:propdel + :prog: propdel + +.. _cmd_propget: +.. click:: vpype_cli:propget + :prog: propget + +.. _cmd_proplist: +.. click:: vpype_cli:proplist + :prog: proplist + +.. _cmd_propset: +.. click:: vpype_cli:propset + :prog: propset + .. _cmd_random: .. click:: vpype_cli:random :prog: random diff --git a/tests/test_commands.py b/tests/test_commands.py index 1b1ee965..3dfe0891 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -69,10 +69,18 @@ class Command: Command("squiggles"), Command("text 'hello wold'"), Command("penwidth 0.15mm", preserves_metadata=False), - Command("metadata vp:name my_name", preserves_metadata=False), Command("color red", preserves_metadata=False), Command("name my_name", preserves_metadata=False), - Command("clearprops", preserves_metadata=False), + Command("propset -g prop:global hello", preserves_metadata=False), + Command("propset -l 1 prop:local hello", preserves_metadata=False), + Command("propget -g prop:global"), + Command("propget -l 1 prop:global"), + Command("proplist -g"), + Command("proplist -l 1"), + Command("propdel -g prop:global", preserves_metadata=False), + Command("propdel -l 1 prop:layer", preserves_metadata=False), + Command("propclear -g", preserves_metadata=False), + Command("propclear -l 1", preserves_metadata=False), ] # noinspection SpellCheckingInspection @@ -131,8 +139,8 @@ def test_commands_keeps_page_size(runner, cmd): args = cmd.command - if args.split()[0] in ["pagesize", "layout"]: - return + if args.split()[0] in ["pagesize", "layout"] or args.startswith("propclear -g"): + pytest.skip(f"command {args.split()[0]} fail this test by design") page_size = None @@ -551,3 +559,94 @@ def test_text_command_wrap(font_name, options): def test_text_command_empty(): doc = execute("text ''") assert doc.is_empty() + + +@pytest.mark.parametrize( + ("cmd", "expected_output"), + [ + ("propset -l1 prop val", ""), + ("propset -g prop val", ""), + ("propget -g prop", "global property prop: n/a"), + ("line 0 0 1 1 propget -l1 prop", "layer 1 property prop: n/a"), + ( + "line 0 0 1 1 propset -l1 -t int prop 10 propget -l1 prop", + "layer 1 property prop: (int) 10", + ), + ( + "line 0 0 1 1 propset -l1 -t str prop hello propget -l1 prop", + "layer 1 property prop: (str) hello", + ), + ( + "line 0 0 1 1 propset -l1 -t float prop 10.2 propget -l1 prop", + "layer 1 property prop: (float) 10.2", + ), + ( + "line 0 0 1 1 propset -l1 -t color prop red propget -l1 prop", + "layer 1 property prop: (color) #ff0000", + ), + ( + "pens rgb proplist -l all", + "listing 2 properties for layer 1\n" + " vp:color: (color) #ff0000\n" + " vp:name: (str) red\n" + "listing 2 properties for layer 2\n" + " vp:color: (color) #008000\n" + " vp:name: (str) green\n" + "listing 2 properties for layer 3\n" + " vp:color: (color) #0000ff\n" + " vp:name: (str) blue", + ), + ( + "pens rgb propdel -l1 vp:color proplist -l all", + "listing 1 properties for layer 1\n vp:name: (str) red\n" + "listing 2 properties for layer 2\n" + " vp:color: (color) #008000\n" + " vp:name: (str) green\n" + "listing 2 properties for layer 3\n" + " vp:color: (color) #0000ff\n" + " vp:name: (str) blue", + ), + ( + "pens rgb propdel -l all vp:color proplist -l all", + "listing 1 properties for layer 1\n vp:name: (str) red\n" + "listing 1 properties for layer 2\n vp:name: (str) green\n" + "listing 1 properties for layer 3\n vp:name: (str) blue", + ), + ( + "pens rgb propdel -l all vp:color proplist -l 2", + "listing 1 properties for layer 2\n vp:name: (str) green", + ), + ( + "pens rgb propclear -l all proplist", + "listing 0 properties for layer 1\n" + "listing 0 properties for layer 2\n" + "listing 0 properties for layer 3\n", + ), + ( + "pagesize 400x1200 proplist -g", + "listing 1 global properties\n vp:page_size: (tuple) (400.0, 1200.0)", + ), + ( + "pagesize 400x1200 propdel -g vp:page_size proplist -g", + "listing 0 global properties", + ), + ("propset -g -t int prop 10 propget -g prop", "global property prop: (int) 10"), + ( + "propset -g -t float prop 11.2 propget -g prop", + "global property prop: (float) 11.2", + ), + ("propset -g -t str prop hello propget -g prop", "global property prop: (str) hello"), + ( + "propset -g -t color prop blue propget -g prop", + "global property prop: (color) #0000ff", + ), + ( + "pagesize a4 propset -g prop val propclear -g proplist -g", + "listing 0 global properties", + ), + ], +) +def test_property_commands(runner, cmd, expected_output): + res = runner.invoke(cli, cmd) + assert res.exit_code == 0 + assert res.stdout.strip() == expected_output.strip() diff --git a/tests/test_model.py b/tests/test_model.py index eda28757..cd9221fd 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -313,11 +313,11 @@ def test_line_collection_merge(lines, merge_lines): assert _line_set(lc) == _line_set(merge_lines) -def test_document_empty_copy(): +def test_document_clone(): doc = Document() doc.add(LineCollection([(0, 1)]), 1) doc.page_size = 3, 4 - new_doc = doc.empty_copy() + new_doc = doc.clone() assert len(new_doc.layers) == 0 assert new_doc.page_size == (3, 4) diff --git a/tests/test_viewer.py b/tests/test_viewer.py index 325b62b7..e7e879b3 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -103,7 +103,7 @@ def test_viewer_engine_properties(assert_image_similarity): def test_viewer(assert_image_similarity, file, render_kwargs): # Note: this test relies on lack of metadata doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / file), 0.4) - doc.clear_metadata() + doc.clear_layer_metadata() # noinspection PyArgumentList assert_image_similarity(render_image(doc, (1024, 1024), **render_kwargs)) @@ -120,7 +120,7 @@ def test_viewer_empty_layer(assert_image_similarity, render_kwargs): def test_viewer_zoom_scale(assert_image_similarity): # Note: this test relies on lack of metadata doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / "issue_124/plotter.svg"), 0.4) - doc.clear_metadata() + doc.clear_layer_metadata() renderer = ImageRenderer((1024, 1024)) renderer.engine.document = doc renderer.engine.fit_to_viewport() @@ -133,7 +133,7 @@ def test_viewer_zoom_scale(assert_image_similarity): def test_viewer_scale_origin(assert_image_similarity): # Note: this test relies on lack of metadata doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / "issue_124/plotter.svg"), 0.4) - doc.clear_metadata() + doc.clear_layer_metadata() assert_image_similarity( render_image(doc, view_mode=ViewMode.OUTLINE, origin=(600, 400), scale=4) @@ -143,7 +143,7 @@ def test_viewer_scale_origin(assert_image_similarity): def test_viewer_debug(assert_image_similarity): # Note: this test relies on lack of metadata doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / "issue_124/plotter.svg"), 0.4) - doc.clear_metadata() + doc.clear_layer_metadata() renderer = ImageRenderer((1024, 1024)) renderer.engine.document = doc renderer.engine.origin = (600, 400) diff --git a/vpype/layers.py b/vpype/layers.py index 5bb9df85..ba27598d 100644 --- a/vpype/layers.py +++ b/vpype/layers.py @@ -1,3 +1,4 @@ +import logging from contextlib import contextmanager from typing import List, Optional, Union @@ -46,7 +47,13 @@ def multiple_to_layer_ids( if layers is None or layers is LayerType.ALL: return sorted(document.ids()) elif isinstance(layers, list): - return sorted(vid for vid in layers if document.exists(vid)) + lids = [] + for lid in sorted(layers): + if document.exists(lid): + lids.append(lid) + else: + logging.warning(f"layer {lid} does not exist") + return lids else: return [] diff --git a/vpype/metadata.py b/vpype/metadata.py index 63c16633..08a23274 100644 --- a/vpype/metadata.py +++ b/vpype/metadata.py @@ -51,17 +51,27 @@ def __init__( object.__setattr__(self, "alpha", int(alpha or 255)) def as_floats(self) -> Tuple[float, float, float, float]: + """Returns a float representation of the instance.""" return self.red / 255, self.green / 255, self.blue / 255, self.alpha / 255 + def as_hex(self) -> str: + """Return a standard, hexadecimal representation of the instance.""" + return svgelements.Color(self.red, self.green, self.blue, self.alpha).hex + +# layer metadata field names METADATA_FIELD_NAME = "vp:name" METADATA_FIELD_COLOR = "vp:color" METADATA_FIELD_PEN_WIDTH = "vp:pen_width" +# global metadata field names +METADATA_FIELD_PAGE_SIZE = "vp:page_size" + METADATA_SYSTEM_FIELD_TYPES = { METADATA_FIELD_NAME: str, METADATA_FIELD_COLOR: Color, METADATA_FIELD_PEN_WIDTH: float, + METADATA_FIELD_PAGE_SIZE: tuple, } # noinspection HttpUrlsUsage diff --git a/vpype/model.py b/vpype/model.py index 701842ea..f96412fb 100644 --- a/vpype/model.py +++ b/vpype/model.py @@ -9,7 +9,7 @@ from .geometry import crop, reloop from .line_index import LineIndex -from .metadata import METADATA_SYSTEM_FIELD_TYPES +from .metadata import METADATA_FIELD_PAGE_SIZE, METADATA_SYSTEM_FIELD_TYPES # REMINDER: anything added here must be added to docs/api.rst __all__ = [ @@ -36,8 +36,63 @@ def as_vector(a: np.ndarray): return a.view(dtype=float).reshape(len(a), 2) +class _MetadataMixin: + def __init__(self, metadata: Optional[Dict[str, Any]] = None): + self._metadata: Dict[str, Any] = metadata or {} + + @property + def metadata(self): + """Returns the collection's metadata. + + Returns: + metadata + """ + return self._metadata + + @metadata.setter + def metadata(self, metadata: Dict[str, Any]) -> None: + """Sets the metadata structure.""" + self._metadata = metadata + + def property_exists(self, prop: str) -> bool: + """Check if a given property is set. + + Args: + prop: the property whose existence to check + """ + return prop in self._metadata + + def property(self, prop: str) -> Optional[Any]: + """Returns the value of a metadata property or None if it does not exist. + + Args: + prop: the property to read + """ + return self._metadata.get(prop, None) + + def set_property(self, prop: str, value: Optional[Any]) -> None: + """Sets the value of a metadata property. For system properties, this function casts + the value to the proper type and throw an exception if this fails. If the value is + None, the property is removed. + + Args: + prop: property to set + value: value to assign + """ + if value is None: + self._metadata.pop(prop, None) + else: + if prop in METADATA_SYSTEM_FIELD_TYPES: + value = METADATA_SYSTEM_FIELD_TYPES[prop](value) + self._metadata[prop] = value + + def clear_metadata(self) -> None: + """Remove all properties.""" + self._metadata = {} + + # noinspection PyShadowingNames -class LineCollection: +class LineCollection(_MetadataMixin): """ :py:class:`LineCollection` encapsulate a list of piecewise linear lines (or paths). Lines are implemented as 1D numpy arrays of complex numbers whose real and imaginary parts @@ -98,9 +153,9 @@ def __init__( lines (LineCollectionLike): iterable of line (accepts the same input as :func:`~LineCollection.append`). """ - self._lines: List[np.ndarray] = [] - self._metadata: Dict[str, Any] = metadata or {} + super().__init__(metadata) + self._lines: List[np.ndarray] = [] self.extend(lines) @property @@ -112,20 +167,6 @@ def lines(self) -> List[np.ndarray]: """ return self._lines - @property - def metadata(self): - """Returns the collection's metadata. - - Returns: - metadata - """ - return self._metadata - - @metadata.setter - def metadata(self, metadata: Dict[str, Any]) -> None: - """Sets the metadata structure.""" - self._metadata = metadata - def clone(self, lines: LineCollectionLike = ()) -> "LineCollection": """Creates a new :class:`LineCollection` with the same metadata. @@ -140,26 +181,6 @@ def clone(self, lines: LineCollectionLike = ()) -> "LineCollection": """ return LineCollection(lines=lines, metadata=self.metadata) - def property(self, prop: str) -> Optional[Any]: - """Returns the value of a metadata property or None if it does not exist. - - Args: - prop: the property to read - """ - return self._metadata.get(prop, None) - - def set_property(self, prop: str, value: Any) -> None: - """Sets the value of a metadata property. For system properties, this function casts - the value to the proper type and throw an exception if this fails. - - Args: - prop: property to set - value: value to assign - """ - if prop in METADATA_SYSTEM_FIELD_TYPES: - value = METADATA_SYSTEM_FIELD_TYPES[prop](value) - self._metadata[prop] = value - def append(self, line: LineLike) -> None: """Append a single line. @@ -479,7 +500,7 @@ def segment_count(self) -> int: return sum(max(0, len(line) - 1) for line in self._lines) -class Document: +class Document(_MetadataMixin): """This class is the core data model of vpype and represent the data that is passed from one command to the other. At its core, a Document is a collection of layers identified by non-zero positive integers and each represented by a :py:class:`LineCollection`. @@ -492,6 +513,7 @@ class Document: def __init__( self, line_collection: LineCollection = None, + metadata: Optional[Dict[str, Any]] = None, page_size: Optional[Tuple[float, float]] = None, ): """Create a Document, optionally providing a :py:class:`LayerCollection` for layer 1. @@ -499,15 +521,19 @@ def __init__( Args: line_collection: if provided, used as layer 1 """ + super().__init__(metadata) + self._layers: Dict[int, LineCollection] = {} - self._page_size: Optional[Tuple[float, float]] = page_size + + if page_size is not None: + self.page_size = page_size if line_collection: self.add(line_collection, 1) - def empty_copy(self) -> "Document": - """Create an empty copy of this document with the same page size.""" - return Document(page_size=self.page_size) + def clone(self) -> "Document": + """Create an empty copy of this document with the same metadata""" + return Document(metadata=self.metadata) @property def layers(self) -> Dict[int, LineCollection]: @@ -520,12 +546,12 @@ def layers(self) -> Dict[int, LineCollection]: @property def page_size(self) -> Optional[Tuple[float, float]]: """Returns the page size or None if it hasn't been set.""" - return self._page_size + return self.property(METADATA_FIELD_PAGE_SIZE) @page_size.setter def page_size(self, page_size=Optional[Tuple[float, float]]) -> None: """Sets the page size to a new value.""" - self._page_size = page_size + self.set_property(METADATA_FIELD_PAGE_SIZE, page_size) def extend_page_size(self, page_size: Optional[Tuple[float, float]]) -> None: """Adjust the page sized according to the following logic: @@ -547,10 +573,10 @@ def extend_page_size(self, page_size: Optional[Tuple[float, float]]) -> None: else: self.page_size = page_size - def clear_metadata(self) -> None: + def clear_layer_metadata(self) -> None: """Clear all metadata from the document.""" for layer in self._layers.values(): - layer.metadata = {} + layer.clear_metadata() def ids(self) -> Iterable[int]: """Returns the list of layer IDs""" diff --git a/vpype_cli/debug.py b/vpype_cli/debug.py index 45d6a0ef..8cf82438 100644 --- a/vpype_cli/debug.py +++ b/vpype_cli/debug.py @@ -154,7 +154,7 @@ def stat(document: Document): str(length / layer.segment_count() if layer.segment_count() else "n/a"), ) print(f" Bounds: {layer.bounds()}") - print(" Metadata:") + print(" Properties:") for key, value in layer.metadata.items(): print(f" {key}: {value!r}") print(f"Totals") @@ -164,12 +164,14 @@ def stat(document: Document): print(f" Total length: {length_tot + pen_up_length_tot}") print(f" Path count: {sum(len(layer) for layer in document.layers.values())}") print(f" Segment count: {document.segment_count()}") - print( f" Mean segment length:", str(length_tot / document.segment_count() if document.segment_count() else "n/a"), ) print(f" Bounds: {document.bounds()}") + print(f" Global properties:") + for key, value in document.metadata.items(): + print(f" {key}: {value!r}") print("========================= ") return document diff --git a/vpype_cli/metadata.py b/vpype_cli/metadata.py index 5154652a..7503adf9 100644 --- a/vpype_cli/metadata.py +++ b/vpype_cli/metadata.py @@ -1,4 +1,5 @@ -from typing import Optional +import logging +from typing import Any, List, Optional, Tuple, Union import click @@ -6,35 +7,259 @@ from .cli import cli -__all__ = ("metadata", "penwidth", "color", "name", "pens", "clearprops") +__all__ = ( + "propset", + "proplist", + "propget", + "propdel", + "propclear", + "penwidth", + "color", + "name", + "pens", +) _STR_TO_TYPE = { "str": str, "int": int, "float": float, + "color": vp.Color, } +def _check_scope( + global_flag: bool, layer: Optional[Union[int, List[int]]] +) -> Tuple[bool, Optional[Union[int, List[int]]]]: + if global_flag and layer is not None: + logging.warning( + "incompatible `--global` and `--layer` options were provided, assuming `--global`" + ) + layer = None + elif not global_flag and layer is None: + logging.warning( + "neither `--global` nor `--layer` options were provide, assuming `--layer all`" + ) + layer = vp.LayerType.ALL + + return global_flag, layer + + @cli.command(group="Metadata") @click.argument("prop") @click.argument("value") +@click.option("--global", "-g", "global_flag", is_flag=True, help="Global mode.") @click.option( - "-type", - "--t", + "-l", "--layer", type=vp.LayerType(accept_multiple=True), help="Target layer(s)." +) +@click.option( + "--type", + "-t", "prop_type", type=click.Choice(list(_STR_TO_TYPE.keys())), - help="specify type for property", + default="str", + help="Property type.", ) -@vp.layer_processor -def metadata( - layer: vp.LineCollection, prop: str, value: str, prop_type: Optional[str] -) -> vp.LineCollection: - """Set the value of a metadata property.""" +@vp.global_processor +def propset( + document: vp.Document, + global_flag: bool, + layer: Optional[Union[int, List[int]]], + prop: str, + value: str, + prop_type: str, +): + """Set the value of a global or layer property. + + Either the `--global` or `--layer` option must be used to specify whether a global or + layer property should be set. When using `--layer`, either a single layer ID, a + coma-separated list of layer ID, or `all` may be used. + + By default, the value is stored as a string. Alternative types may be specified with the + `--type` option. The following types are available: + + \b + str: text string + int: integer number + float: floating-point number + color: color + + When using the `color` type, any SVG-compatible string may be used for VALUE, including + 16-bit RGB (#ff0000), 16-bit RGBA (#ff0000ff), 8-bit variants (#f00 or #f00f), or color + names (red). - converted_value = _STR_TO_TYPE[prop_type](value) if prop_type is not None else value - layer.set_property(prop, converted_value) - return layer + Examples: + + Set a global property of type `int`: + + vpype [...] propset --global --type int my_prop 10 [...] + + Set a layer property of type `color`: + + vpype [...] propset --layer 1 --type color my_prop red [...] + """ + + global_flag, layer = _check_scope(global_flag, layer) + + if global_flag: + document.set_property(prop, _STR_TO_TYPE[prop_type](value)) + else: + for lid in vp.multiple_to_layer_ids(layer, document): + document.layers[lid].set_property(prop, _STR_TO_TYPE[prop_type](value)) + + return document + + +def _value_to_str(value: Any) -> str: + if value is None: + return "n/a" + elif isinstance(value, vp.Color): + value_str = value.as_hex() + value_type = "color" + else: + value_str = str(value) + value_type = type(value).__name__ + return f"({value_type}) {value_str}" + + +@cli.command(group="Metadata") +@click.option("--global", "-g", "global_flag", is_flag=True, help="Global mode.") +@click.option( + "-l", "--layer", type=vp.LayerType(accept_multiple=True), help="Target layer(s)." +) +@vp.global_processor +def proplist(document: vp.Document, global_flag: bool, layer: Optional[Union[int, List[int]]]): + """Print a list the existing global or layer properties and their values. + + Either the `--global` or `--layer` option must be used to specify whether global or + layer properties should be listed. When using `--layer`, either a single layer ID, a + coma-separated list of layer ID, or `all` may be used. + + Examples: + + Print a list of global properties: + + vpype pagesize a4 proplist -g + """ + global_flag, layer = _check_scope(global_flag, layer) + + if global_flag: + print(f"listing {len(document.metadata)} global properties") + for prop in sorted(document.metadata.keys()): + print(f" {prop}: {_value_to_str(document.property(prop))}") + else: + for lid in vp.multiple_to_layer_ids(layer, document): + lc = document.layers[lid] + print(f"listing {len(lc.metadata)} properties for layer {lid}") + for prop in sorted(lc.metadata.keys()): + print(f" {prop}: {_value_to_str(lc.property(prop))}") + + return document + + +@cli.command(group="Metadata") +@click.argument("prop") +@click.option("--global", "-g", "global_flag", is_flag=True, help="Global mode.") +@click.option( + "-l", "--layer", type=vp.LayerType(accept_multiple=True), help="Target layer(s)." +) +@vp.global_processor +def propget( + document: vp.Document, global_flag: bool, layer: Optional[Union[int, List[int]]], prop: str +): + """Print the value of a global or layer property. + + Either the `--global` or `--layer` option must be used to specify whether a global or + layer property should be displayed. When using `--layer`, either a single layer ID, a + coma-separated list of layer ID, or `all` may be used. + + Examples: + + Print the value of property `vp:color` for all layers: + + vpype [...] pens cmyk propget --layer all vp:color [...] + """ + global_flag, layer = _check_scope(global_flag, layer) + + if global_flag: + print(f"global property {prop}: {_value_to_str(document.property(prop))}") + else: + for lid in vp.multiple_to_layer_ids(layer, document): + print( + f"layer {lid} property {prop}: " + f"{_value_to_str(document.layers[lid].property(prop))}" + ) + + return document + + +@cli.command(group="Metadata") +@click.argument("prop") +@click.option("--global", "-g", "global_flag", is_flag=True, help="Global mode.") +@click.option( + "-l", "--layer", type=vp.LayerType(accept_multiple=True), help="Target layer(s)." +) +@vp.global_processor +def propdel( + document: vp.Document, global_flag: bool, layer: Optional[Union[int, List[int]]], prop: str +): + """Remove a global or layer property. + + Either the `--global` or `--layer` option must be used to specify whether a global or + layer property should be removed. When using `--layer`, either a single layer ID, a + coma-separated list of layer ID, or `all` may be used. + + Examples: + + Remove a property from a layer: + + vpype [...] pens cmyk propdel --layer 1 vp:name [...] + """ + global_flag, layer = _check_scope(global_flag, layer) + + if global_flag: + document.set_property(prop, None) + else: + for lid in vp.multiple_to_layer_ids(layer, document): + document.layers[lid].set_property(prop, None) + + return document + + +@cli.command(group="Metadata") +@click.option("--global", "-g", "global_flag", is_flag=True, help="Global mode.") +@click.option( + "-l", "--layer", type=vp.LayerType(accept_multiple=True), help="Target layer(s)." +) +@vp.global_processor +def propclear( + document: vp.Document, global_flag: bool, layer: Optional[Union[int, List[int]]] +): + """Remove all global or layer properties. + + Either the `--global` or `--layer` option must be used to specify whether global or + layer properties should be cleared. When using `--layer`, either a single layer ID, a + coma-separated list of layer ID, or `all` may be used. + + Examples: + + Remove all global properties: + + vpype [...] propclear --global [...] + + Remove all properties from layer 1 and 2: + + vpype [...] propclear --layer 1,2 [...] + """ + global_flag, layer = _check_scope(global_flag, layer) + + if global_flag: + document.clear_metadata() + else: + for lid in vp.multiple_to_layer_ids(layer, document): + document.layers[lid].clear_metadata() + + return document @cli.command(group="Metadata") @@ -162,12 +387,3 @@ def pens(document: vp.Document, pen_config: str) -> vp.Document: ) return document - - -@cli.command(group="Metadata") -@vp.layer_processor -def clearprops(lc: vp.LineCollection) -> vp.LineCollection: - """Remove all metadata properties.""" - - lc.metadata = {} - return lc