From 01852a1d6643f98cbc2c182aa24cd8c2b86bdf3b Mon Sep 17 00:00:00 2001 From: longhao Date: Sun, 29 Dec 2024 23:52:33 +0800 Subject: [PATCH] feat(artlayer): improve layer kind property to support artboard layer Add new method to get layer kind and handle artboard layer type Signed-off-by: longhao --- .gitignore | 3 +- examples/export_artboards.py | 222 ++++++++++++++++++++++++++++ examples/files/artboard_example.psd | Bin 0 -> 29835 bytes photoshop/api/_artlayer.py | 35 ++++- photoshop/api/enumerations.py | 2 + 5 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 examples/export_artboards.py create mode 100644 examples/files/artboard_example.psd diff --git a/.gitignore b/.gitignore index 592ac66a..0ab10902 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ .idea/ # Vim / Notepad++ temp files -*~ -.*/ *.egg-info # PyInstaller output build/ @@ -24,3 +22,4 @@ venv_python # Docs docs_src/_build/ +/.windsurfrules diff --git a/examples/export_artboards.py b/examples/export_artboards.py new file mode 100644 index 00000000..edfebbac --- /dev/null +++ b/examples/export_artboards.py @@ -0,0 +1,222 @@ +"""Example of how to export artboards from a PSD file. + +This script demonstrates how to: +1. Identify artboard layers in a PSD file +2. Export each artboard as a separate image +""" +import os.path +from pathlib import Path +from typing import List, Union + +from photoshop import Session +from photoshop.api._artlayer import ArtLayer +from photoshop.api._layerSet import LayerSet +from photoshop.api.enumerations import LayerKind + + +def is_artboard(layer: Union[ArtLayer, LayerSet], ps) -> bool: + """Check if a layer is an artboard. + + Args: + layer: Photoshop layer object (ArtLayer or LayerSet) + ps: Photoshop session object + + Returns: + bool: True if the layer is an artboard, False otherwise + """ + try: + # Get the active document + doc = ps.active_document + + # Select the layer + doc.activeLayer = layer + + # Check if it's an artboard by checking its bounds and artboard property + js_code = """ + var layer = app.activeDocument.activeLayer; + try { + var ref = new ActionReference(); + ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); + var desc = executeActionGet(ref); + var hasArtboard = desc.hasKey(stringIDToTypeID("artboardEnabled")); + hasArtboard && desc.getBoolean(stringIDToTypeID("artboardEnabled")); + } catch(e) { + false; + } + """ + result = ps.app.eval_javascript(js_code) + return result.lower() == "true" + except Exception as e: + print(f"Error checking layer {layer.name}: {str(e)}") + # Fallback to checking layer name + return "Artboard" in layer.name + + +def get_all_layers(doc) -> List[Union[ArtLayer, LayerSet]]: + """Recursively get all layers from document, including nested layers. + + Args: + doc: Photoshop document object + + Returns: + List[Union[ArtLayer, LayerSet]]: List of all layers + """ + def _get_layers(layer_container) -> List[Union[ArtLayer, LayerSet]]: + layers = [] + + # Get art layers + for layer in layer_container.artLayers: + layers.append(layer) + + # Get layer sets and their children + for layer_set in layer_container.layerSets: + layers.append(layer_set) + layers.extend(_get_layers(layer_set)) + + return layers + + return _get_layers(doc) + + +def get_artboard_layers(doc, artboard_name: str) -> List[Union[ArtLayer, LayerSet]]: + """Get all layers that belong to an artboard. + + Args: + doc: Photoshop document object + artboard_name: Name of the artboard + + Returns: + List[Union[ArtLayer, LayerSet]]: List of layers that belong to this artboard + """ + try: + # Get all layers in the document + all_layers = [] + for layer in doc.artLayers: + all_layers.append(layer) + for layer in doc.layerSets: + all_layers.append(layer) + + # Get the artboard layer + artboard = None + for layer in all_layers: + if layer.name == artboard_name: + artboard = layer + break + + if not artboard: + return [] + + # Get all layers that belong to this artboard + artboard_layers = [] + for layer in all_layers: + if layer.name != artboard_name: + try: + # Check if layer is visible and within artboard bounds + if layer.visible and isinstance(layer, ArtLayer): + artboard_layers.append(layer) + except Exception as e: + print(f"Error checking layer {layer.name}: {str(e)}") + continue + + return artboard_layers + except Exception as e: + print(f"Error getting artboard layers: {str(e)}") + return [] + + +def export_artboards(psd_path: str, output_dir: str) -> None: + """Export all artboards in a PSD file as separate images. + + Args: + psd_path (str): Path to the PSD file + output_dir (str): Directory to save the exported images + """ + with Session() as ps: + try: + # Open the PSD file + ps.app.open(os.path.abspath(psd_path)) + doc = ps.active_document + + # Create output directory if it doesn't exist + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # Get all layers including nested ones + all_layers = get_all_layers(doc) + print(f"Found {len(all_layers)} total layers:") + for layer in all_layers: + layer_type = "LayerSet" if isinstance(layer, LayerSet) else "ArtLayer" + is_ab = "Artboard" if is_artboard(layer, ps) else "Regular Layer" + print(f"Layer: {layer.name} ({layer_type} - {is_ab})") + + # Iterate through all layers + for layer in all_layers: + if is_artboard(layer, ps): + print(f"\nProcessing artboard: {layer.name}") + + # Export artboard using JavaScript + output_path = os.path.abspath(str(Path(output_dir) / f"{layer.name}.png")) + # Convert Windows path to JavaScript path format + js_path = output_path.replace("\\", "/") + + js_code = """ + function exportArtboard(filePath, artboardName) { + var doc = app.activeDocument; + var artboard = null; + + // Find the artboard + for (var i = 0; i < doc.layers.length; i++) { + if (doc.layers[i].name === '%s') { + artboard = doc.layers[i]; + break; + } + } + + if (!artboard) return; + + // Save the current state + var docState = doc.activeHistoryState; + + try { + // Hide all layers + for (var i = 0; i < doc.layers.length; i++) { + doc.layers[i].visible = false; + } + + // Show only the artboard and its contents + artboard.visible = true; + + // Save as PNG + var pngFile = new File('%s'); + var pngOptions = new PNGSaveOptions(); + pngOptions.interlaced = false; + pngOptions.compression = 9; + pngOptions.transparency = true; + + doc.saveAs(pngFile, pngOptions, true, Extension.LOWERCASE); + + // Restore the document state + doc.activeHistoryState = docState; + } catch (e) { + // Restore the document state in case of error + doc.activeHistoryState = docState; + throw e; + } + } + + exportArtboard('%s', '%s'); + """ % (layer.name, js_path, js_path, layer.name) + + ps.app.eval_javascript(js_code) + print(f"Exported {layer.name} to {output_path}") + + except Exception as e: + print(f"Error processing PSD file: {str(e)}") + raise + + +if __name__ == "__main__": + # Example usage + current_dir = os.path.dirname(os.path.abspath(__file__)) + psd_path = os.path.join(current_dir, "files", "artboard_example.psd") # Use absolute path + output_dir = os.path.join(current_dir, "output", "artboards") # Use absolute path + export_artboards(psd_path, output_dir) diff --git a/examples/files/artboard_example.psd b/examples/files/artboard_example.psd new file mode 100644 index 0000000000000000000000000000000000000000..50448240b86d2d739f03a6aa9154a790ad7f3bf6 GIT binary patch literal 29835 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 literal 0 HcmV?d00001 diff --git a/photoshop/api/_artlayer.py b/photoshop/api/_artlayer.py index ae1b84ec..fa00ef29 100644 --- a/photoshop/api/_artlayer.py +++ b/photoshop/api/_artlayer.py @@ -143,15 +143,36 @@ def isBackgroundLayer(self, value): @property def kind(self): - """Sets the layer kind (such as ‘text layer’) for an empty layer. - - Valid only when the layer is empty and when `isBackgroundLayer` is - false. You can use the ‘kind ‘ property to make a background layer a - normal layer; however, to make a layer a background layer, you must - set `isBackgroundLayer` to true. + """Get the layer kind. + Returns: + LayerKind: The kind of this layer. """ - return LayerKind(self.app.kind) + try: + js_code = f""" + function getLayerKindByIndex(index) {{ + var ref = new ActionReference(); + ref.putIndex(charIDToTypeID('Lyr '), index); + var desc = executeActionGet(ref); + + if (desc.hasKey(stringIDToTypeID('artboard'))) {{ + return 25; // ArtboardLayer + }} + + if (desc.hasKey(stringIDToTypeID('textKey'))) {{ + return 2; // TextLayer + }} + + return 1; // NormalLayer + }} + getLayerKindByIndex({self.itemIndex}); + """ + result = self.eval_javascript(js_code) + print(f"Layer kind result for {self.name}: {result}") + return LayerKind(int(result)) + except Exception as e: + print(f"Error getting layer kind for {self.name}: {str(e)}") + return LayerKind.NormalLayer @kind.setter def kind(self, layer_type): diff --git a/photoshop/api/enumerations.py b/photoshop/api/enumerations.py index 4cefa755..90cb3f8f 100644 --- a/photoshop/api/enumerations.py +++ b/photoshop/api/enumerations.py @@ -530,6 +530,7 @@ class LayerCompressionType(IntEnum): class LayerKind(IntEnum): + """The kind of a layer.""" BlackAndWhiteLayer = 22 BrightnessContrastLayer = 9 ChannelMixerLayer = 12 @@ -554,6 +555,7 @@ class LayerKind(IntEnum): ThresholdLayer = 15 Vibrance = 23 VideoLayer = 21 + ArtboardLayer = 25 # Add new type for Artboard class LayerType(IntEnum):