diff --git a/examples/active_layer.py b/examples/active_layer.py index 10a82fca..3c3ab1c2 100644 --- a/examples/active_layer.py +++ b/examples/active_layer.py @@ -28,9 +28,9 @@ docRef.artLayers.add() # Display current active layer name - ps.echo(docRef.activeLayer.name) + print(docRef.activeLayer.name) # Create and rename a new layer new_layer = docRef.artLayers.add() - ps.echo(new_layer.name) + print(new_layer.name) new_layer.name = "test" diff --git a/examples/add_metadata.py b/examples/add_metadata.py index 63d471bd..a8369db2 100644 --- a/examples/add_metadata.py +++ b/examples/add_metadata.py @@ -13,4 +13,4 @@ doc.info.provinceState = "Beijing" doc.info.title = "My Demo" print("Print all metadata of current active document.") - ps.echo(doc.info) + print(doc.info) diff --git a/examples/change_color_of_background_and_foreground.py b/examples/change_color_of_background_and_foreground.py index 8263f90a..8f78f651 100644 --- a/examples/change_color_of_background_and_foreground.py +++ b/examples/change_color_of_background_and_foreground.py @@ -34,10 +34,10 @@ ps.app.backgroundColor = bg_color # Print current colors - ps.echo(f"Foreground RGB: {ps.app.foregroundColor.rgb.red}, " + print(f"Foreground RGB: {ps.app.foregroundColor.rgb.red}, " f"{ps.app.foregroundColor.rgb.green}, " f"{ps.app.foregroundColor.rgb.blue}") - ps.echo(f"Background RGB: {ps.app.backgroundColor.rgb.red}, " + print(f"Background RGB: {ps.app.backgroundColor.rgb.red}, " f"{ps.app.backgroundColor.rgb.green}, " f"{ps.app.backgroundColor.rgb.blue}") diff --git a/examples/compare_colors.py b/examples/compare_colors.py index ab1f1e3b..5a430d57 100644 --- a/examples/compare_colors.py +++ b/examples/compare_colors.py @@ -34,4 +34,4 @@ color1.rgb.green == color2.rgb.green and color1.rgb.blue == color2.rgb.blue) - ps.echo(f"Colors are {'same' if is_same else 'different'}") + print(f"Colors are {'same' if is_same else 'different'}") diff --git a/examples/convert_smartobject_to_layer.py b/examples/convert_smartobject_to_layer.py index 12368bb8..428df623 100644 --- a/examples/convert_smartobject_to_layer.py +++ b/examples/convert_smartobject_to_layer.py @@ -26,12 +26,12 @@ # Convert to smart object layer.convertToSmartObject() - ps.echo("Layer converted to Smart Object") + print("Layer converted to Smart Object") # Check if it's a smart object if layer.kind == ps.LayerKind.SmartObjectLayer: - ps.echo("Layer is now a Smart Object") + print("Layer is now a Smart Object") # Convert back to regular layer layer.rasterize(ps.RasterizeType.EntireLayer) - ps.echo("Smart Object converted back to regular layer") + print("Smart Object converted back to regular layer") diff --git a/examples/current_tool.py b/examples/current_tool.py index 949929e0..0b13289f 100644 --- a/examples/current_tool.py +++ b/examples/current_tool.py @@ -22,4 +22,4 @@ current = ps.app.currentTool # Print current tool name - ps.echo(f"Current tool: {current}") + print(f"Current tool: {current}") diff --git a/examples/eval_javascript.py b/examples/eval_javascript.py index 805a643b..59ec5ccd 100644 --- a/examples/eval_javascript.py +++ b/examples/eval_javascript.py @@ -21,4 +21,4 @@ # Execute JavaScript command js_code = "app.documents.length" result = ps.app.eval_javascript(js_code) - ps.echo(f"Number of open documents: {result}") + print(f"Number of open documents: {result}") diff --git a/examples/export_layers_use_export_options_saveforweb.py b/examples/export_layers_use_export_options_saveforweb.py index aa84ecc2..00c89b76 100644 --- a/examples/export_layers_use_export_options_saveforweb.py +++ b/examples/export_layers_use_export_options_saveforweb.py @@ -33,7 +33,7 @@ def main(): image_path = os.path.join(layer_path, f"{layer.name}.png") doc.exportDocument(image_path, exportAs=ps.ExportType.SaveForWeb, options=options) ps.alert("Task done!") - ps.echo(doc.activeLayer) + print(doc.activeLayer) if __name__ == "__main__": diff --git a/examples/get_document_by_name.py b/examples/get_document_by_name.py index 6ca9c797..b50482ac 100644 --- a/examples/get_document_by_name.py +++ b/examples/get_document_by_name.py @@ -19,7 +19,7 @@ # Try to get document named 'test.psd' for doc in ps.app.documents: if doc.name == "test.psd": - ps.echo(doc.name) + print(doc.name) break else: - ps.echo("Document not found!") + print("Document not found!") diff --git a/examples/get_layer_by_name.py b/examples/get_layer_by_name.py index c287e621..92078784 100644 --- a/examples/get_layer_by_name.py +++ b/examples/get_layer_by_name.py @@ -20,5 +20,5 @@ doc = ps.app.activeDocument for layer in doc.layers: if layer.name == "example layer": - ps.echo(layer.name) + print(layer.name) break diff --git a/examples/link_layer.py b/examples/link_layer.py index 0a327955..de4a520a 100644 --- a/examples/link_layer.py +++ b/examples/link_layer.py @@ -35,9 +35,9 @@ layer2.link(layer3) # Check link status - ps.echo(f"Layer 1 linked: {layer1.linked}") - ps.echo(f"Layer 2 linked: {layer2.linked}") - ps.echo(f"Layer 3 linked: {layer3.linked}") + print(f"Layer 1 linked: {layer1.linked}") + print(f"Layer 2 linked: {layer2.linked}") + print(f"Layer 3 linked: {layer3.linked}") # Move linked layers together layer1.translate(100, 100) diff --git a/examples/open_psd.py b/examples/open_psd.py index ac03d4d7..ed6e0fb4 100644 --- a/examples/open_psd.py +++ b/examples/open_psd.py @@ -9,4 +9,4 @@ # style 2 with Session("your/psd/or/psb/file_path.psd", action="open") as ps: - ps.echo(ps.active_document.name) + print(ps.active_document.name) diff --git a/examples/operate_channels.py b/examples/operate_channels.py index d64becb3..84f1a126 100644 --- a/examples/operate_channels.py +++ b/examples/operate_channels.py @@ -22,7 +22,7 @@ # List all channels for channel in doc.channels: - ps.echo(f"Channel: {channel.name}") + print(f"Channel: {channel.name}") # Create a new alpha channel new_channel = doc.channels.add() diff --git a/examples/operate_layerSet.py b/examples/operate_layerSet.py index a4a4bb3f..67fdc595 100644 --- a/examples/operate_layerSet.py +++ b/examples/operate_layerSet.py @@ -41,10 +41,10 @@ # List layers in groups for layer in main_group.layers: - ps.echo(f"Layer in main group: {layer.name}") + print(f"Layer in main group: {layer.name}") for layer in sub_group.layers: - ps.echo(f"Layer in sub group: {layer.name}") + print(f"Layer in sub group: {layer.name}") # Move a layer between groups layer1.move(sub_group, ps.ElementPlacement.INSIDE) diff --git a/examples/session_callback.py b/examples/session_callback.py index 9cc2eaf6..cdd97d3c 100644 --- a/examples/session_callback.py +++ b/examples/session_callback.py @@ -23,4 +23,4 @@ def on_close(): with Session(callback=on_close) as ps: - ps.echo("Working in session...") + print("Working in session...") diff --git a/examples/session_new_document.py b/examples/session_new_document.py index 7e34c73f..bdbc03e4 100644 --- a/examples/session_new_document.py +++ b/examples/session_new_document.py @@ -4,4 +4,4 @@ with Session(action="new_document") as ps: - ps.echo(ps.active_document.name) + print(ps.active_document.name) diff --git a/photoshop/api/_active_layer.py b/photoshop/api/_active_layer.py deleted file mode 100644 index 16449766..00000000 --- a/photoshop/api/_active_layer.py +++ /dev/null @@ -1,18 +0,0 @@ -# Import local modules -from photoshop.api._core import Photoshop - - -class ActiveLayer(Photoshop): - """The selected layer.""" - - def __int__(self): - super().__init__() - - @property - def name(self) -> str: - """The name of the layer.""" - return self.active_layer.Typename - - def add(self): - """Adds an element.""" - self.app.ActiveDocument.ArtLayers.Add() diff --git a/photoshop/api/_artlayer.py b/photoshop/api/_artlayer.py index ae144f8e..e6f71517 100644 --- a/photoshop/api/_artlayer.py +++ b/photoshop/api/_artlayer.py @@ -1,21 +1,34 @@ # Import built-in modules -from typing import Any +from os import PathLike # Import local modules from photoshop.api._core import Photoshop +from photoshop.api._layer import Layer +from photoshop.api.enumerations import CreateFields +from photoshop.api.enumerations import DepthMaource +from photoshop.api.enumerations import DisplacementMapType +from photoshop.api.enumerations import ElementPlacement +from photoshop.api.enumerations import EliminateFields +from photoshop.api.enumerations import Geometry +from photoshop.api.enumerations import LayerKind +from photoshop.api.enumerations import LensType +from photoshop.api.enumerations import NoiseDistribution +from photoshop.api.enumerations import OffsetUndefinedAreas from photoshop.api.enumerations import RasterizeType +from photoshop.api.enumerations import TextureType +from photoshop.api.enumerations import UndefinedAreas from photoshop.api.text_item import TextItem # pylint: disable=too-many-public-methods, too-many-arguments -class ArtLayer(Photoshop): +class ArtLayer(Layer): """An object within a document that contains the visual elements of the image (equivalent to a layer in the Adobe Photoshop application). """ - def __init__(self, parent: Any = None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "add", @@ -48,79 +61,39 @@ def __init__(self, parent: Any = None): "applyOceanRipple", "applyOffset", "applyPinch", - "delete", - "duplicate", "invert", - "link", "merge", - "move", "posterize", "rasterize", - "unlink", "convertToSmartObject", ) @property - def allLocked(self): - return self.app.allLocked - - @allLocked.setter - def allLocked(self, value): - self.app.allLocked = value - - @property - def blendMode(self): - return self.app.blendMode - - @blendMode.setter - def blendMode(self, mode): - self.app.blendMode = mode - - @property - def bounds(self): - """The bounding rectangle of the layer.""" - return self.app.bounds - - @property - def linkedLayers(self) -> list: - """Get all layers linked to this layer. - - Returns: - list: Layer objects""" - return [ArtLayer(layer) for layer in self.app.linkedLayers] - - @property - def name(self) -> str: - return self.app.name - - @name.setter - def name(self, text: str): - self.app.name = text - - @property - def fillOpacity(self): + def fillOpacity(self) -> float: """The interior opacity of the layer. Range: 0.0 to 100.0.""" return self.app.fillOpacity @fillOpacity.setter - def fillOpacity(self, value): + def fillOpacity(self, value: float) -> None: """The interior opacity of the layer. Range: 0.0 to 100.0.""" self.app.fillOpacity = value @property - def filterMaskDensity(self): + def filterMaskDensity(self) -> float: + """The density of the filter mask (between 0.0 and 100.0)""" return self.app.filterMaskDensity @filterMaskDensity.setter - def filterMaskDensity(self, value): + def filterMaskDensity(self, value: float) -> None: self.app.filterMaskDensity = value @property - def filterMaskFeather(self): + def filterMaskFeather(self) -> float: + """The feather of the filter mask (between 0.0 and 250.0)""" return self.app.filterMaskFeather @filterMaskFeather.setter - def filterMaskFeather(self, value): + def filterMaskFeather(self, value: float) -> None: self.app.filterMaskFeather = value @property @@ -129,97 +102,66 @@ def grouped(self) -> bool: return self.app.grouped @grouped.setter - def grouped(self, value): + def grouped(self, value: bool) -> None: self.app.grouped = value @property - def isBackgroundLayer(self): + def isBackgroundLayer(self) -> bool: """bool: If true, the layer is a background layer.""" return self.app.isBackgroundLayer @isBackgroundLayer.setter - def isBackgroundLayer(self, value): + def isBackgroundLayer(self, value: bool) -> None: self.app.isBackgroundLayer = value @property - def kind(self): + def kind(self) -> LayerKind: """Get the layer kind. Returns: LayerKind: The kind of this layer. """ - try: - js = """ - var ref = new ActionReference(); - ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); - var desc = executeActionGet(ref); - var layerType = desc.getInteger(stringIDToTypeID("layerKind")); - layerType; - """ - return int(self.eval_javascript(js)) - except Exception as e: - print(f"Error getting layer kind: {str(e)}") - return None + return LayerKind(self.app.kind) @kind.setter - def kind(self, layer_type): - """set the layer kind.""" - self.app.kind = layer_type + def kind(self, value: LayerKind) -> None: + self.app.kind = value @property - def layerMaskDensity(self): + def layerMaskDensity(self) -> float: """The density of the layer mask (between 0.0 and 100.0).""" return self.app.layerMaskDensity @layerMaskDensity.setter - def layerMaskDensity(self, value): + def layerMaskDensity(self, value: float) -> None: self.app.layerMaskDensity = value @property - def layerMaskFeather(self): + def layerMaskFeather(self) -> float: """The feather of the layer mask (between 0.0 and 250.0).""" return self.app.layerMaskFeather @layerMaskFeather.setter - def layerMaskFeather(self, value): + def layerMaskFeather(self, value: float) -> None: self.app.layerMaskFeather = value @property - def opacity(self): - """The master opacity of the layer.""" - return round(self.app.opacity) - - @opacity.setter - def opacity(self, value): - self.app.opacity = value - - @property - def parent(self): - """The object’s container.""" - return self.app.parent - - @parent.setter - def parent(self, value): - """Set the object’s container.""" - self.app.parent = value - - @property - def pixelsLocked(self): + def pixelsLocked(self) -> bool: """If true, the pixels in the layer’s image cannot be edited.""" return self.app.pixelsLocked @pixelsLocked.setter - def pixelsLocked(self, value): + def pixelsLocked(self, value: bool) -> None: self.app.pixelsLocked = value @property - def positionLocked(self): + def positionLocked(self) -> bool: """bool: If true, the pixels in the layer’s image cannot be moved within the layer.""" return self.app.positionLocked @positionLocked.setter - def positionLocked(self, value): + def positionLocked(self, value: bool) -> None: self.app.positionLocked = value @property @@ -233,49 +175,36 @@ def textItem(self) -> TextItem: return TextItem(self.app.textItem) @textItem.setter - def textItem(self, value): + def textItem(self, value: TextItem) -> None: self.app.textItem = value @property - def transparentPixelsLocked(self): + def transparentPixelsLocked(self) -> bool: return self.app.transparentPixelsLocked @transparentPixelsLocked.setter - def transparentPixelsLocked(self, value): + def transparentPixelsLocked(self, value: bool) -> None: self.app.transparentPixelsLocked = value @property - def vectorMaskDensity(self): + def vectorMaskDensity(self) -> float: + """The density of the vector mask (between 0.0 and 100.0)""" return self.app.vectorMaskDensity @vectorMaskDensity.setter - def vectorMaskDensity(self, value): + def vectorMaskDensity(self, value: float) -> None: self.app.vectorMaskDensity = value @property - def vectorMaskFeather(self): + def vectorMaskFeather(self) -> float: + """The feather of the vector mask (between 0.0 and 250.0)""" return self.app.vectorMaskFeather @vectorMaskFeather.setter - def vectorMaskFeather(self, value): + def vectorMaskFeather(self, value: float) -> None: self.app.vectorMaskFeather = value - @property - def visible(self): - return self.app.visible - - @visible.setter - def visible(self, value): - self.app.visible = value - - @property - def length(self): - return len(list(self.app)) - - def add(self): - return self.app.add() - - def adjustBrightnessContrast(self, brightness, contrast): + def adjustBrightnessContrast(self, brightness: int, contrast: int) -> None: """Adjusts the brightness and contrast. Args: @@ -283,15 +212,15 @@ def adjustBrightnessContrast(self, brightness, contrast): contrast (int): The contrast amount. Range: -100 to 100. """ - return self.app.adjustBrightnessContrast(brightness, contrast) + self.app.adjustBrightnessContrast(brightness, contrast) def adjustColorBalance( self, - shadows, - midtones, - highlights, - preserveLuminosity, - ): + shadows: tuple[int, int, int], + midtones: tuple[int, int, int], + highlights: tuple[int, int, int], + preserveLuminosity: bool, + ) -> None: """Adjusts the color balance of the layer’s component channels. Args: @@ -310,14 +239,14 @@ def adjustColorBalance( preserveLuminosity: If true, luminosity is preserved. """ - return self.app.adjustColorBalance( + self.app.adjustColorBalance( shadows, midtones, highlights, preserveLuminosity, ) - def adjustCurves(self, curveShape): + def adjustCurves(self, curveShape: list[tuple[float, float]]) -> None: """Adjusts the tonal range of the selected channel using up to fourteen points. @@ -330,16 +259,16 @@ def adjustCurves(self, curveShape): Returns: """ - return self.app.adjustCurves(curveShape) + self.app.adjustCurves(curveShape) def adjustLevels( self, - inputRangeStart, - inputRangeEnd, - inputRangeGamma, - outputRangeStart, - outputRangeEnd, - ): + inputRangeStart: int, + inputRangeEnd: int, + inputRangeGamma: float, + outputRangeStart: int, + outputRangeEnd: int, + ) -> None: """Adjusts levels of the selected channels. Args: @@ -352,7 +281,7 @@ def adjustLevels( Returns: """ - return self.app.adjustLevels( + self.app.adjustLevels( inputRangeStart, inputRangeEnd, inputRangeGamma, @@ -360,10 +289,10 @@ def adjustLevels( outputRangeEnd, ) - def applyAddNoise(self, amount, distribution, monochromatic): - return self.app.applyAddNoise(amount, distribution, monochromatic) + def applyAddNoise(self, amount: float, distribution: NoiseDistribution, monochromatic: bool) -> None: + self.app.applyAddNoise(amount, distribution, monochromatic) - def applyDiffuseGlow(self, graininess, amount, clear_amount): + def applyDiffuseGlow(self, graininess: int, amount: int, clear_amount: int) -> None: """Applies the diffuse glow filter. Args: @@ -374,103 +303,103 @@ def applyDiffuseGlow(self, graininess, amount, clear_amount): Returns: """ - return self.app.applyDiffuseGlow(graininess, amount, clear_amount) + self.app.applyDiffuseGlow(graininess, amount, clear_amount) - def applyAverage(self): + def applyAverage(self) -> None: """Applies the average filter.""" - return self.app.applyAverage() + self.app.applyAverage() - def applyBlur(self): + def applyBlur(self) -> None: """Applies the blur filter.""" - return self.app.applyBlur() + self.app.applyBlur() - def applyBlurMore(self): + def applyBlurMore(self) -> None: """Applies the blur more filter.""" - return self.app.applyBlurMore() + self.app.applyBlurMore() - def applyClouds(self): + def applyClouds(self) -> None: """Applies the clouds filter.""" - return self.app.applyClouds() + self.app.applyClouds() - def applyCustomFilter(self, characteristics, scale, offset): + def applyCustomFilter(self, characteristics: list[int], scale: int, offset: int) -> None: """Applies the custom filter.""" - return self.app.applyCustomFilter(characteristics, scale, offset) + self.app.applyCustomFilter(characteristics, scale, offset) - def applyDeInterlace(self, eliminateFields, createFields): + def applyDeInterlace(self, eliminateFields: EliminateFields, createFields: CreateFields) -> None: """Applies the de-interlace filter.""" - return self.app.applyDeInterlace(eliminateFields, createFields) + self.app.applyDeInterlace(eliminateFields, createFields) - def applyDespeckle(self): - return self.app.applyDespeckle() + def applyDespeckle(self) -> None: + self.app.applyDespeckle() - def applyDifferenceClouds(self): + def applyDifferenceClouds(self) -> None: """Applies the difference clouds filter.""" - return self.app.applyDifferenceClouds() + self.app.applyDifferenceClouds() def applyDisplace( self, - horizontalScale, - verticalScale, - displacementType, - undefinedAreas, - displacementMapFile, - ): + horizontalScale: int, + verticalScale: int, + displacementType: DisplacementMapType, + undefinedAreas: UndefinedAreas, + displacementMapFile: str | PathLike[str], + ) -> None: """Applies the displace filter.""" - return self.app.applyDisplace( + self.app.applyDisplace( horizontalScale, verticalScale, displacementType, undefinedAreas, - displacementMapFile, + str(displacementMapFile), ) - def applyDustAndScratches(self, radius, threshold): + def applyDustAndScratches(self, radius: int, threshold: int) -> None: """Applies the dust and scratches filter.""" - return self.app.applyDustAndScratches(radius, threshold) + self.app.applyDustAndScratches(radius, threshold) - def applyGaussianBlur(self, radius): + def applyGaussianBlur(self, radius: float) -> None: """Applies the gaussian blur filter.""" - return self.app.applyGaussianBlur(radius) + self.app.applyGaussianBlur(radius) def applyGlassEffect( self, - distortion, - smoothness, - scaling, - invert, - texture, - textureFile, - ): - return self.app.applyGlassEffect( + distortion: int, + smoothness: int, + scaling: int, + invert: bool, + texture: TextureType, + textureFile: str | PathLike[str], + ) -> None: + self.app.applyGlassEffect( distortion, smoothness, scaling, invert, texture, - textureFile, + str(textureFile), ) - def applyHighPass(self, radius): + def applyHighPass(self, radius: float) -> None: """Applies the high pass filter.""" - return self.app.applyHighPass(radius) + self.app.applyHighPass(radius) def applyLensBlur( self, - source, - focalDistance, - invertDepthMap, - shape, - radius, - bladeCurvature, - rotation, - brightness, - threshold, - amount, - distribution, - monochromatic, - ): + source: DepthMaource, + focalDistance: int, + invertDepthMap: bool, + shape: Geometry, + radius: int, + bladeCurvature: int, + rotation: int, + brightness: int, + threshold: int, + amount: int, + distribution: NoiseDistribution, + monochromatic: bool, + ) -> None: """Apply the lens blur filter.""" - return self.app.applyLensBlur( + self.app.applyLensBlur( source, focalDistance, invertDepthMap, @@ -485,60 +414,50 @@ def applyLensBlur( monochromatic, ) - def applyLensFlare(self, brightness, flareCenter, lensType): - return self.app.applyLensFlare(brightness, flareCenter, lensType) + def applyLensFlare(self, brightness: int, flareCenter: tuple[float, float], lensType: LensType) -> None: + self.app.applyLensFlare(brightness, flareCenter, lensType) - def applyMaximum(self, radius): + def applyMaximum(self, radius: float) -> None: self.app.applyMaximum(radius) - def applyMedianNoise(self, radius): + def applyMedianNoise(self, radius: float) -> None: self.app.applyMedianNoise(radius) - def applyMinimum(self, radius): + def applyMinimum(self, radius: float) -> None: self.app.applyMinimum(radius) - def applyMotionBlur(self, angle, radius): + def applyMotionBlur(self, angle: int, radius: float) -> None: self.app.applyMotionBlur(angle, radius) - def applyNTSC(self): + def applyNTSC(self) -> None: self.app.applyNTSC() - def applyOceanRipple(self, size, magnitude): + def applyOceanRipple(self, size: int, magnitude: int) -> None: self.app.applyOceanRipple(size, magnitude) - def applyOffset(self, horizontal, vertical, undefinedAreas): + def applyOffset(self, horizontal: int, vertical: int, undefinedAreas: OffsetUndefinedAreas) -> None: self.app.applyOffset(horizontal, vertical, undefinedAreas) - def applyPinch(self, amount): + def applyPinch(self, amount: int) -> None: self.app.applyPinch(amount) - def remove(self): - """Removes this layer from the document.""" - self.app.delete() - - def rasterize(self, target: RasterizeType): + def rasterize(self, target: RasterizeType) -> None: self.app.rasterize(target) - def posterize(self, levels): + def posterize(self, levels: int) -> None: self.app.posterize(levels) - def move(self, relativeObject, insertionLocation): - self.app.move(relativeObject, insertionLocation) - - def merge(self): + def merge(self) -> "ArtLayer": return ArtLayer(self.app.merge()) - def link(self, with_layer): - self.app.link(with_layer) - - def unlink(self): - """Unlink this layer from any linked layers.""" - self.app.unlink() - - def invert(self): + def invert(self) -> None: self.app.invert() - def duplicate(self, relativeObject=None, insertionLocation=None): + def duplicate( + self, + relativeObject: "Layer | None" = None, + insertionLocation: ElementPlacement | None = None, + ) -> "ArtLayer": """Duplicates the layer. Args: @@ -549,10 +468,9 @@ def duplicate(self, relativeObject=None, insertionLocation=None): ArtLayer: The duplicated layer. """ - dup = self.app.duplicate(relativeObject, insertionLocation) - return ArtLayer(dup) + return ArtLayer(self.app.duplicate(relativeObject, insertionLocation)) - def convertToSmartObject(self): + def convertToSmartObject(self) -> "ArtLayer": """Converts the layer to a smart object. Returns: diff --git a/photoshop/api/_artlayers.py b/photoshop/api/_artlayers.py index dddb7fa7..614aaa0b 100644 --- a/photoshop/api/_artlayers.py +++ b/photoshop/api/_artlayers.py @@ -1,72 +1,18 @@ -# Import third-party modules -from comtypes import ArgumentError - # Import local modules from photoshop.api._artlayer import ArtLayer from photoshop.api._core import Photoshop -from photoshop.api.errors import PhotoshopPythonAPIError +from photoshop.api.collections import CollectionOfNamedObjects +from photoshop.api.collections import CollectionOfRemovables +from photoshop.api.collections import CollectionWithAdd # pylint: disable=too-many-public-methods -class ArtLayers(Photoshop): +class ArtLayers( + CollectionOfRemovables[ArtLayer, int | str], + CollectionOfNamedObjects[ArtLayer, int | str], + CollectionWithAdd[ArtLayer, int | str], +): """The collection of art layer objects in the document.""" - def __init__(self, parent): - super().__init__(parent=parent) - self._flag_as_method( - "add", - ) - - @property - def _layers(self): - return list(self.app) - - def __len__(self): - return self.length - - def __iter__(self): - for layer in self.app: - yield layer - - def __getitem__(self, key: str): - """Access a given ArtLayer using dictionary key lookup.""" - try: - return ArtLayer(self.app[key]) - except ArgumentError: - raise PhotoshopPythonAPIError(f'Could not find an artLayer named "{key}"') - - @property - def length(self): - return len(self._layers) - - @property - def parent(self): - return self.app.parent - - @property - def typename(self): - return self.app.typename - - def add(self): - """Adds an element.""" - return ArtLayer(self.app.add()) - - def getByIndex(self, index: int): - """Access ArtLayer using list index lookup.""" - return ArtLayer(self._layers[index]) - - def getByName(self, name: str) -> ArtLayer: - """Get the first element in the collection with the provided name. - - Raises: - PhotoshopPythonAPIError: Could not find a artLayer. - """ - for layer in self.app: - if layer.name == name: - return ArtLayer(layer) - raise PhotoshopPythonAPIError(f'Could not find an artLayer named "{name}"') - - def removeAll(self): - """Deletes all elements.""" - for layer in self.app: - ArtLayer(layer).remove() + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(ArtLayer, parent) diff --git a/photoshop/api/_channel.py b/photoshop/api/_channel.py index ee47309c..d4990a7e 100644 --- a/photoshop/api/_channel.py +++ b/photoshop/api/_channel.py @@ -1,10 +1,20 @@ +# Import built-in modules +from typing import TYPE_CHECKING + # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import ChannelType +from photoshop.api.solid_color import SolidColor + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api._document import Document # pylint: disable=too-many-public-methods class Channel(Photoshop): - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "duplicate", @@ -12,55 +22,54 @@ def __init__(self, parent): ) @property - def color(self): - return self.app.color + def color(self) -> SolidColor: + return SolidColor(self.app.color) @color.setter - def color(self, value): + def color(self, value: SolidColor) -> None: self.app.color = value @property - def histogram(self): + def histogram(self) -> tuple[int, ...]: return self.app.histogram @histogram.setter - def histogram(self, value): + def histogram(self, value: tuple[int, ...]) -> None: self.app.histogram = value @property - def kind(self): - return self.app.kind + def kind(self) -> ChannelType: + return ChannelType(self.app.kind) @kind.setter - def kind(self, value): + def kind(self, value: ChannelType) -> None: self.app.kind = value @property - def opacity(self): + def opacity(self) -> float: return self.app.opacity @opacity.setter - def opacity(self, value): + def opacity(self, value: float) -> None: self.app.opacity = value @property - def visible(self): + def visible(self) -> bool: return self.app.visible @visible.setter - def visible(self, value): + def visible(self, value: bool) -> None: self.app.visible = value @property - def name(self): + def name(self) -> str: return self.app.name - def duplicate(self, targetDocument=None): - self.app.duplicate(targetDocument) + def duplicate(self, targetDocument: "Document | None" = None) -> "Channel": + return Channel(self.app.duplicate(targetDocument)) - def merge(self): + def merge(self) -> None: self.app.merge() - def remove(self): - channel = f'app.activeDocument.channels.getByName("{self.name}")' - self.eval_javascript(f"{channel}.remove()") + def remove(self) -> None: + self.eval_javascript(f'app.activeDocument.channels.getByName("{self.name}").remove()') diff --git a/photoshop/api/_channels.py b/photoshop/api/_channels.py index 00b3b6a2..f41af88e 100644 --- a/photoshop/api/_channels.py +++ b/photoshop/api/_channels.py @@ -1,44 +1,15 @@ # Import local modules from photoshop.api._channel import Channel from photoshop.api._core import Photoshop -from photoshop.api.errors import PhotoshopPythonAPIError - - -# pylint: disable=too-many-public-methods -class Channels(Photoshop): - def __init__(self, parent): - super().__init__(parent=parent) - self._flag_as_method( - "add", - "removeAll", - ) - - @property - def _channels(self): - return list(self.app) - - def __len__(self): - return self.length - - def __iter__(self): - for layer in self.app: - yield layer - - def __getitem__(self, item): - return self.app[item] - - @property - def length(self): - return len(self._channels) - - def add(self): - self.app.add() - - def removeAll(self): - self.app.removeAll() - - def getByName(self, name) -> Channel: - for channel in self._channels: - if channel.name == name: - return Channel(channel) - raise PhotoshopPythonAPIError(f'Could not find a channel named "{name}"') +from photoshop.api.collections import CollectionOfNamedObjects +from photoshop.api.collections import CollectionOfRemovables +from photoshop.api.collections import CollectionWithAdd + + +class Channels( + CollectionWithAdd[Channel, int | str], + CollectionOfRemovables[Channel, int | str], + CollectionOfNamedObjects[Channel, int | str], +): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(Channel, parent=parent) diff --git a/photoshop/api/_core.py b/photoshop/api/_core.py index 28be680a..5079517f 100644 --- a/photoshop/api/_core.py +++ b/photoshop/api/_core.py @@ -1,4 +1,5 @@ """This class provides all photoshop API core functions.""" + # Import built-in modules from contextlib import suppress from functools import cached_property @@ -9,8 +10,7 @@ import os import platform from typing import Any -from typing import List -from typing import Optional +from typing import TYPE_CHECKING import winreg # Import third-party modules @@ -30,7 +30,7 @@ class Photoshop: _reg_path = "SOFTWARE\\Adobe\\Photoshop" object_name: str = "Application" - def __init__(self, ps_version: Optional[str] = None, parent: Any = None): + def __init__(self, ps_version: str | None = None, parent: "Photoshop | None" = None): """ Initialize the Photoshop core object. @@ -40,7 +40,7 @@ def __init__(self, ps_version: Optional[str] = None, parent: Any = None): """ # Establish the initial app and program ID ps_version = os.getenv("PS_VERSION", ps_version) - self._app_id = PHOTOSHOP_VERSION_MAPPINGS.get(ps_version, "") + self._app_id = PHOTOSHOP_VERSION_MAPPINGS.get(ps_version, "") if ps_version else "" self._has_parent, self.adobe, self.app = False, None, None # Store current photoshop version @@ -49,7 +49,7 @@ def __init__(self, ps_version: Optional[str] = None, parent: Any = None): # Establish the application object using provided version ID if self.app_id: - self.app = self._get_application_object([self.app_id]) + self.app: Any = self._get_application_object([self.app_id]) if not self.app: # Attempt unsuccessful self._logger.debug( @@ -70,20 +70,19 @@ def __init__(self, ps_version: Optional[str] = None, parent: Any = None): self.app = parent self._has_parent = True - def __repr__(self): - return self - - def __call__(self, *args, **kwargs): + def __call__(self): return self.app - def __str__(self): + def __str__(self) -> str: return f"{self.__class__.__name__} <{self.program_name}>" - def __getattribute__(self, item): - try: - return super().__getattribute__(item) - except AttributeError: - return getattr(self.app, item) + if not TYPE_CHECKING: + + def __getattribute__(self, name): + try: + return super().__getattribute__(name) + except AttributeError: + return getattr(self.app, name) """ * Debug Logger @@ -141,14 +140,14 @@ def app_id(self) -> str: return self._app_id @app_id.setter - def app_id(self, value: str): + def app_id(self, value: str) -> None: self._app_id = value """ * Private Methods """ - def _flag_as_method(self, *names: str): + def _flag_as_method(self, *names: str) -> None: """ * This is a hack for Photoshop's broken COM implementation. * Photoshop does not implement 'IDispatch::GetTypeInfo', so when @@ -161,7 +160,7 @@ def _flag_as_method(self, *names: str): if isinstance(self.app, FullyDynamicDispatch): self.app._FlagAsMethod(*names) - def _get_photoshop_versions(self) -> List[str]: + def _get_photoshop_versions(self) -> list[str]: """Retrieve a list of Photoshop version ID's from registry.""" with suppress(OSError, IndexError): key = self._open_key(self._reg_path) @@ -172,7 +171,7 @@ def _get_photoshop_versions(self) -> List[str]: self._logger.debug("Unable to find Photoshop version number in HKEY_LOCAL_MACHINE registry!") return [] - def _get_application_object(self, versions: List[str] = None) -> Optional[Dispatch]: + def _get_application_object(self, versions: list[str] | None = None) -> Dispatch | None: """ Try each version string until a valid Photoshop application Dispatch object is returned. @@ -185,10 +184,11 @@ def _get_application_object(self, versions: List[str] = None) -> Optional[Dispat Raises: OSError: If a Dispatch object wasn't resolved. """ - for v in versions: - self.app_id = v - with suppress(OSError): - return CreateObject(self.program_name, dynamic=True) + if versions: + for v in versions: + self.app_id = v + with suppress(OSError): + return CreateObject(self.program_name, dynamic=True) return """ @@ -215,7 +215,11 @@ def get_script_path(self) -> str: def eval_javascript(self, javascript: str, Arguments: Any = None, ExecutionMode: Any = None) -> str: """Instruct the application to execute javascript code.""" executor = self.adobe if self._has_parent else self.app - return executor.doJavaScript(javascript, Arguments, ExecutionMode) + if executor: + return executor.doJavaScript(javascript, Arguments, ExecutionMode) + else: + print("Tried to eval javascript, but executor is not available.") + return "" """ * Private Static Methods diff --git a/photoshop/api/_document.py b/photoshop/api/_document.py index ff8cd36c..54321f66 100644 --- a/photoshop/api/_document.py +++ b/photoshop/api/_document.py @@ -14,12 +14,10 @@ """ # Import built-in modules +from os import PathLike from pathlib import Path -from typing import List -from typing import NoReturn from typing import Optional -from typing import TypeVar -from typing import Union +from typing import TYPE_CHECKING # Import third-party modules from comtypes import COMError @@ -27,23 +25,48 @@ # Import local modules from photoshop.api._artlayer import ArtLayer from photoshop.api._artlayers import ArtLayers +from photoshop.api._channel import Channel from photoshop.api._channels import Channels from photoshop.api._core import Photoshop from photoshop.api._documentinfo import DocumentInfo +from photoshop.api._layer import Layer from photoshop.api._layerComps import LayerComps from photoshop.api._layerSet import LayerSet from photoshop.api._layerSets import LayerSets from photoshop.api._layers import Layers -from photoshop.api._selection import Selection +from photoshop.api.enumerations import AnchorPosition +from photoshop.api.enumerations import BitsPerChannelType +from photoshop.api.enumerations import ChangeMode +from photoshop.api.enumerations import ColorProfileType +from photoshop.api.enumerations import Direction +from photoshop.api.enumerations import DocumentMode from photoshop.api.enumerations import ExportType from photoshop.api.enumerations import ExtensionType +from photoshop.api.enumerations import Intent +from photoshop.api.enumerations import MeasurementSource +from photoshop.api.enumerations import ResampleMethod from photoshop.api.enumerations import SaveOptions +from photoshop.api.enumerations import SourceSpaceType from photoshop.api.enumerations import TrimType +from photoshop.api.path_items import PathItems +from photoshop.api.protocols import HistoryState +from photoshop.api.protocols import MeasurementScale +from photoshop.api.protocols import XMPMetadata from photoshop.api.save_options import ExportOptionsSaveForWeb +from photoshop.api.save_options.bmp import BMPSaveOptions +from photoshop.api.save_options.eps import EPSSaveOptions +from photoshop.api.save_options.gif import GIFSaveOptions +from photoshop.api.save_options.jpg import JPEGSaveOptions +from photoshop.api.save_options.pdf import PDFSaveOptions +from photoshop.api.save_options.png import PNGSaveOptions +from photoshop.api.save_options.psd import PhotoshopSaveOptions +from photoshop.api.save_options.tag import TargaSaveOptions +from photoshop.api.save_options.tif import TiffSaveOptions -# Custom types. -PS_Layer = TypeVar("PS_Layer", LayerSet, ArtLayer) +if TYPE_CHECKING: + # Import local modules + from photoshop.api._selection import Selection # pylint: disable=too-many-public-methods @@ -56,7 +79,7 @@ class Document(Photoshop): object_name = "Application" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "autoCount", @@ -64,6 +87,7 @@ def __init__(self, parent): "close", "convertProfile", "Flatten", + "flipCanvas", "mergeVisibleLayers", "crop", "export", @@ -77,7 +101,9 @@ def __init__(self, parent): "splitChannels", "trap", "trim", + "resizeCanvas", "resizeImage", + "rotateCanvas", ) @property @@ -85,15 +111,18 @@ def artLayers(self) -> ArtLayers: return ArtLayers(self.app.artLayers) @property - def activeLayer(self) -> PS_Layer: + def activeLayer(self) -> ArtLayer | LayerSet: """The selected layer.""" type_ = self.eval_javascript("app.activeDocument.activeLayer.typename") - mappings = {"LayerSet": LayerSet, "ArtLayer": ArtLayer} + mappings: dict[str, type[ArtLayer] | type[LayerSet]] = { + "LayerSet": LayerSet, + "ArtLayer": ArtLayer, + } func = mappings[type_] return func(self.app.activeLayer) @activeLayer.setter - def activeLayer(self, layer) -> NoReturn: + def activeLayer(self, layer: Layer) -> None: """Sets the select layer as active layer. Args: @@ -104,64 +133,64 @@ def activeLayer(self, layer) -> NoReturn: self.app.activeLayer = layer @property - def activeChannels(self): + def activeChannels(self) -> list[Channel]: """The selected channels.""" - return self.app.activeChannels + return [Channel(channel) for channel in self.app.activeChannels] @activeChannels.setter - def activeChannels(self, channels): + def activeChannels(self, channels: list[Channel]) -> None: self.app.activeChannels = channels @property - def activeHistoryBrushSource(self): + def activeHistoryBrushSource(self) -> HistoryState: """The history state to use with the history brush.""" return self.app.activeHistoryBrushSource @property - def activeHistoryState(self): + def activeHistoryState(self) -> HistoryState: """The current history state for this document.""" return self.app.activeHistoryState @activeHistoryState.setter - def activeHistoryState(self, state): + def activeHistoryState(self, state: HistoryState) -> None: self.app.activeHistoryState = state @property - def backgroundLayer(self): + def backgroundLayer(self) -> ArtLayer: """The background layer for the Document.""" - return self.app.backgroundLayer + return ArtLayer(self.app.backgroundLayer) @property - def bitsPerChannel(self): + def bitsPerChannel(self) -> BitsPerChannelType: """The number of bits per channel.""" - return self.app.bitsPerChannel + return BitsPerChannelType(self.app.bitsPerChannel) @bitsPerChannel.setter - def bitsPerChannel(self, value): + def bitsPerChannel(self, value: BitsPerChannelType) -> None: self.app.bitsPerChannel = value @property - def channels(self): + def channels(self) -> Channels: return Channels(self.app.channels) @property - def colorProfileName(self): + def colorProfileName(self) -> str: """The name of the color profile. Valid only when no value is specified for color profile kind (to indicate a custom color profile).""" return self.app.colorProfileName @colorProfileName.setter - def colorProfileName(self, name): + def colorProfileName(self, name: str) -> None: self.app.colorProfileName = name @property - def colorProfileType(self): + def colorProfileType(self) -> ColorProfileType: """The type of color model that defines the working space of the Document.""" - return self.app.colorProfileType + return ColorProfileType(self.app.colorProfileType) @colorProfileType.setter - def colorProfileType(self, profile_type): + def colorProfileType(self, profile_type: ColorProfileType) -> None: self.app.colorProfileType = profile_type @property @@ -170,9 +199,9 @@ def colorSamplers(self): return self.app.colorSamplers @property - def componentChannels(self): + def componentChannels(self) -> list[Channel]: """The color component channels for this Document.""" - return self.app.componentChannels + return [Channel(channel) for channel in self.app.componentChannels] @property def countItems(self): @@ -180,22 +209,22 @@ def countItems(self): return self.app.countItems @property - def fullName(self): + def fullName(self) -> Path | None: """The full path name of the Document.""" try: return Path(self.app.fullName) except COMError: self.eval_javascript( - 'alert ("Please save your Document first!",' '"{}")'.format(self.name), + 'alert ("Please save your Document first!","{}")'.format(self.name), ) @property - def height(self): + def height(self) -> float: """The height of the Document.""" return self.app.Height @property - def histogram(self): + def histogram(self) -> tuple[int, ...]: """A histogram showing the number of pixels at each color intensity level for the composite channel.""" return self.app.Histogram @@ -206,44 +235,44 @@ def history_states(self): return self.app.HistoryStates @property - def id(self): + def id(self) -> int: """The unique ID of this Document.""" return self.app.Id @property - def info(self): + def info(self) -> DocumentInfo: """Metadata about the Document.""" return DocumentInfo(self.app.info) @property - def layerComps(self): + def layerComps(self) -> LayerComps: """The layer comps collection in this Document.""" return LayerComps(self.app.layerComps) @property - def layers(self): + def layers(self) -> Layers: """The layers collection in the Document.""" return Layers(self.app.Layers) @property - def layerSets(self): + def layerSets(self) -> LayerSets: """The layer sets collection in the Document.""" return LayerSets(self.app.layerSets) @property - def managed(self): + def managed(self) -> bool: """If true, the Document is a workgroup Document.""" return self.app.Managed @property - def measurement_scale(self): + def measurement_scale(self) -> MeasurementScale: """The measurement scale of the Document.""" return self.app.MeasurementScale @property - def mode(self): + def mode(self) -> DocumentMode: """The color profile.""" - return self.app.Mode + return DocumentMode(self.app.Mode) @property def name(self) -> str: @@ -251,30 +280,30 @@ def name(self) -> str: return self.app.name @property - def parent(self): + def parent(self) -> object: """The object's container.""" return self.app.Parent @property - def path(self) -> str: + def path(self) -> Path | None: """The path to the Document.""" try: return Path(self.app.path) except COMError: self.eval_javascript( - 'alert ("Please save your Document first!",' '"{}")'.format(self.name), + 'alert ("Please save your Document first!","{}")'.format(self.name), ) @path.setter - def path(self, path: str) -> NoReturn: - self.app.fullName = path + def path(self, path: str | PathLike[str]) -> None: + self.app.fullName = str(path) @property - def pathItems(self): - return self.app.pathItems + def pathItems(self) -> PathItems: + return PathItems(self.app.pathItems) @property - def pixelAspectRatio(self): + def pixelAspectRatio(self) -> float: """The (custom) pixel aspect ratio of the Document. Range: 0.100 to 10.000. @@ -288,29 +317,26 @@ def printSettings(self): return self.app.printSettings @property - def quickMaskMode(self): + def quickMaskMode(self) -> bool: """If true, the document is in Quick Mask mode.""" return self.app.quickMaskMode @property - def saved(self): + def saved(self) -> bool: """If true, the Document been saved since the last change.""" return self.app.Saved @property - def resolution(self): + def resolution(self) -> float: """The resolution of the Document (in pixels per inch)""" return self.app.resolution @property - def selection(self): + def selection(self) -> "Selection": """The selected area of the Document.""" - return Selection(self.app.selection) + from ._selection import Selection - @property - def typename(self): - """The class name of the object.""" - return self.app.typename + return Selection(self.app.selection) @property def cloudDocument(self): @@ -323,45 +349,54 @@ def cloudWorkAreaDirectory(self): return self.app.cloudWorkAreaDirectory @property - def width(self): + def width(self) -> float: return self.app.Width @property - def xmpMetadata(self): + def xmpMetadata(self) -> XMPMetadata: """The XMP properties of the Document. The Camera RAW settings are stored here.""" return self.app.xmpMetadata # Methods - def autoCount(self, *args, **kwargs): + def autoCount(self, channel: Channel, threshold: int) -> None: """Counts the objects in the Document.""" - return self.app.autoCount(*args, **kwargs) + self.app.autoCount(channel, threshold) - def changeMode(self, *args, **kwargs): + def changeMode(self, destinationMode: ChangeMode, options: object) -> None: """Changes the mode of the Document.""" - return self.app.changeMode(*args, **kwargs) - - def close(self, saving=SaveOptions.DoNotSaveChanges): - return self.app.close(saving) + self.app.changeMode(destinationMode, options) - def convertProfile(self): - return self.app.convertProfile() + def close(self, saving: SaveOptions = SaveOptions.DoNotSaveChanges) -> None: + self.app.close(saving) - def flatten(self): + def convertProfile( + self, + destinationProfile: str, + intent: Intent, + blackPointCompensation: bool, + dither: bool, + ) -> None: + self.app.convertProfile(destinationProfile, intent, blackPointCompensation, dither) + + def flatten(self) -> None: """Flattens all layers.""" - return self.app.Flatten() + self.app.Flatten() + + def flipCanvas(self, direction: Direction) -> None: + self.app.flipCanvas(direction) - def mergeVisibleLayers(self): + def mergeVisibleLayers(self) -> None: """Flattens all visible layers in the Document.""" - return self.app.mergeVisibleLayers() + self.app.mergeVisibleLayers() def crop( self, - bounds: List[int], - angle: Optional[float] = None, - width: Optional[int] = None, - height: Optional[int] = None, - ): + bounds: list[int], + angle: float | None = None, + width: int | None = None, + height: int | None = None, + ) -> None: """Crops the document. Args: @@ -371,9 +406,14 @@ def crop( height: The height of the resulting document. """ - return self.app.crop(bounds, angle, width, height) + self.app.crop(bounds, angle, width, height) - def exportDocument(self, file_path: str, exportAs: ExportType, options: Union[ExportOptionsSaveForWeb]): + def exportDocument( + self, + file_path: str, + exportAs: ExportType, + options: ExportOptionsSaveForWeb, + ) -> None: """Exports the Document. Note: @@ -387,51 +427,72 @@ def exportDocument(self, file_path: str, exportAs: ExportType, options: Union[Ex file_path = file_path.replace("\\", "/") self.app.export(file_path, exportAs, options) - def duplicate(self, name=None, merge_layers_only=False): + def duplicate(self, name: str | None = None, merge_layers_only: bool = False) -> "Document": return Document(self.app.duplicate(name, merge_layers_only)) - def paste(self): + def paste(self) -> ArtLayer | LayerSet: """Pastes contents of the clipboard into the Document.""" self.eval_javascript("app.activeDocument.paste()") return self.activeLayer - def print(self): + def print( + self, + sourceSpace: SourceSpaceType, + printSpace: str, + intent: Intent, + blackPointCompensation: bool, + ) -> None: """Prints the document.""" - return self.app.print() + self.app.print(sourceSpace, printSpace, intent, blackPointCompensation) - def printOneCopy(self): + def printOneCopy(self) -> None: self.app.printOneCopy() - def rasterizeAllLayers(self): - return self.app.rasterizeAllLayers() + def rasterizeAllLayers(self) -> None: + self.app.rasterizeAllLayers() - def recordMeasurements(self, source, dataPoints): + def recordMeasurements(self, source: MeasurementSource, dataPoints: str) -> None: """Records the measurements of document.""" self.app.recordMeasurements(source, dataPoints) - def reveal_all(self): + def reveal_all(self) -> None: """Expands the Document to show clipped sections.""" - return self.app.revealAll() + self.app.revealAll() - def save(self): + def save(self) -> None: """Saves the Document.""" - return self.app.save() + self.app.save() - def saveAs(self, file_path, options, asCopy=True, extensionType=ExtensionType.Lowercase): + def saveAs( + self, + file_path: str, + options: BMPSaveOptions + | EPSSaveOptions + | GIFSaveOptions + | JPEGSaveOptions + | PDFSaveOptions + | PNGSaveOptions + | PhotoshopSaveOptions + | TargaSaveOptions + | TiffSaveOptions + | None = None, + asCopy: bool = False, + extensionType: ExtensionType = ExtensionType.Lowercase, + ) -> None: """Saves the documents with the specified save options. Args: - file_path (str): Absolute path of psd file. - options (JPEGSaveOptions): Save options. - asCopy (bool): + file_path: Absolute path of psd file. + options: Save options. + asCopy: Saves the document as a copy, leaving the original open. """ - return self.app.saveAs(file_path, options, asCopy, extensionType) + self.app.saveAs(file_path, options, asCopy, extensionType) - def splitChannels(self): + def splitChannels(self) -> None: """Splits the channels of the document.""" self.app.splitChannels() - def suspendHistory(self, historyString, javaScriptString): + def suspendHistory(self, historyString: str, javaScriptString: str) -> None: """Provides a single history state for the entire script. Allows a single undo for all actions taken in the script. @@ -439,7 +500,7 @@ def suspendHistory(self, historyString, javaScriptString): """ self.eval_javascript(f"app.activeDocument.suspendHistory('{historyString}', '{javaScriptString}')") - def trap(self, width: int): + def trap(self, width: int) -> None: """ Applies trapping to a CMYK document. Valid only when ‘mode’ = CMYK. @@ -454,7 +515,7 @@ def trim( left: Optional[bool] = True, bottom: Optional[bool] = True, right: Optional[bool] = True, - ): + ) -> None: """Trims the transparent area around the image on the specified sides of the canvas. Args: @@ -471,16 +532,35 @@ def trim( right: If true, trims away the right of the document. """ - return self.app.trim(trim_type, top, left, bottom, right) + self.app.trim(trim_type, top, left, bottom, right) + + def resizeCanvas( + self, + width: int, + height: int, + anchor: AnchorPosition = AnchorPosition.MiddleCenter, + ) -> None: + self.app.resizeCanvas(width, height, anchor) - def resizeImage(self, width: int, height: int, resolution: int = 72, automatic: int = 8): + def resizeImage( + self, + width: int, + height: int, + resolution: float = 72, + resampleMethod: ResampleMethod = ResampleMethod.Automatic, + amount: int = 0, + ) -> None: """Changes the size of the image. Args: width: The desired width of the image. height: The desired height of the image. resolution: The resolution (in pixels per inch) - automatic: Value for automatic. + resampleMethod: The downsample method. + amount: Amount of noise value when using preserve details (range: 0 - 100) """ - return self.app.resizeImage(width, height, resolution, automatic) + self.app.resizeImage(width, height, resolution, resampleMethod, amount) + + def rotateCanvas(self, angle: float) -> None: + self.app.rotateCanvas(angle) diff --git a/photoshop/api/_documentinfo.py b/photoshop/api/_documentinfo.py index c24068cd..05f4a1a2 100644 --- a/photoshop/api/_documentinfo.py +++ b/photoshop/api/_documentinfo.py @@ -7,19 +7,22 @@ # Import built-in modules from pprint import pformat +from typing import Sequence # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import CopyrightedType +from photoshop.api.enumerations import Urgency # pylint: disable=too-many-public-methods class DocumentInfo(Photoshop): """Metadata about a document object.""" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) - def __str__(self): + def __str__(self) -> str: return pformat( { "author": self.author, @@ -49,195 +52,195 @@ def __str__(self): ) @property - def author(self): + def author(self) -> str: """str: The author.""" return self.app.author @author.setter - def author(self, name): + def author(self, name: str) -> None: self.app.author = name @property - def authorPosition(self): + def authorPosition(self) -> str: """str:The author’s position.""" return self.app.authorPosition @authorPosition.setter - def authorPosition(self, name): + def authorPosition(self, name: str) -> None: self.app.authorPosition = name @property - def caption(self): + def caption(self) -> str: return self.app.caption @caption.setter - def caption(self, name): + def caption(self, name: str) -> None: self.app.caption = name @property - def captionWriter(self): + def captionWriter(self) -> str: return self.app.captionWriter @captionWriter.setter - def captionWriter(self, name): + def captionWriter(self, name: str) -> None: self.app.captionWriter = name @property - def category(self): + def category(self) -> str: """str: The document category.""" return self.app.category @category.setter - def category(self, name): + def category(self, name: str) -> None: self.app.category = name @property - def city(self): + def city(self) -> str: return self.app.city @city.setter - def city(self, city_name): + def city(self, city_name: str) -> None: self.app.city = city_name @property - def copyrightNotice(self): + def copyrightNotice(self) -> str: """str: The copyright notice.""" return self.app.copyrightNotice @copyrightNotice.setter - def copyrightNotice(self, name): + def copyrightNotice(self, name: str) -> None: self.app.copyrightNotice = name @property - def copyrighted(self): - """str: The copyright status.""" - return self.app.copyrighted + def copyrighted(self) -> CopyrightedType: + """The copyright status.""" + return CopyrightedType(self.app.copyrighted) @copyrighted.setter - def copyrighted(self, info): + def copyrighted(self, info: CopyrightedType) -> None: self.app.copyrighted = info @property - def country(self): + def country(self) -> str: return self.app.country @country.setter - def country(self, name): + def country(self, name: str) -> None: self.app.country = name @property - def creationDate(self): + def creationDate(self) -> str: return self.app.creationDate @creationDate.setter - def creationDate(self, name): + def creationDate(self, name: str) -> None: self.app.creationDate = name @property - def credit(self): + def credit(self) -> str: """str: The author credit.""" return self.app.credit @credit.setter - def credit(self, value): + def credit(self, value: str) -> None: self.app.credit = value @property - def exif(self): + def exif(self) -> tuple[tuple[str, str], ...]: return self.app.exif @exif.setter - def exif(self, info): + def exif(self, info: Sequence[tuple[str, str]]) -> None: self.app.exif = info @property - def headline(self): + def headline(self) -> str: return self.app.headline @headline.setter - def headline(self, value): + def headline(self, value: str) -> None: self.app.headline = value @property - def instructions(self): + def instructions(self) -> str: return self.app.instructions @instructions.setter - def instructions(self, value): + def instructions(self, value: str) -> None: self.app.instructions = value @property - def jobName(self): + def jobName(self) -> str: return self.app.jobName @jobName.setter - def jobName(self, job): + def jobName(self, job: str) -> None: self.app.jobName = job @property - def keywords(self): + def keywords(self) -> tuple[str, ...] | None: return self.app.keywords @keywords.setter - def keywords(self, words): + def keywords(self, words: Sequence[str]) -> None: self.app.keywords = words @property - def ownerUrl(self): + def ownerUrl(self) -> str: return self.app.ownerUrl @ownerUrl.setter - def ownerUrl(self, url): + def ownerUrl(self, url: str) -> None: self.app.ownerUrl = url @property - def provinceState(self): + def provinceState(self) -> str: """str: The state or province.""" return self.app.provinceState @provinceState.setter - def provinceState(self, state_name): + def provinceState(self, state_name: str) -> None: self.app.provinceState = state_name @property - def source(self): + def source(self) -> str: return self.app.source @source.setter - def source(self, source_name): + def source(self, source_name: str) -> None: self.app.source = source_name @property - def supplementalCategories(self): + def supplementalCategories(self) -> tuple[str, ...]: """str: Other categories.""" return self.app.supplementalCategories @supplementalCategories.setter - def supplementalCategories(self, info): + def supplementalCategories(self, info: Sequence[str]) -> None: self.app.supplementalCategories = info @property - def title(self): + def title(self) -> str: return self.app.title @title.setter - def title(self, name): + def title(self, name: str) -> None: self.app.title = name @property - def transmissionReference(self): + def transmissionReference(self) -> str: """str: The transmission reference.""" return self.app.transmissionReference @transmissionReference.setter - def transmissionReference(self, reference): + def transmissionReference(self, reference: str) -> None: self.app.transmissionReference = reference @property - def urgency(self): + def urgency(self) -> Urgency: """The document urgency.""" - return self.app.urgency + return Urgency(self.app.urgency) @urgency.setter - def urgency(self, status): + def urgency(self, status: Urgency) -> None: self.app.urgency = status diff --git a/photoshop/api/_documents.py b/photoshop/api/_documents.py index 365aaefc..86d10d25 100644 --- a/photoshop/api/_documents.py +++ b/photoshop/api/_documents.py @@ -1,41 +1,38 @@ # Import local modules from photoshop.api._core import Photoshop from photoshop.api._document import Document +from photoshop.api.collections import CollectionOfNamedObjects from photoshop.api.enumerations import BitsPerChannelType from photoshop.api.enumerations import DocumentFill from photoshop.api.enumerations import NewDocumentMode -from photoshop.api.errors import PhotoshopPythonAPIError # pylint: disable=too-many-public-methods, too-many-arguments -class Documents(Photoshop): +class Documents(CollectionOfNamedObjects[Document, int | str]): """The collection of open documents.""" - def __init__(self, parent): - super().__init__(parent=parent) + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(Document, parent) self._flag_as_method("add") - def __len__(self) -> int: - return self.length - def add( self, - width: int = 960, - height: int = 540, + width: float = 960, + height: float = 540, resolution: float = 72.0, - name: str = None, - mode: int = NewDocumentMode.NewRGB, - initialFill: int = DocumentFill.White, + name: str | None = None, + mode: NewDocumentMode = NewDocumentMode.NewRGB, + initialFill: DocumentFill = DocumentFill.White, pixelAspectRatio: float = 1.0, - bitsPerChannel: int = BitsPerChannelType.Document8Bits, - colorProfileName: str = None, + bitsPerChannel: BitsPerChannelType = BitsPerChannelType.Document8Bits, + colorProfileName: str | None = None, ) -> Document: """Creates a new document object and adds it to this collections. Args: - width (int): The width of the document. - height (int): The height of the document. - resolution (int): The resolution of the document (in pixels per inch) + width (float): The width of the document. Non-integer values are converted to integers. + height (float): The height of the document. Non-integer values are converted to integers. + resolution (float): The resolution of the document (in pixels per inch) name (str): The name of the document. mode (): The document mode. initialFill : The initial fill of the document. @@ -61,25 +58,3 @@ def add( colorProfileName, ) ) - - def __iter__(self) -> Document: - for doc in self.app: - self.adobe.activeDocument = doc - yield Document(doc) - - def __getitem__(self, item) -> Document: - try: - return Document(self.app[item]) - except IndexError: - raise PhotoshopPythonAPIError("Currently Photoshop did not find Documents.") - - @property - def length(self) -> int: - return len(list(self.app)) - - def getByName(self, document_name: str) -> Document: - """Get document by given document name.""" - for doc in self.app: - if doc.name == document_name: - return Document(doc) - raise PhotoshopPythonAPIError(f'Could not find a document named "{document_name}"') diff --git a/photoshop/api/_layer.py b/photoshop/api/_layer.py new file mode 100644 index 00000000..28c81bfb --- /dev/null +++ b/photoshop/api/_layer.py @@ -0,0 +1,161 @@ +# Import built-in modules +from typing import TYPE_CHECKING + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.enumerations import AnchorPosition +from photoshop.api.enumerations import BlendMode +from photoshop.api.enumerations import ElementPlacement +from photoshop.api.protocols import XMPMetadata + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api._document import Document + from photoshop.api._layerSet import LayerSet + + +class Layer(Photoshop): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) + self._flag_as_method( + "delete", + "duplicate", + "link", + "move", + "moveToEnd", + "resize", + "rotate", + "translate", + "unlink", + ) + + @property + def allLocked(self) -> bool: + return self.app.allLocked + + @allLocked.setter + def allLocked(self, value: bool) -> None: + self.app.allLocked = value + + @property + def blendMode(self) -> BlendMode: + return BlendMode(self.app.blendMode) + + @blendMode.setter + def blendMode(self, mode: BlendMode) -> None: + self.app.blendMode = mode + + @property + def bounds(self) -> tuple[float, float, float, float]: + """The bounding rectangle of the layer.""" + return self.app.bounds + + @property + def boundsNoEffects(self) -> tuple[float, float, float, float]: + """Bounding rectangle of the Layer not including effects.""" + return self.app.boundsNoEffects + + @property + def id(self) -> int: + return self.app.id + + @property + def itemIndex(self) -> int: + return self.app.itemIndex + + @property + def linkedLayers(self) -> list["Layer"]: + """Get all layers linked to this layer. + + Returns: + list: Layer objects""" + return [Layer(layer) for layer in self.app.linkedLayers] + + @property + def name(self) -> str: + return self.app.name + + @name.setter + def name(self, text: str) -> None: + self.app.name = text + + @property + def opacity(self) -> float: + """The layer's master opacity (as a percentage). Range: 0.0 to 100.0.""" + return round(self.app.opacity) + + @opacity.setter + def opacity(self, value: float) -> None: + self.app.opacity = value + + @property + def parent(self) -> "Document | LayerSet": + """The layers's container.""" + parent = self.app.parent + try: + parent.resolution + # Import local modules + from photoshop.api._document import Document + + return Document(parent) + except NameError: + # Import local modules + from photoshop.api._layerSet import LayerSet + + return LayerSet(parent) + + @property + def visible(self) -> bool: + return self.app.visible + + @visible.setter + def visible(self, value: bool) -> None: + self.app.visible = value + + @property + def xmpMetadata(self) -> XMPMetadata: + return self.app.xmpMetadata + + def duplicate( + self, + relativeObject: "Layer | None" = None, + insertionLocation: ElementPlacement | None = None, + ) -> "Layer": + """Duplicates the layer. + + Args: + relativeObject: Layer or LayerSet. + insertionLocation: The location to insert the layer. + + Returns: + Layer: The duplicated layer. + """ + return Layer(self.app.duplicate(relativeObject, insertionLocation)) + + def link(self, with_layer: "Layer") -> None: + self.app.link(with_layer) + + def move(self, relativeObject: "Layer | LayerSet", insertionLocation: ElementPlacement) -> None: + self.app.move(relativeObject, insertionLocation) + + def moveToEnd(self, layer_set: "LayerSet") -> None: + self.app.moveToEnd(layer_set) + + def remove(self) -> None: + """Removes this layer from the document.""" + self.app.delete() + + def resize(self, horizontal: float, vertical: float, anchor: AnchorPosition) -> None: + """Scales the object.""" + self.app.resize(horizontal, vertical, anchor) + + def rotate(self, angle: float, anchor: AnchorPosition = AnchorPosition.MiddleCenter) -> None: + return self.app.rotate(angle, anchor) + + def translate(self, deltaX: float, deltaY: float) -> None: + return self.app.translate(deltaX, deltaY) + + def unlink(self) -> None: + """Unlink this layer from any linked layers.""" + self.app.unlink() diff --git a/photoshop/api/_layerComp.py b/photoshop/api/_layerComp.py index 79e60049..a9af259b 100644 --- a/photoshop/api/_layerComp.py +++ b/photoshop/api/_layerComp.py @@ -1,11 +1,19 @@ +# Import built-in modules +from typing import TYPE_CHECKING + # Import local modules from photoshop.api._core import Photoshop +if TYPE_CHECKING: + # Import local modules + from photoshop.api._document import Document + + class LayerComp(Photoshop): """A snapshot of a state of the layers in a document (can be used to view different page layouts or compostions).""" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "apply", @@ -14,87 +22,82 @@ def __init__(self, parent): "resetfromComp", ) - def __len__(self): - return self.length - @property - def appearance(self): + def appearance(self) -> bool: return self.app.appearance @appearance.setter - def appearance(self, value): + def appearance(self, value: bool) -> None: self.app.appearance = value @property - def childLayerCompState(self): + def childLayerCompState(self) -> bool: return self.app.childLayerCompState @childLayerCompState.setter - def childLayerCompState(self, value): + def childLayerCompState(self, value: bool) -> None: self.app.childLayerCompState = value @property - def comment(self): + def comment(self) -> str: return self.app.comment @comment.setter - def comment(self, text): + def comment(self, text: str) -> None: self.app.comment = text @property - def name(self): + def name(self) -> str: return self.app.name @name.setter - def name(self, text): + def name(self, text: str) -> None: self.app.name = text @property - def parent(self): - return self.app.parent + def parent(self) -> "Document": + from ._document import Document + + return Document(self.app.parent) @property - def position(self): + def position(self) -> bool: return self.app.position @position.setter - def position(self, value): + def position(self, value: bool) -> None: self.app.position = value @property - def selected(self): + def selected(self) -> bool: """True if the layer comp is currently selected.""" return self.app.selected @selected.setter - def selected(self, value): + def selected(self, value: bool) -> None: self.app.selected = value @property - def typename(self): - return self.app.typename - - @property - def visibility(self): + def visibility(self) -> bool: """True to use layer visibility settings.""" return self.app.visibility @visibility.setter - def visibility(self, value): + def visibility(self, value: bool) -> None: self.app.visibility = value - def apply(self): + def apply(self) -> None: """Applies the layer comp to the document.""" self.app.apply() - def recapture(self): + def recapture(self) -> None: """Recaptures the current layer state(s) for this layer comp.""" self.app.recapture() - def remove(self): + def remove(self) -> None: """Deletes the layerComp object.""" self.app.remove() - def resetfromComp(self): + def resetfromComp(self) -> None: """Resets the layer comp state to thedocument state.""" self.app.resetfromComp() diff --git a/photoshop/api/_layerComps.py b/photoshop/api/_layerComps.py index e57dcccf..e453a8d2 100644 --- a/photoshop/api/_layerComps.py +++ b/photoshop/api/_layerComps.py @@ -1,58 +1,27 @@ # Import local modules from photoshop.api._core import Photoshop from photoshop.api._layerComp import LayerComp -from photoshop.api.errors import PhotoshopPythonAPIError +from photoshop.api.collections import CollectionOfNamedObjects +from photoshop.api.collections import CollectionOfRemovables -class LayerComps(Photoshop): +class LayerComps( + CollectionOfRemovables[LayerComp, int | str], + CollectionOfNamedObjects[LayerComp, int | str], +): """The layer comps collection in this document.""" - def __init__(self, parent): - super().__init__(parent=parent) - self._flag_as_method( - "add", - "removeAll", - ) - - def __len__(self): - return self.length - - @property - def length(self): - return len(self._layers) - - @property - def _layers(self): - return list(self.app) - - @property - def parent(self): - return self.app.parent - - @property - def typename(self): - return self.app.typename + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(LayerComp, parent=parent) + self._flag_as_method("add") def add( self, - name, - comment="No Comment.", - appearance=True, - position=True, - visibility=True, - childLayerCompStat=False, - ): + name: str, + comment: str = "No Comment.", + appearance: bool = True, + position: bool = True, + visibility: bool = True, + childLayerCompStat: bool = False, + ) -> LayerComp: return LayerComp(self.app.add(name, comment, appearance, position, visibility, childLayerCompStat)) - - def getByName(self, name): - for layer in self._layers: - if layer.name == name: - return LayerComp(layer) - raise PhotoshopPythonAPIError(f'Could not find a layer named "{name}"') - - def removeAll(self): - self.app.removeAll() - - def __iter__(self): - for layer in self._layers: - yield LayerComp(layer) diff --git a/photoshop/api/_layerSet.py b/photoshop/api/_layerSet.py index 3351da61..29e35bb6 100644 --- a/photoshop/api/_layerSet.py +++ b/photoshop/api/_layerSet.py @@ -1,144 +1,78 @@ +# Import built-in modules +from typing import Iterator +from typing import TYPE_CHECKING + # Import local modules from photoshop.api._artlayer import ArtLayer from photoshop.api._artlayers import ArtLayers +from photoshop.api._channel import Channel +from photoshop.api._channels import Channels from photoshop.api._core import Photoshop -from photoshop.api._layers import Layers -from photoshop.api.enumerations import AnchorPosition -from photoshop.api.enumerations import BlendMode +from photoshop.api._layer import Layer +from photoshop.api.enumerations import ElementPlacement + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api._layerSets import LayerSets + from photoshop.api._layers import Layers -class LayerSet(Photoshop): +class LayerSet(Layer): """A group of layer objects, which can include art layer objects and other (nested) layer set objects. A single command or set of commands manipulates all layers in a layer set object. """ - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( - "merge", - "duplicate", "add", - "delete", - "link", - "move", - "resize", - "rotate", - "translate", - "unlink", + "merge", ) @property - def allLocked(self): - return self.app.allLocked - - @allLocked.setter - def allLocked(self, value): - self.app.allLocked = value - - @property - def artLayers(self): + def artLayers(self) -> ArtLayers: return ArtLayers(self.app.artLayers) @property - def blendMode(self): - return BlendMode(self.app.blendMode) - - @property - def bounds(self): - """The bounding rectangle of the layer set.""" - return self.app.bounds - - @property - def enabledChannels(self): - return self.app.enabledChannels + def enabledChannels(self) -> Channels: + return Channels(self.app.enabledChannels) @enabledChannels.setter - def enabledChannels(self, value): + def enabledChannels(self, value: list[Channel] | Channels) -> None: self.app.enabledChannels = value @property - def layers(self): + def layers(self) -> "Layers": + # pylint: disable=import-outside-toplevel + from ._layers import Layers + return Layers(self.app.layers) @property - def layerSets(self): + def layerSets(self) -> "LayerSets": # pylint: disable=import-outside-toplevel from ._layerSets import LayerSets return LayerSets(self.app.layerSets) - @property - def linkedLayers(self): - """The layers linked to this layerSet object.""" - return self.app.linkedLayers or [] - - @property - def name(self) -> str: - return self.app.name - - @name.setter - def name(self, value): - """The name of this layer set.""" - self.app.name = value - - @property - def opacity(self): - """The master opacity of the set.""" - return round(self.app.opacity) - - @opacity.setter - def opacity(self, value): - self.app.opacity = value - - @property - def parent(self): - return self.app.parent - - @property - def visible(self): - return self.app.visible - - @visible.setter - def visible(self, value): - self.app.visible = value - - def duplicate(self, relativeObject=None, insertionLocation=None): + def duplicate( + self, + relativeObject: Layer | None = None, + insertionLocation: ElementPlacement | None = None, + ): return LayerSet(self.app.duplicate(relativeObject, insertionLocation)) - def link(self, with_layer): - self.app.link(with_layer) - - def add(self): + def add(self) -> "LayerSet": """Adds an element.""" - self.app.add() + return LayerSet(self.app.add()) def merge(self) -> ArtLayer: """Merges the layer set.""" return ArtLayer(self.app.merge()) - def move(self, relativeObject, insertionLocation): - self.app.move(relativeObject, insertionLocation) - - def remove(self): - """Remove this layer set from the document.""" - self.app.delete() - - def resize(self, horizontal=None, vertical=None, anchor: AnchorPosition = None): - self.app.resize(horizontal, vertical, anchor) - - def rotate(self, angle, anchor=None): - self.app.rotate(angle, anchor) - - def translate(self, delta_x, delta_y): - """Moves the position relative to its current position.""" - self.app.translate(delta_x, delta_y) - - def unlink(self): - """Unlinks the layer set.""" - self.app.unlink() - - def __iter__(self): - for layer in self.app: + def __iter__(self) -> Iterator[Layer]: + for layer in self.layers: yield layer diff --git a/photoshop/api/_layerSets.py b/photoshop/api/_layerSets.py index 8b5b3aef..98db50eb 100644 --- a/photoshop/api/_layerSets.py +++ b/photoshop/api/_layerSets.py @@ -1,62 +1,17 @@ -# Import third-party modules -from comtypes import ArgumentError - # Import local modules from photoshop.api._core import Photoshop from photoshop.api._layerSet import LayerSet -from photoshop.api.errors import PhotoshopPythonAPIError +from photoshop.api.collections import CollectionOfNamedObjects +from photoshop.api.collections import CollectionOfRemovables +from photoshop.api.collections import CollectionWithAdd -class LayerSets(Photoshop): +class LayerSets( + CollectionWithAdd[LayerSet, int | str], + CollectionOfRemovables[LayerSet, int | str], + CollectionOfNamedObjects[LayerSet, int | str], +): """The layer sets collection in the document.""" - def __init__(self, parent): - super().__init__(parent=parent) - self._flag_as_method( - "add", - "item", - "removeAll", - ) - - def __len__(self): - return self.length - - def __iter__(self): - for layer_set in self.app: - yield layer_set - - def __getitem__(self, key: str): - """Access a given LayerSet using dictionary key lookup.""" - try: - return LayerSet(self.app[key]) - except ArgumentError: - raise PhotoshopPythonAPIError(f'Could not find a LayerSet named "{key}"') - - @property - def _layerSets(self): - return list(self.app) - - @property - def length(self) -> int: - """Number of elements in the collection.""" - return len(self._layerSets) - - def add(self): - return LayerSet(self.app.add()) - - def item(self, index: int) -> LayerSet: - return LayerSet(self.app.item(index)) - - def removeAll(self): - self.app.removeAll() - - def getByIndex(self, index: int): - """Access LayerSet using list index lookup.""" - return LayerSet(self._layerSets[index]) - - def getByName(self, name: str) -> LayerSet: - """Get the first element in the collection with the provided name.""" - for layer in self.app: - if name == layer.name: - return LayerSet(layer) - raise PhotoshopPythonAPIError(f'Could not find a LayerSet named "{name}"') + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(LayerSet, parent) diff --git a/photoshop/api/_layers.py b/photoshop/api/_layers.py index dfe1d126..fe96aff1 100644 --- a/photoshop/api/_layers.py +++ b/photoshop/api/_layers.py @@ -1,50 +1,56 @@ +# Import built-in modules +from typing import Any +from typing import Iterator + # Import local modules from photoshop.api._artlayer import ArtLayer from photoshop.api._core import Photoshop -from photoshop.api.errors import PhotoshopPythonAPIError +from photoshop.api._layer import Layer +from photoshop.api._layerSet import LayerSet # pylint: disable=too-many-public-methods class Layers(Photoshop): """The layers collection in the document.""" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) - self._flag_as_method( - "add", - "item", - ) + self._flag_as_method("add", "item", "removeAll") - @property - def _layers(self): - return list(self.app) + def _get_appropriate_layer(self, layer: Any) -> ArtLayer | LayerSet: + try: + layer.layers + return LayerSet(layer) + except NameError: + return ArtLayer(layer) - def __len__(self): + def __len__(self) -> int: return self.length - def __getitem__(self, key): - item = self._layers[key] - return ArtLayer(item) + def __getitem__(self, key: int) -> ArtLayer | LayerSet: + for idx, layer in enumerate(self.app): + if idx == key: + return self._get_appropriate_layer(layer) + raise KeyError(f"Key '{key}' was not found in {type(self)}.") @property - def length(self): - return len(self._layers) + def length(self) -> int: + return len(list(self.app)) - def removeAll(self): + def removeAll(self) -> None: """Deletes all elements.""" - for layer in self.app: - ArtLayer(layer).remove() + self.app.removeAll() - def item(self, index): - return ArtLayer(self.app.item(index)) + def item(self, index: int) -> Layer: + return self[index] - def __iter__(self): - for layer in self._layers: - yield ArtLayer(layer) + def __iter__(self) -> Iterator[ArtLayer | LayerSet]: + for layer in self.app: + yield self._get_appropriate_layer(layer) - def getByName(self, name: str) -> ArtLayer: + def getByName(self, name: str) -> ArtLayer | LayerSet | None: """Get the first element in the collection with the provided name.""" for layer in self.app: if layer.name == name: - return ArtLayer(layer) - raise PhotoshopPythonAPIError("X") + return self._get_appropriate_layer(layer) + return None diff --git a/photoshop/api/_measurement_log.py b/photoshop/api/_measurement_log.py index 1e2d9720..09201412 100644 --- a/photoshop/api/_measurement_log.py +++ b/photoshop/api/_measurement_log.py @@ -1,21 +1,30 @@ +# Import built-in modules +from typing import Sequence + # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import MeasurementRange class MeasurementLog(Photoshop): """The log of measurements taken.""" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "exportMeasurements", "deleteMeasurements", ) - def exportMeasurements(self, file_path: str, range_: int = None, data_point=None): + def exportMeasurements( + self, + file_path: str, + range_: MeasurementRange | None = None, + data_point: Sequence[str] | None = None, + ): if data_point is None: data_point = [] self.app.exportMeasurements(file_path, range_, data_point) - def deleteMeasurements(self, range_: int): + def deleteMeasurements(self, range_: MeasurementRange) -> None: self.app.deleteMeasurements(range_) diff --git a/photoshop/api/_notifier.py b/photoshop/api/_notifier.py index e0f651de..349d96c9 100644 --- a/photoshop/api/_notifier.py +++ b/photoshop/api/_notifier.py @@ -5,6 +5,7 @@ Notifiers must be enabled using the Application.notifiersEnabled property """ + # Import built-in modules from pathlib import Path @@ -13,19 +14,19 @@ class Notifier(Photoshop): - def __init__(self, parent=None): - super().__init__() + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) self._flag_as_method( "remove", ) @property - def event(self): + def event(self) -> str: """The event identifier, a four-character code or a unique string.""" return self.app.event @property - def eventClass(self): + def eventClass(self) -> str: """The class identifier, a four-character code or a unique string. When an event applies to multiple types of objects, use this @@ -42,7 +43,7 @@ def eventFile(self) -> Path: activates the notifier.""" return Path(self.app.eventFile) - def remove(self): + def remove(self) -> None: """Deletes this object. You can also remove a Notifier object @@ -53,4 +54,4 @@ def remove(self): Photoshop CC help for more information. """ - return self.app.remove() + self.app.remove() diff --git a/photoshop/api/_notifiers.py b/photoshop/api/_notifiers.py index 3ecbcdc3..c6a6f6ae 100644 --- a/photoshop/api/_notifiers.py +++ b/photoshop/api/_notifiers.py @@ -11,46 +11,31 @@ """ # Import built-in modules -from typing import Any -from typing import Optional +from typing import TYPE_CHECKING # Import local modules from photoshop.api._core import Photoshop from photoshop.api._notifier import Notifier +from photoshop.api.collections import CollectionOfRemovables -class Notifiers(Photoshop): +class Notifiers(CollectionOfRemovables[Notifier, int]): """The `notifiers` currently configured (in the Scripts Events Manager menu in the application).""" - def __init__(self, parent: Optional[Any] = None): - super().__init__(parent=parent) - self._flag_as_method( - "add", - "removeAll", - ) + if TYPE_CHECKING: + # Import local modules + from photoshop.api.application import Application - @property - def _notifiers(self) -> list: - return [n for n in self.app] + parent: Application - def __len__(self): - return self.length + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(type=Notifier, parent=parent) + self._flag_as_method("add") - def __iter__(self): - for app in self.app: - yield app - - def __getitem__(self, item): - return self._notifiers[item] - - @property - def length(self): - return len(self._notifiers) - - def add(self, event, event_file: Optional[Any] = None, event_class: Optional[Any] = None) -> Notifier: + def add(self, event: str, event_file: str, event_class: str | None = None) -> Notifier: self.parent.notifiersEnabled = True return Notifier(self.app.add(event, event_file, event_class)) - def removeAll(self): + def removeAll(self) -> None: self.app.removeAll() self.parent.notifiersEnabled = False diff --git a/photoshop/api/_preferences.py b/photoshop/api/_preferences.py index c4d57356..efab3008 100644 --- a/photoshop/api/_preferences.py +++ b/photoshop/api/_preferences.py @@ -1,36 +1,65 @@ # Import built-in modules +from os import PathLike from pathlib import Path # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import ColorPicker +from photoshop.api.enumerations import EditLogItemsType +from photoshop.api.enumerations import FontPreviewType +from photoshop.api.enumerations import FontSize +from photoshop.api.enumerations import GridLineStyle +from photoshop.api.enumerations import GridSize +from photoshop.api.enumerations import GuideLineStyle +from photoshop.api.enumerations import OtherPaintingCursors +from photoshop.api.enumerations import PaintingCursors +from photoshop.api.enumerations import PointType +from photoshop.api.enumerations import QueryStateType +from photoshop.api.enumerations import ResampleMethod +from photoshop.api.enumerations import SaveBehavior +from photoshop.api.enumerations import TypeUnits +from photoshop.api.enumerations import Units class Preferences(Photoshop): """The application preference settings.""" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) @property - def additionalPluginFolder(self): + def additionalPluginFolder(self) -> Path: """The path to an additional plug-in folder.""" return Path(self.app.additionalPluginFolder) + @additionalPluginFolder.setter + def additionalPluginFolder(self, value: str | PathLike[str]) -> None: + """The path to an additional plug-in folder.""" + self.app.additionalPluginFolder = str(value) + @property - def appendExtension(self): - return self.app.appendExtension + def appendExtension(self) -> SaveBehavior: + return SaveBehavior(self.app.appendExtension) + + @appendExtension.setter + def appendExtension(self, value: SaveBehavior) -> None: + self.app.appendExtension = value @property def askBeforeSavingLayeredTIFF(self) -> bool: return self.app.askBeforeSavingLayeredTIFF + @askBeforeSavingLayeredTIFF.setter + def askBeforeSavingLayeredTIFF(self, value: bool) -> None: + self.app.askBeforeSavingLayeredTIFF = value + @property def autoUpdateOpenDocuments(self) -> bool: """True to automatically update open documents.""" return self.app.autoUpdateOpenDocuments @autoUpdateOpenDocuments.setter - def autoUpdateOpenDocuments(self, boolean: bool): + def autoUpdateOpenDocuments(self, boolean: bool) -> None: """True to automatically update open documents.""" self.app.autoUpdateOpenDocuments = boolean @@ -40,109 +69,378 @@ def beepWhenDone(self) -> bool: return self.app.beepWhenDone @beepWhenDone.setter - def beepWhenDone(self, boolean): + def beepWhenDone(self, boolean: bool) -> None: self.app.beepWhenDone = boolean @property - def colorChannelsInColor(self): + def colorChannelsInColor(self) -> bool: """True to display component channels in the Channels palette in color.""" return self.app.colorChannelsInColor @colorChannelsInColor.setter - def colorChannelsInColor(self, value): + def colorChannelsInColor(self, value: bool) -> None: self.app.colorChannelsInColor = value @property - def colorPicker(self): + def colorPicker(self) -> ColorPicker: """The preferred color selection tool.""" - return self.app.colorPicker + return ColorPicker(self.app.colorPicker) @colorPicker.setter - def colorPicker(self, value): + def colorPicker(self, value: ColorPicker) -> None: self.app.colorPicker = value @property - def columnGutter(self): + def columnGutter(self) -> float: return self.app.columnGutter @columnGutter.setter - def columnGutter(self, value): + def columnGutter(self, value: float) -> None: self.app.columnGutter = value @property - def columnWidth(self): + def columnWidth(self) -> float: return self.app.columnWidth @columnWidth.setter - def columnWidth(self, value): + def columnWidth(self, value: float) -> None: self.app.columnWidth = value @property - def createFirstSnapshot(self): + def createFirstSnapshot(self) -> bool: """Automatically make the first snapshot when a new document is created.""" return self.app.createFirstSnapshot @createFirstSnapshot.setter - def createFirstSnapshot(self, boolean): + def createFirstSnapshot(self, boolean: bool) -> None: self.app.createFirstSnapshot = boolean @property - def dynamicColorSliders(self): + def dynamicColorSliders(self) -> bool: return self.app.dynamicColorSliders @dynamicColorSliders.setter - def dynamicColorSliders(self, boolean): + def dynamicColorSliders(self, boolean: bool) -> None: self.app.dynamicColorSliders = boolean @property - def editLogItems(self) -> bool: + def editLogItems(self) -> EditLogItemsType: """The preferred level of detail in the history log.""" - return self.app.editLogItems + return EditLogItemsType(self.app.editLogItems) @editLogItems.setter - def editLogItems(self, boolean: bool): + def editLogItems(self, value: EditLogItemsType) -> None: """The preferred level of detail in the history log. Valid only when useHistoryLog = True """ - self.app.editLogItems = boolean + self.app.editLogItems = value @property - def exportClipboard(self): + def exportClipboard(self) -> bool: """Retain Photoshop contents on the clipboard after exit the app.""" return self.app.exportClipboard @exportClipboard.setter - def exportClipboard(self, boolean: bool): + def exportClipboard(self, boolean: bool) -> None: self.app.exportClipboard = boolean @property - def fontPreviewSize(self): - return self.app.fontPreviewSize + def fontPreviewSize(self) -> FontPreviewType: + return FontPreviewType(self.app.fontPreviewSize) @fontPreviewSize.setter - def fontPreviewSize(self, value): + def fontPreviewSize(self, value: FontPreviewType) -> None: self.app.fontPreviewSize = value @property - def fullSizePreview(self): + def fullSizePreview(self) -> bool: return self.app.fullSizePreview @fullSizePreview.setter - def fullSizePreview(self, value): + def fullSizePreview(self, value: bool) -> None: self.app.fullSizePreview = value @property - def gamutWarningOpacity(self): + def gamutWarningOpacity(self) -> float: return self.app.gamutWarningOpacity + @gamutWarningOpacity.setter + def gamutWarningOpacity(self, value: float) -> None: + self.app.gamutWarningOpacity = value + + @property + def gridSize(self) -> GridSize: + return GridSize(self.app.gridSize) + + @gridSize.setter + def gridSize(self, value: GridSize) -> None: + self.app.gridSize = value + + @property + def gridStyle(self) -> GridLineStyle: + return GridLineStyle(self.app.gridStyle) + + @gridStyle.setter + def gridStyle(self, value: GridLineStyle) -> None: + self.app.gridStyle = value + + @property + def gridSubDivisions(self) -> int: + return self.app.gridSubDivisions + + @gridSubDivisions.setter + def gridSubDivisions(self, value: int) -> None: + self.app.gridSubDivisions = value + + @property + def guideStyle(self) -> GuideLineStyle: + return GuideLineStyle(self.app.guideStyle) + + @guideStyle.setter + def guideStyle(self, value: GuideLineStyle) -> None: + self.app.guideStyle = value + + @property + def iconPreview(self) -> bool: + return self.app.iconPreview + + @iconPreview.setter + def iconPreview(self, value: bool) -> None: + self.app.iconPreview = value + + @property + def imageCacheLevels(self) -> int: + return self.app.imageCacheLevels + + @imageCacheLevels.setter + def imageCacheLevels(self, value: int) -> None: + self.app.imageCacheLevels = value + + @property + def imagePreviews(self) -> SaveBehavior: + return SaveBehavior(self.app.imagePreviews) + + @imagePreviews.setter + def imagePreviews(self, value: SaveBehavior) -> None: + self.app.imagePreviews = value + + @property + def interpolation(self) -> ResampleMethod: + return ResampleMethod(self.app.interpolation) + + @interpolation.setter + def interpolation(self, value: ResampleMethod) -> None: + self.app.interpolation = value + + @property + def keyboardZoomResizesWindows(self) -> bool: + return self.app.keyboardZoomResizesWindows + + @keyboardZoomResizesWindows.setter + def keyboardZoomResizesWindows(self, value: bool) -> None: + self.app.keyboardZoomResizesWindows = value + + @property + def maximizeCompatibility(self) -> QueryStateType: + return QueryStateType(self.app.maximizeCompatibility) + + @maximizeCompatibility.setter + def maximizeCompatibility(self, value: QueryStateType) -> None: + self.app.maximizeCompatibility = value + + @property + def maxRAMuse(self) -> int: + return self.app.maxRAMuse + + @maxRAMuse.setter + def maxRAMuse(self, value: int) -> None: + self.app.maxRAMuse = value + + @property + def nonLinearHistory(self) -> bool: + return self.app.nonLinearHistory + + @nonLinearHistory.setter + def nonLinearHistory(self, value: bool) -> None: + self.app.nonLinearHistory = value + + @property + def numberofHistoryStates(self) -> int: + return self.app.numberofHistoryStates + + @numberofHistoryStates.setter + def numberofHistoryStates(self, value: int) -> None: + self.app.numberofHistoryStates = value + + @property + def otherCursors(self) -> OtherPaintingCursors: + return OtherPaintingCursors(self.app.otherCursors) + + @otherCursors.setter + def otherCursors(self, value: OtherPaintingCursors) -> None: + self.app.otherCursors = value + + @property + def paintingCursors(self) -> PaintingCursors: + return PaintingCursors(self.app.paintingCursors) + + @paintingCursors.setter + def paintingCursors(self, value: PaintingCursors) -> None: + self.app.paintingCursors = value + + @property + def pixelDoubling(self) -> bool: + return self.app.pixelDoubling + + @pixelDoubling.setter + def pixelDoubling(self, value: bool) -> None: + self.app.pixelDoubling = value + + @property + def pointSize(self) -> PointType: + return PointType(self.app.pointSize) + + @pointSize.setter + def pointSize(self, value: PointType) -> None: + self.app.pointSize = value + + @property + def recentFileListLength(self) -> int: + return self.app.recentFileListLength + + @recentFileListLength.setter + def recentFileListLength(self, value: int) -> None: + self.app.recentFileListLength = value + @property - def rulerUnits(self): - return self.app.rulerUnits + def rulerUnits(self) -> Units: + return Units(self.app.rulerUnits) @rulerUnits.setter - def rulerUnits(self, value): + def rulerUnits(self, value: Units) -> None: self.app.rulerUnits = value + + @property + def saveLogItems(self) -> Path: + return Path(self.app.saveLogItems) + + @saveLogItems.setter + def saveLogItems(self, value: str | PathLike[str]) -> None: + self.app.saveLogItems = str(value) + + @property + def savePaletteLocations(self) -> bool: + return self.app.savePaletteLocations + + @savePaletteLocations.setter + def savePaletteLocations(self, value: bool) -> None: + self.app.savePaletteLocations = value + + @property + def showAsianTextOptions(self) -> bool: + return self.app.showAsianTextOptions + + @showAsianTextOptions.setter + def showAsianTextOptions(self, value: bool) -> None: + self.app.showAsianTextOptions = value + + @property + def showEnglishFontNames(self) -> bool: + return self.app.showEnglishFontNames + + @showEnglishFontNames.setter + def showEnglishFontNames(self, value: bool) -> None: + self.app.showEnglishFontNames = value + + @property + def showSliceNumber(self) -> bool: + return self.app.showSliceNumber + + @showSliceNumber.setter + def showSliceNumber(self, value: bool) -> None: + self.app.showSliceNumber = value + + @property + def showToolTips(self) -> bool: + return self.app.showToolTips + + @showToolTips.setter + def showToolTips(self, value: bool) -> None: + self.app.showToolTips = value + + @property + def smartQuotes(self) -> bool: + return self.app.smartQuotes + + @smartQuotes.setter + def smartQuotes(self, value: bool) -> None: + self.app.smartQuotes = value + + # textFontSize doesn't seem to be accessible via the COM API + @property + def textFontSize(self) -> FontSize: + return FontSize(self.eval_javascript("app.preferences.textFontSize")) + + @textFontSize.setter + def textFontSize(self, value: FontSize) -> None: + self.eval_javascript(f"app.preferences.textFontSize = {FontSize(value)}") + + @property + def typeUnits(self) -> TypeUnits: + return TypeUnits(self.app.typeUnits) + + @typeUnits.setter + def typeUnits(self, value: TypeUnits) -> None: + self.app.typeUnits = value + + @property + def useAdditionalPluginFolder(self) -> bool: + return self.app.useAdditionalPluginFolder + + @useAdditionalPluginFolder.setter + def useAdditionalPluginFolder(self, value: bool) -> None: + self.app.useAdditionalPluginFolder = value + + @property + def useHistoryLog(self) -> bool: + return self.app.useHistoryLog + + @useHistoryLog.setter + def useHistoryLog(self, value: bool) -> None: + self.app.useHistoryLog = value + + @property + def useLowerCaseExtension(self) -> bool: + return self.app.useLowerCaseExtension + + @useLowerCaseExtension.setter + def useLowerCaseExtension(self, value: bool) -> None: + self.app.useLowerCaseExtension = value + + @property + def useShiftKeyForToolSwitch(self) -> bool: + return self.app.useShiftKeyForToolSwitch + + @useShiftKeyForToolSwitch.setter + def useShiftKeyForToolSwitch(self, value: bool) -> None: + self.app.useShiftKeyForToolSwitch = value + + @property + def useVideoAlpha(self) -> bool: + return self.app.useVideoAlpha + + @useVideoAlpha.setter + def useVideoAlpha(self, value: bool) -> None: + self.app.useVideoAlpha = value + + @property + def windowsThumbnail(self) -> bool: + return self.app.windowsThumbnail + + @windowsThumbnail.setter + def windowsThumbnail(self, value: bool) -> None: + self.app.windowsThumbnail = value diff --git a/photoshop/api/_selection.py b/photoshop/api/_selection.py index 63e19e14..99f746e5 100644 --- a/photoshop/api/_selection.py +++ b/photoshop/api/_selection.py @@ -1,9 +1,13 @@ """The selected area of the document or layer.""" # Import local modules +from photoshop.api._channel import Channel from photoshop.api._core import Photoshop +from photoshop.api._document import Document +from photoshop.api.enumerations import AnchorPosition from photoshop.api.enumerations import ColorBlendMode from photoshop.api.enumerations import SelectionType +from photoshop.api.enumerations import StrokeLocation from photoshop.api.solid_color import SolidColor @@ -11,7 +15,7 @@ class Selection(Photoshop): """The selected area of the document.""" - def __init__(self, parent=None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "clear", @@ -32,6 +36,7 @@ def __init__(self, parent=None): "rotateBoundary", "select", "selectBorder", + "selectAll", "similar", "smooth", "store", @@ -41,44 +46,51 @@ def __init__(self, parent=None): ) @property - def bounds(self): + def bounds(self) -> tuple[float, float, float, float]: return self.app.bounds - def parent(self): - return self.app.parent + def parent(self) -> Document: + return Document(self.app.parent) @property - def solid(self): + def solid(self) -> bool: return self.app.solid - @property - def typename(self): - return self.app.typename - - def clear(self): + def clear(self) -> None: """Clears the selection and does not copy it to the clipboard.""" self.app.clear() - def contract(self, contract_by): + def contract(self, contract_by: float) -> None: """Contracts the selection.""" self.app.contract(contract_by) - def copy(self): + def copy(self, merge: bool = False) -> None: """Copies the selection to the clipboard.""" - self.app.copy() + self.app.copy(merge) - def cut(self): - """Cuts the current selection to the clipboard.""" + def cut(self) -> None: + """Clears the current selection and copies it to the clipboard.""" self.app.cut() - def select(self, *args, **kwargs): - return self.app.select(*args, **kwargs) - - def deselect(self): + def select( + self, + region: tuple[ + tuple[float, float], + tuple[float, float], + tuple[float, float], + tuple[float, float], + ], + selection_type: SelectionType | None = None, + feather: float = 0, + anti_alias: bool = True, + ) -> None: + self.app.select(region, selection_type, feather, anti_alias) + + def deselect(self) -> None: """Deselects the current selection.""" - return self.app.deselect() + self.app.deselect() - def expand(self, by: int): + def expand(self, by: float) -> None: """Expands the selection. Args: @@ -87,26 +99,26 @@ def expand(self, by: int): """ self.app.expand(by) - def feather(self, by: int): + def feather(self, by: float) -> None: """Feathers the edges of the selection. Args: by: The amount to feather the edge. """ - return self.app.feather(by) + self.app.feather(by) def fill( self, fill_type: SolidColor, - mode: ColorBlendMode = None, - opacity=None, - preserve_transparency=None, - ): + mode: ColorBlendMode | None = None, + opacity: float | None = None, + preserve_transparency: bool = False, + ) -> None: """Fills the selection.""" - return self.app.fill(fill_type, mode, opacity, preserve_transparency) + self.app.fill(fill_type, mode, opacity, preserve_transparency) - def grow(self, tolerance, anti_alias): + def grow(self, tolerance: int, anti_alias: bool) -> None: """Grows the selection to include all adjacent pixels falling within The specified tolerance range. @@ -117,38 +129,54 @@ def grow(self, tolerance, anti_alias): """ - return self.app.grow(tolerance, anti_alias) + self.app.grow(tolerance, anti_alias) - def invert(self): + def invert(self) -> None: """Inverts the selection.""" self.app.invert() - def load(self, from_channel, combination, inverting): + def load( + self, + from_channel: Channel, + combination: SelectionType | None = None, + inverting: bool = False, + ) -> None: """Loads the selection from the specified channel.""" - return self.app.load(from_channel, combination, inverting) + self.app.load(from_channel, combination, inverting) - def makeWorkPath(self, tolerance): + def makeWorkPath(self, tolerance: float) -> None: """Makes this selection item the workpath for this document.""" self.app.makeWorkPath(tolerance) - def resize(self, horizontal, vertical, anchor): + def resize(self, horizontal: float, vertical: float, anchor: AnchorPosition) -> None: """Resizes the selected area to the specified dimensions and anchor position.""" self.app.resize(horizontal, vertical, anchor) - def resizeBoundary(self, horizontal, vertical, anchor): + def resizeBoundary(self, horizontal: float, vertical: float, anchor: AnchorPosition) -> None: """Scales the boundary of the selection.""" self.app.resizeBoundary(horizontal, vertical, anchor) - def rotate(self, angle, anchor): + def rotate(self, angle: float, anchor: AnchorPosition) -> None: """Rotates the object.""" self.app.rotate(angle, anchor) - def rotateBoundary(self, angle, anchor): + def rotateBoundary(self, angle: float, anchor: AnchorPosition) -> None: """Rotates the boundary of the selection.""" self.app.rotateBoundary(angle, anchor) - def stroke(self, strokeColor, width, location, mode, opacity, preserveTransparency): + def selectAll(self) -> None: + self.app.selectAll() + + def stroke( + self, + strokeColor: SolidColor, + width: int, + location: StrokeLocation | None = None, + mode: ColorBlendMode | None = None, + opacity: int = 100, + preserveTransparency: bool = False, + ) -> None: """Strokes the selection. Args: @@ -161,9 +189,9 @@ def stroke(self, strokeColor, width, location, mode, opacity, preserveTransparen preserveTransparency (bool): If true, preserves transparency. """ - return self.app.stroke(strokeColor, width, location, mode, opacity, preserveTransparency) + self.app.stroke(strokeColor, width, location, mode, opacity, preserveTransparency) - def selectBorder(self, width): + def selectBorder(self, width: float) -> None: """Selects the selection border only (in the specified width); subsequent actions do not affect the selected area within the borders. @@ -171,24 +199,24 @@ def selectBorder(self, width): width (int): The width of the border selection. """ - return self.app.selectBorder(width) + self.app.selectBorder(width) - def similar(self, tolerance, antiAlias): - return self.app.similar(tolerance, antiAlias) + def similar(self, tolerance: int, anti_alias: bool = True) -> None: + self.app.similar(tolerance, anti_alias) - def smooth(self, radius): + def smooth(self, radius: int) -> None: """Cleans up stray pixels left inside or outside a color-based selection (within the radius specified in pixels).""" - return self.app.smooth(radius) + self.app.smooth(radius) - def store(self, into, combination=SelectionType.ReplaceSelection): + def store(self, into: Channel, combination: SelectionType = SelectionType.ReplaceSelection) -> None: """Saves the selection as a channel.""" - return self.app.store(into, combination) + self.app.store(into, combination) - def translate(self, deltaX, deltaY): + def translate(self, deltaX: float = 0, deltaY: float = 0) -> None: """Moves the object relative to its current position.""" - return self.app.translate(deltaX, deltaY) + self.app.translate(deltaX, deltaY) - def translateBoundary(self, deltaX, deltaY): + def translateBoundary(self, deltaX: float = 0, deltaY: float = 0) -> None: """Moves the boundary of selection relative to its current position.""" - return self.app.translateBoundary(deltaX, deltaY) + self.app.translateBoundary(deltaX, deltaY) diff --git a/photoshop/api/_text_fonts.py b/photoshop/api/_text_fonts.py index dbe43d63..4f7f008a 100644 --- a/photoshop/api/_text_fonts.py +++ b/photoshop/api/_text_fonts.py @@ -1,6 +1,7 @@ # Import built-in modules -from typing import Any -from typing import Union +from typing import Iterator +from typing import TypeVar +from typing import overload # Import third-party modules from comtypes import ArgumentError @@ -12,24 +13,27 @@ from photoshop.api.text_font import TextFont +T = TypeVar("T") + + class TextFonts(Photoshop): """An installed font.""" - def __init__(self, parent=None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) """ MAGIC METHODS """ - def __len__(self): + def __len__(self) -> int: return self.length - def __iter__(self): + def __iter__(self) -> Iterator[TextFont]: for font in self.app: yield TextFont(font) - def __contains__(self, name: str): + def __contains__(self, name: str) -> bool: """Check if a font is installed. Lookup by font postScriptName (fastest) or name. Args: @@ -50,7 +54,7 @@ def __contains__(self, name: str): continue return False - def __getitem__(self, key: str): + def __getitem__(self, key: str) -> TextFont: """Access a given TextFont using dictionary key lookup, must provide the postScriptName. Args: @@ -69,7 +73,15 @@ def __getitem__(self, key: str): METHODS """ - def get(self, key: str, default: Any = None) -> Union[TextFont, Any]: + @overload + def get(self, key: str, default: T) -> TextFont | T: + ... + + @overload + def get(self, key: str) -> TextFont | None: + ... + + def get(self, key: str, default: T | None = None) -> TextFont | T | None: """ Accesses a given TextFont using dictionary key lookup of postScriptName, returns default if not found. @@ -86,7 +98,7 @@ def get(self, key: str, default: Any = None) -> Union[TextFont, Any]: except (KeyError, ArgumentError): return default - def getByName(self, name: str) -> TextFont: + def getByName(self, name: str) -> TextFont | None: """Gets the font by the font name. Args: @@ -100,17 +112,12 @@ def getByName(self, name: str) -> TextFont: for font in self.app: if font.name == name: return TextFont(font) - raise PhotoshopPythonAPIError('Could not find a TextFont named "{name}"') """ PROPERTIES """ @property - def _fonts(self): - return [a for a in self.app] - - @property - def length(self): + def length(self) -> int: """The number pf elements in the collection.""" - return len(self._fonts) + return len(list(self.app)) diff --git a/photoshop/api/action_descriptor.py b/photoshop/api/action_descriptor.py index 6ecd4b0b..fe859ca8 100644 --- a/photoshop/api/action_descriptor.py +++ b/photoshop/api/action_descriptor.py @@ -8,16 +8,16 @@ """ # Import built-in modules -from pathlib import Path +from os import PathLike # Import local modules from photoshop.api._core import Photoshop from photoshop.api.action_list import ActionList from photoshop.api.action_reference import ActionReference -from photoshop.api.enumerations import DescValueType +from photoshop.api.base_action import BaseAction -class ActionDescriptor(Photoshop): +class ActionDescriptor(BaseAction): """A record of key-value pairs for actions, such as those included on the Adobe Photoshop Actions menu. The ActionDescriptor class is part of the Action Manager functionality. @@ -27,30 +27,11 @@ class ActionDescriptor(Photoshop): object_name = "ActionDescriptor" - def __init__(self): - super().__init__() + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) self._flag_as_method( - "clear", "erase", "fromStream", - "getBoolean", - "getClass", - "getData", - "getDouble", - "getEnumerationType", - "getEnumerationValue", - "getInteger", - "getKey", - "getLargeInteger", - "getList", - "getObjectType", - "getObjectValue", - "getPath", - "getReference", - "getString", - "getType", - "getUnitDoubleType", - "getUnitDoubleValue", "hasKey", "isEqual", "putBoolean", @@ -69,120 +50,20 @@ def __init__(self): "toSteadm", ) - @property - def count(self): - """The number of keys contained in the descriptor.""" - return self.app.count - - def clear(self): - """Clears the descriptor.""" - self.app.clear() - - def erase(self, key: int): + def erase(self, key: int) -> None: """Erases a key form the descriptor.""" self.app.erase(key) - def fromStream(self, value: str): - """Create a descriptor from a stream of bytes. - - for reading from disk. - - """ + def fromStream(self, value: str) -> None: + """Create a descriptor from a stream of bytes, + for reading from disk.""" self.app.fromStream(value) - def getBoolean(self, key: int) -> int: - """Gets the text_font of a key of type boolean. - - Args: - key (str): key of type boolean. - - Returns: - bool: The text_font of a key of type boolean. - - """ - return self.app.getBoolean(key) - - def getClass(self, key): - """Gets the text_font of a key of type class. - - Args: - key (str): The key of type class. - - Returns: - int: The text_font of a key of type class. - - """ - return self.app.getClass(key) - - def getData(self, key: int) -> int: - """Gets raw byte data as a string value.""" - return self.app.getData(key) - - def getDouble(self, key: int) -> float: - """Gets the value of a key of type double.""" - return self.app.getDouble(key) - - def getEnumerationType(self, index: int) -> int: - """Gets the enumeration type of a key.""" - return self.app.getEnumerationType(index) - - def getEnumerationValue(self, index: int) -> int: - """Gets the enumeration value of a key.""" - return self.app.getEnumerationValue(index) - - def getInteger(self, index: int) -> int: - """Gets the value of a key of type integer.""" - return self.app.getInteger(index) - - def getKey(self, index: int) -> int: - """Gets the ID of the key provided by index.""" - return self.app.getKey(index) - - def getLargeInteger(self, index: int) -> int: - """Gets the value of a key of type large integer.""" - return self.app.getLargeInteger(index) - - def getList(self, index: int) -> ActionList: - """Gets the value of a key of type list.""" - return ActionList(self.app.getList(index)) - - def getObjectType(self, key: int) -> int: - """Gets the class ID of an object in a key of type object.""" - return self.app.getObjectType(key) - - def getObjectValue(self, key: int) -> int: - """Get the class ID of an object in a key of type object.""" - return self.app.getObjectValue(key) - - def getPath(self, key: int) -> Path: - """Gets the value of a key of type.""" - return Path(self.app.getPath(key)) - - def getReference(self, key: int) -> ActionReference: - """Gets the value of a key of type.""" - return ActionReference(self.app.getReference(key)) - - def getString(self, key: int) -> str: - """Gets the value of a key of type.""" - return self.app.getString(key) - - def getType(self, key: int) -> DescValueType: - """Gets the type of a key.""" - return DescValueType(self.app.getType(key)) - - def getUnitDoubleType(self, key: int) -> int: - """Gets the unit type of a key of type UnitDouble.""" - return self.app.getUnitDoubleType(key) - - def getUnitDoubleValue(self, key: int) -> float: - """Gets the unit type of a key of type UnitDouble.""" - return self.app.getUnitDoubleValue(key) - def hasKey(self, key: int) -> bool: """Checks whether the descriptor contains the provided key.""" return self.app.hasKey(key) - def isEqual(self, otherDesc) -> bool: + def isEqual(self, otherDesc: "ActionDescriptor") -> bool: """Determines whether the descriptor is the same as another descriptor. Args: @@ -191,55 +72,55 @@ def isEqual(self, otherDesc) -> bool: """ return self.app.isEqual(otherDesc) - def putBoolean(self, key: int, value: bool): + def putBoolean(self, key: int, value: bool) -> None: """Sets the value for a key whose type is boolean.""" self.app.putBoolean(key, value) - def putClass(self, key: int, value: int): + def putClass(self, key: int, value: int) -> None: """Sets the value for a key whose type is class.""" self.app.putClass(key, value) - def putData(self, key: int, value: str): + def putData(self, key: int, value: str) -> None: """Puts raw byte data as a string value.""" self.app.putData(key, value) - def putDouble(self, key: int, value: float): + def putDouble(self, key: int, value: float) -> None: """Sets the value for a key whose type is double.""" self.app.putDouble(key, value) - def putEnumerated(self, key: int, enum_type: int, value: int): + def putEnumerated(self, key: int, enum_type: int, value: int) -> None: """Sets the enumeration type and value for a key.""" self.app.putEnumerated(key, enum_type, value) - def putInteger(self, key: int, value: int): + def putInteger(self, key: int, value: int) -> None: """Sets the value for a key whose type is integer.""" self.app.putInteger(key, value) - def putLargeInteger(self, key: int, value: int): + def putLargeInteger(self, key: int, value: int) -> None: """Sets the value for a key whose type is large integer.""" self.app.putLargeInteger(key, value) - def putList(self, key: int, value: ActionList): + def putList(self, key: int, value: "ActionList") -> None: """Sets the value for a key whose type is an ActionList object.""" self.app.putList(key, value) - def putObject(self, key: int, class_id: int, value): + def putObject(self, key: int, class_id: int, value: "ActionDescriptor") -> None: """Sets the value for a key whose type is an object.""" self.app.putObject(key, class_id, value) - def putPath(self, key: int, value: str): + def putPath(self, key: int, value: str | PathLike[str]) -> None: """Sets the value for a key whose type is path.""" - self.app.putPath(key, value) + self.app.putPath(key, str(value)) - def putReference(self, key: int, value: ActionReference): + def putReference(self, key: int, value: ActionReference) -> None: """Sets the value for a key whose type is an object reference.""" self.app.putReference(key, value) - def putString(self, key: int, value: str): + def putString(self, key: int, value: str) -> None: """Sets the value for a key whose type is string.""" self.app.putString(key, value) - def putUnitDouble(self, key: int, unit_id: int, value: float): + def putUnitDouble(self, key: int, unit_id: int, value: float) -> None: """Sets the value for a key whose type is a unit value formatted as double.""" self.app.putUnitDouble(key, unit_id, value) diff --git a/photoshop/api/action_list.py b/photoshop/api/action_list.py index b8c1e3f8..e54a80ff 100644 --- a/photoshop/api/action_list.py +++ b/photoshop/api/action_list.py @@ -4,11 +4,23 @@ """ + +# Import built-in modules +from os import PathLike +from typing import TYPE_CHECKING + # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.action_reference import ActionReference +from photoshop.api.base_action import BaseAction + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api.action_descriptor import ActionDescriptor -class ActionList(Photoshop): +class ActionList(BaseAction): """The list of commands that comprise an Action. (such as an Action created using the Actions palette in the Adobe Photoshop application). @@ -19,51 +31,79 @@ class ActionList(Photoshop): object_name = "ActionList" - def __init__(self, parent=None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( - "getBoolean", - "getClass", - "getData", - "getDouble", - "getEnumerationType", - "getEnumerationValue", - "getInteger", - "getLargeInteger", - "getList", - "getObjectType", + "putBoolean", + "putClass", + "putData", + "putDouble", + "putEnumerated", + "putInteger", + "putLargeInteger", + "putList", + "putObject", + "putPath", + "putReference", + "putString", + "putUnitDouble", + "toSteadm", ) - @property - def count(self): - return self.app.count + def putBoolean(self, value: bool) -> None: + """Sets the value for a key whose type is boolean.""" + self.app.putBoolean(value) + + def putClass(self, value: int) -> None: + """Sets the value for a key whose type is class.""" + self.app.putClass(value) + + def putData(self, value: str) -> None: + """Puts raw byte data as a string value.""" + self.app.putData(value) + + def putDouble(self, value: float) -> None: + """Sets the value for a key whose type is double.""" + self.app.putDouble(value) - def getBoolean(self, index): - return self.app.getBoolean(index) + def putEnumerated(self, enum_type: int, value: int) -> None: + """Sets the enumeration type and value for a key.""" + self.app.putEnumerated(enum_type, value) - def getClass(self, index): - return self.app.getClass(index) + def putInteger(self, value: int) -> None: + """Sets the value for a key whose type is integer.""" + self.app.putInteger(value) - def getData(self, index): - return self.app.getData(index) + def putLargeInteger(self, value: int) -> None: + """Sets the value for a key whose type is large integer.""" + self.app.putLargeInteger(value) - def getDouble(self, index): - return self.app.getDouble(index) + def putList(self, value: "ActionList") -> None: + """Sets the value for a key whose type is an ActionList object.""" + self.app.putList(value) - def getEnumerationType(self, index): - return self.app.getEnumerationType(index) + def putObject(self, class_id: int, value: "ActionDescriptor") -> None: + """Sets the value for a key whose type is an object.""" + self.app.putObject(class_id, value) - def getEnumerationValue(self, index): - return self.app.getEnumerationValue(index) + def putPath(self, value: str | PathLike[str]) -> None: + """Sets the value for a key whose type is path.""" + self.app.putPath(str(value)) - def getInteger(self, index): - return self.app.getInteger(index) + def putReference(self, value: ActionReference) -> None: + """Sets the value for a key whose type is an object reference.""" + self.app.putReference(value) - def getLargeInteger(self, index): - return self.app.getLargeInteger(index) + def putString(self, value: str) -> None: + """Sets the value for a key whose type is string.""" + self.app.putString(value) - def getList(self, index): - return self.app.getList(index) + def putUnitDouble(self, unit_id: int, value: float) -> None: + """Sets the value for a key whose type is a unit value formatted as + double.""" + self.app.putUnitDouble(unit_id, value) - def getObjectType(self, index): - return self.app.getObjectType(index) + def toStream(self) -> str: + """Gets the entire descriptor as as stream of bytes, + for writing to disk.""" + return self.app.toSteadm() diff --git a/photoshop/api/action_reference.py b/photoshop/api/action_reference.py index 2e72e3a4..f73efcd2 100644 --- a/photoshop/api/action_reference.py +++ b/photoshop/api/action_reference.py @@ -8,6 +8,7 @@ with an ActionDescriptor. """ + # Import local modules from photoshop.api._core import Photoshop from photoshop.api.enumerations import ReferenceFormType @@ -23,7 +24,7 @@ class ActionReference(Photoshop): object_name = "ActionReference" - def __init__(self, parent=None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "getContainer", @@ -33,6 +34,9 @@ def __init__(self, parent=None): "getForm", "getIdentifier", "getIndex", + "getName", + "getOffset", + "getProperty", "putName", "putClass", "putEnumerated", @@ -42,10 +46,10 @@ def __init__(self, parent=None): "putProperty", ) - def getContainer(self): + def getContainer(self) -> "ActionReference": return self.app.getContainer() - def getDesiredClass(self): + def getDesiredClass(self) -> int: return self.app.getDesiredClass() def getEnumeratedType(self) -> int: @@ -67,25 +71,37 @@ def getIndex(self) -> int: """Gets the index value for a reference in a list or array,""" return self.app.getIndex() - def putName(self, key, value): - return self.app.putName(key, value) + def getName(self) -> str: + """Gets the name of a reference.""" + return self.app.getName() + + def getOffset(self) -> int: + """Gets the offset of the object's index value.""" + return self.app.getOffset() + + def getProperty(self) -> int: + """Gets the property ID value.""" + return self.app.getProperty() + + def putName(self, key: int, value: str) -> None: + self.app.putName(key, value) - def putClass(self, value): - return self.app.putClass(value) + def putClass(self, value: int) -> None: + self.app.putClass(value) - def putEnumerated(self, desired_class, enum_type, value): + def putEnumerated(self, desired_class: int, enum_type: int, value: int) -> None: """Puts an enumeration type and ID into a reference along with the desired class for the reference.""" - return self.app.putEnumerated(desired_class, enum_type, value) + self.app.putEnumerated(desired_class, enum_type, value) - def putIdentifier(self, desired_class, value): - return self.app.putIdentifier(desired_class, value) + def putIdentifier(self, desired_class: int, value: int) -> None: + self.app.putIdentifier(desired_class, value) - def putIndex(self, desired_class, value): - return self.app.putIndex(desired_class, value) + def putIndex(self, desired_class: int, value: int) -> None: + self.app.putIndex(desired_class, value) - def putOffset(self, desired_class, value): - return self.app.putOffset(desired_class, value) + def putOffset(self, desired_class: int, value: int) -> None: + self.app.putOffset(desired_class, value) - def putProperty(self, desired_class, value): - return self.app.putProperty(desired_class, value) + def putProperty(self, desired_class: int, value: int) -> None: + self.app.putProperty(desired_class, value) diff --git a/photoshop/api/application.py b/photoshop/api/application.py index 0df16cfa..d7d8a55b 100644 --- a/photoshop/api/application.py +++ b/photoshop/api/application.py @@ -10,13 +10,12 @@ app.documents.add(800, 600, 72, "docRef") """ + # Import built-in modules import os from pathlib import Path import time -from typing import List -from typing import Optional -from typing import Union +from typing import Any # Import third-party modules from _ctypes import COMError @@ -31,6 +30,9 @@ from photoshop.api._notifiers import Notifiers from photoshop.api._preferences import Preferences from photoshop.api._text_fonts import TextFonts +from photoshop.api.action_descriptor import ActionDescriptor +from photoshop.api.action_reference import ActionReference +from photoshop.api.batch_options import BatchOptions from photoshop.api.enumerations import DialogModes from photoshop.api.enumerations import PurgeTarget from photoshop.api.errors import PhotoshopPythonAPIError @@ -45,7 +47,7 @@ class Application(Photoshop): """ - def __init__(self, version: Optional[str] = None): + def __init__(self, version: str | None = None) -> None: super().__init__(ps_version=version) self._flag_as_method( "batch", @@ -91,7 +93,7 @@ def activeDocument(self) -> Document: return Document(self.app.activeDocument) @activeDocument.setter - def activeDocument(self, document: Document): + def activeDocument(self, document: Document) -> None: self.app.activeDocument = document @property @@ -100,7 +102,7 @@ def backgroundColor(self) -> SolidColor: return SolidColor(self.app.backgroundColor) @backgroundColor.setter - def backgroundColor(self, color: SolidColor): + def backgroundColor(self, color: SolidColor) -> None: """Sets the default background color and color style for documents. Args: @@ -123,7 +125,7 @@ def colorSettings(self) -> str: return self.app.colorSettings @colorSettings.setter - def colorSettings(self, settings: str): + def colorSettings(self, settings: str) -> None: """Sets the currently selected color settings profile. Args: @@ -141,7 +143,7 @@ def currentTool(self) -> str: return self.app.currentTool @currentTool.setter - def currentTool(self, tool_name: str): + def currentTool(self, tool_name: str) -> None: """Sets the currently selected tool. Args: @@ -157,7 +159,7 @@ def displayDialogs(self) -> DialogModes: return DialogModes(self.app.displayDialogs) @displayDialogs.setter - def displayDialogs(self, dialog_mode: DialogModes): + def displayDialogs(self, dialog_mode: DialogModes) -> None: """The dialog mode for the document, which indicates whether Photoshop displays dialogs when the script runs. """ @@ -185,7 +187,7 @@ def foregroundColor(self) -> SolidColor: return SolidColor(parent=self.app.foregroundColor) @foregroundColor.setter - def foregroundColor(self, color: SolidColor): + def foregroundColor(self, color: SolidColor) -> None: """Set the `foregroundColor`. Args: @@ -205,7 +207,7 @@ def locale(self) -> str: return self.app.locale @property - def macintoshFileTypes(self) -> List[str]: + def macintoshFileTypes(self) -> tuple[str]: """A list of the image file types Photoshop can open.""" return self.app.macintoshFileTypes @@ -230,11 +232,11 @@ def notifiersEnabled(self) -> bool: return self.app.notifiersEnabled @notifiersEnabled.setter - def notifiersEnabled(self, value: bool): + def notifiersEnabled(self, value: bool) -> None: self.app.notifiersEnabled = value @property - def parent(self): + def parent(self) -> object: """The object’s container.""" return self.app.parent @@ -244,16 +246,16 @@ def path(self) -> Path: return Path(self.app.path) @property - def playbackDisplayDialogs(self): + def playbackDisplayDialogs(self) -> DialogModes: return self.doJavaScript("app.playbackDisplayDialogs") @property - def playbackParameters(self): + def playbackParameters(self) -> ActionDescriptor: """Stores and retrieves parameters used as part of a recorded action.""" return self.app.playbackParameters @playbackParameters.setter - def playbackParameters(self, value): + def playbackParameters(self, value: ActionDescriptor) -> None: self.app.playbackParameters = value @property @@ -265,31 +267,37 @@ def preferencesFolder(self) -> Path: return Path(self.app.preferencesFolder) @property - def recentFiles(self): - return self.app.recentFiles + def recentFiles(self) -> list[Path]: + return [Path(pth) for pth in self.app.recentFiles] @property - def scriptingBuildDate(self): + def scriptingBuildDate(self) -> str: return self.app.scriptingBuildDate @property - def scriptingVersion(self): + def scriptingVersion(self) -> str: return self.app.scriptingVersion @property - def systemInformation(self): + def systemInformation(self) -> str: return self.app.systemInformation @property - def version(self): + def version(self) -> str: return self.app.version @property - def windowsFileTypes(self): + def windowsFileTypes(self) -> tuple[str, ...]: return self.app.windowsFileTypes # Methods. - def batch(self, files, actionName, actionSet, options): + def batch( + self, + files: list[str], + actionName: str, + actionSet: str, + options: BatchOptions, + ) -> None: """Runs the batch automation routine. Similar to the **File** > **Automate** > **Batch** command. @@ -297,30 +305,25 @@ def batch(self, files, actionName, actionSet, options): """ self.app.batch(files, actionName, actionSet, options) - def beep(self): + def beep(self) -> None: """Causes a "beep" sound.""" - return self.eval_javascript("app.beep()") + self.eval_javascript("app.beep()") - def bringToFront(self): - return self.eval_javascript("app.bringToFront()") + def bringToFront(self) -> None: + self.eval_javascript("app.bringToFront()") - def changeProgressText(self, text): + def changeProgressText(self, text: str) -> None: """Changes the text that appears in the progress window.""" self.eval_javascript(f"app.changeProgressText('{text}')") - def charIDToTypeID(self, char_id): + def charIDToTypeID(self, char_id: str) -> int: return self.app.charIDToTypeID(char_id) - @staticmethod - def compareWithNumbers(first, second): - return first > second - - def doAction(self, action, action_from="Default Actions"): + def doAction(self, action: str, action_from: str = "Default Actions") -> None: """Plays the specified action from the Actions palette.""" self.app.doAction(action, action_from) - return True - def doForcedProgress(self, title, javascript): + def doForcedProgress(self, title: str, javascript: str) -> None: script = "app.doForcedProgress('{}', '{}')".format( title, javascript, @@ -329,7 +332,7 @@ def doForcedProgress(self, title, javascript): # Ensure the script execute success. time.sleep(1) - def doProgress(self, title, javascript): + def doProgress(self, title: str, javascript: str) -> None: """Performs a task with a progress bar. Other progress APIs must be called periodically to update the progress bar and allow cancelling. @@ -346,7 +349,7 @@ def doProgress(self, title, javascript): # Ensure the script execute success. time.sleep(1) - def doProgressSegmentTask(self, segmentLength, done, total, javascript): + def doProgressSegmentTask(self, segmentLength: int, done: int, total: int, javascript: str) -> None: script = "app.doProgressSegmentTask({}, {}, {}, '{}');".format( segmentLength, done, @@ -357,7 +360,7 @@ def doProgressSegmentTask(self, segmentLength, done, total, javascript): # Ensure the script execute success. time.sleep(1) - def doProgressSubTask(self, index, limit, javascript): + def doProgressSubTask(self, index: int, limit: int, javascript: str) -> None: script = "app.doProgressSubTask({}, {}, '{}');".format( index, limit, @@ -367,27 +370,32 @@ def doProgressSubTask(self, index, limit, javascript): # Ensure the script execute success. time.sleep(1) - def doProgressTask(self, index, javascript): + def doProgressTask(self, taskLength: float, javascript: str) -> None: """Sections off a portion of the unused progress bar for execution of a subtask. Returns false on cancel. """ - script = f"app.doProgressTask({index}, '{javascript}');" + script = f"app.doProgressTask({taskLength}, '{javascript}');" self.eval_javascript(script) # Ensure the script execute success. time.sleep(1) - def eraseCustomOptions(self, key): + def eraseCustomOptions(self, key: str) -> None: """Removes the specified user objects from the Photoshop registry.""" self.app.eraseCustomOptions(key) - def executeAction(self, event_id, descriptor, display_dialogs=2): - return self.app.executeAction(event_id, descriptor, display_dialogs) + def executeAction( + self, + event_id: int, + descriptor: ActionDescriptor | None = None, + display_dialogs: DialogModes = DialogModes.DisplayNoDialogs, + ) -> ActionDescriptor: + return ActionDescriptor(self.app.executeAction(event_id, descriptor, display_dialogs)) - def executeActionGet(self, reference): - return self.app.executeActionGet(reference) + def executeActionGet(self, reference: ActionReference) -> ActionDescriptor: + return ActionDescriptor(self.app.executeActionGet(reference)) - def featureEnabled(self, name): + def featureEnabled(self, name: str) -> bool: """Determines whether the feature specified by name is enabled. @@ -401,37 +409,37 @@ def featureEnabled(self, name): """ return self.app.featureEnabled(name) - def getCustomOptions(self, key): + def getCustomOptions(self, key: str) -> ActionDescriptor: """Retrieves user objects in the Photoshop registry for the ID with value key.""" - return self.app.getCustomOptions(key) + return ActionDescriptor(self.app.getCustomOptions(key)) def open( self, - document_file_path, - document_type: str = None, + document_file_path: str | os.PathLike[str], + document_type: str | None = None, as_smart_object: bool = False, ) -> Document: - document = self.app.open(document_file_path, document_type, as_smart_object) + document = self.app.open(str(document_file_path), document_type, as_smart_object) if not as_smart_object: return Document(document) return document - def load(self, document_file_path: Union[str, os.PathLike]) -> Document: + def load(self, document_file_path: str | os.PathLike[str]) -> Document: """Loads a supported Photoshop document.""" self.app.load(str(document_file_path)) return self.activeDocument - def doJavaScript(self, javascript, Arguments=None, ExecutionMode=None): + def doJavaScript(self, javascript: str, Arguments: Any = None, ExecutionMode: Any = None): return self.app.doJavaScript(javascript, Arguments, ExecutionMode) def isQuicktimeAvailable(self) -> bool: return self.app.isQuicktimeAvailable - def openDialog(self): - return self.app.openDialog() + def openDialog(self) -> list[Path]: + return [Path(pth) for pth in self.app.openDialog()] - def purge(self, target: PurgeTarget): + def purge(self, target: PurgeTarget) -> None: """Purges one or more caches. Args: @@ -444,10 +452,10 @@ def purge(self, target: PurgeTarget): """ self.app.purge(target) - def putCustomOptions(self, key, custom_object, persistent): + def putCustomOptions(self, key: str, custom_object: ActionDescriptor, persistent: bool) -> None: self.app.putCustomOptions(key, custom_object, persistent) - def refresh(self): + def refresh(self) -> None: """Pauses the script while the application refreshes. Ues to slow down execution and show the results to the user as the @@ -458,42 +466,38 @@ def refresh(self): """ self.app.refresh() - def refreshFonts(self): + def refreshFonts(self) -> None: """Force the font list to get refreshed.""" - return self.eval_javascript("app.refreshFonts();") + self.eval_javascript("app.refreshFonts();") - def runMenuItem(self, menu_id): + def runMenuItem(self, menu_id: int) -> None: """Run a menu item given the menu ID.""" - return self.eval_javascript( + self.eval_javascript( f"app.runMenuItem({menu_id})", ) - def showColorPicker(self): + def showColorPicker(self) -> str: """Returns false if dialog is cancelled, true otherwise.""" return self.eval_javascript("app.showColorPicker();") - def stringIDToTypeID(self, string_id): + def stringIDToTypeID(self, string_id: str) -> int: return self.app.stringIDToTypeID(string_id) - def togglePalettes(self): + def togglePalettes(self) -> None: """Toggle palette visibility.""" - return self.doJavaScript("app.togglePalettes()") + self.doJavaScript("app.togglePalettes()") - def toolSupportsBrushes(self, tool): + def toolSupportsBrushes(self, tool: str) -> bool: return self.app.toolSupportsBrushes(tool) - def toolSupportsBrushPresets(self, tool): + def toolSupportsBrushPresets(self, tool: str) -> bool: return self.app.toolSupportsPresets(tool) - @staticmethod - def system(command): - os.system(command) - def typeIDToStringID(self, type_id: int) -> str: return self.app.typeIDToStringID(type_id) def typeIDToCharID(self, type_id: int) -> str: return self.app.typeIDToCharID(type_id) - def updateProgress(self, done, total): + def updateProgress(self, done: int, total: int) -> None: self.eval_javascript(f"app.updateProgress({done}, {total})") diff --git a/photoshop/api/base_action.py b/photoshop/api/base_action.py new file mode 100644 index 00000000..4aa2b201 --- /dev/null +++ b/photoshop/api/base_action.py @@ -0,0 +1,146 @@ +# Import built-in modules +from pathlib import Path +from typing import TYPE_CHECKING + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.action_reference import ActionReference +from photoshop.api.enumerations import DescValueType + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api.action_descriptor import ActionDescriptor + from photoshop.api.action_list import ActionList + + +class BaseAction(Photoshop): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) + self._flag_as_method( + "clear", + "getBoolean", + "getClass", + "getData", + "getDouble", + "getEnumerationType", + "getEnumerationValue", + "getInteger", + "getKey", + "getLargeInteger", + "getList", + "getObjectType", + "getObjectValue", + "getPath", + "getReference", + "getString", + "getType", + "getUnitDoubleType", + "getUnitDoubleValue", + ) + + @property + def count(self) -> int: + """The number of keys contained in the descriptor.""" + return self.app.count + + def clear(self) -> None: + """Clears the descriptor.""" + self.app.clear() + + def getBoolean(self, key: int) -> bool: + """Gets the text_font of a key of type boolean. + + Args: + key (str): key of type boolean. + + Returns: + bool: The text_font of a key of type boolean. + + """ + return self.app.getBoolean(key) + + def getClass(self, key: int) -> int: + """Gets the text_font of a key of type class. + + Args: + key (str): The key of type class. + + Returns: + int: The text_font of a key of type class. + + """ + return self.app.getClass(key) + + def getData(self, key: int) -> str: + """Gets raw byte data as a string value.""" + return self.app.getData(key) + + def getDouble(self, key: int) -> float: + """Gets the value of a key of type double.""" + return self.app.getDouble(key) + + def getEnumerationType(self, index: int) -> int: + """Gets the enumeration type of a key.""" + return self.app.getEnumerationType(index) + + def getEnumerationValue(self, index: int) -> int: + """Gets the enumeration value of a key.""" + return self.app.getEnumerationValue(index) + + def getInteger(self, index: int) -> int: + """Gets the value of a key of type integer.""" + return self.app.getInteger(index) + + def getKey(self, index: int) -> int: + """Gets the ID of the key provided by index.""" + return self.app.getKey(index) + + def getLargeInteger(self, index: int) -> int: + """Gets the value of a key of type large integer.""" + return self.app.getLargeInteger(index) + + def getList(self, index: int) -> "ActionList": + """Gets the value of a key of type list.""" + # Import local modules + from photoshop.api.action_list import ActionList + + return ActionList(self.app.getList(index)) + + def getObjectType(self, key: int) -> int: + """Gets the class ID of an object in a key of type object.""" + return self.app.getObjectType(key) + + def getObjectValue(self, key: int) -> "ActionDescriptor": + """Get the class ID of an object in a key of type object.""" + from .action_descriptor import ActionDescriptor + + return ActionDescriptor(self.app.getObjectValue(key)) + + def getPath(self, key: int) -> Path: + """Gets the value of a key of type File.""" + return Path(self.app.getPath(key)) + + def getReference(self, key: int) -> ActionReference: + """Gets the value of a key of type.""" + return ActionReference(self.app.getReference(key)) + + def getString(self, key: int) -> str: + """Gets the value of a key of type.""" + return self.app.getString(key) + + def getType(self, key: int) -> DescValueType: + """Gets the type of a key.""" + return DescValueType(self.app.getType(key)) + + def getUnitDoubleType(self, key: int) -> int: + """Gets the unit type of a key of type UnitDouble.""" + return self.app.getUnitDoubleType(key) + + def getUnitDoubleValue(self, key: int) -> float: + """Gets the unit type of a key of type UnitDouble.""" + return self.app.getUnitDoubleValue(key) + + def hasKey(self, key: int) -> bool: + """Checks whether the descriptor contains the provided key.""" + return self.app.hasKey(key) diff --git a/photoshop/api/batch_options.py b/photoshop/api/batch_options.py index 87156b24..b7e0fb33 100644 --- a/photoshop/api/batch_options.py +++ b/photoshop/api/batch_options.py @@ -1,49 +1,59 @@ # https://theiviaxx.github.io/photoshop-docs/Photoshop/BatchOptions.html +# Import built-in modules +from os import PathLike +from pathlib import Path +from typing import Sequence + # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import BatchDestinationType +from photoshop.api.enumerations import FileNamingType class BatchOptions(Photoshop): object_name = "BatchOptions" - def __init__(self): - super().__init__() - @property - def destination(self): + def destination(self) -> BatchDestinationType: """The type of destination for the processed files.""" - return self.app.destination + return BatchDestinationType(self.app.destination) @destination.setter - def destination(self, value): + def destination(self, value: BatchDestinationType) -> None: self.app.destination = value @property - def destinationFolder(self): - """The folder location for the processed files. Valid only when ‘destination’ = folder.""" - return self.app.destinationFolder + def destinationFolder(self) -> Path | None: + """The folder location for the processed files. Valid only when ‘destination’ == BatchDestinationType.FOLDER.""" + destination = self.app.destinationFolder + if destination: + return Path(self.app.destinationFolder) + return None @destinationFolder.setter - def destinationFolder(self, path): - self.app.destinationFolder = path + def destinationFolder(self, path: str | PathLike[str]) -> None: + self.app.destinationFolder = str(path) @property - def errorFile(self): + def errorFile(self) -> Path | None: """The file in which to log errors encountered. To display errors on the screen and stop batch processing when errors occur, leave blank.""" - return self.app.errorFile + err_file = self.app.errorFile + if err_file: + return Path(self.app.errorFile) + return None @errorFile.setter - def errorFile(self, file_path): - self.app.errorFile = file_path + def errorFile(self, file_path: str | PathLike[str] | None) -> None: + self.app.errorFile = str(file_path) if file_path else None @property - def fileNaming(self) -> list: + def fileNaming(self) -> list[FileNamingType]: """A list of file naming options. Maximum: 6.""" - return self.app.fileNaming + return [FileNamingType(file_naming) for file_naming in self.app.fileNaming] @fileNaming.setter - def fileNaming(self, file_naming: list): + def fileNaming(self, file_naming: Sequence[FileNamingType]) -> None: self.app.fileNaming = file_naming @property @@ -52,7 +62,7 @@ def macintoshCompatible(self) -> bool: return self.app.macintoshCompatible @macintoshCompatible.setter - def macintoshCompatible(self, value: bool): + def macintoshCompatible(self, value: bool) -> None: self.app.macintoshCompatible = value @property @@ -61,7 +71,7 @@ def overrideOpen(self) -> bool: return self.app.overrideOpen @overrideOpen.setter - def overrideOpen(self, value: bool): + def overrideOpen(self, value: bool) -> None: self.app.overrideOpen = value @property @@ -70,7 +80,7 @@ def overrideSave(self) -> bool: return self.app.overrideSave @overrideSave.setter - def overrideSave(self, value: bool): + def overrideSave(self, value: bool) -> None: self.app.overrideSave = value @property @@ -79,7 +89,7 @@ def startingSerial(self) -> int: return self.app.startingSerial @startingSerial.setter - def startingSerial(self, value: int): + def startingSerial(self, value: int) -> None: self.app.startingSerial = value @property @@ -88,7 +98,7 @@ def suppressOpen(self) -> bool: return self.app.suppressOpen @suppressOpen.setter - def suppressOpen(self, value: bool): + def suppressOpen(self, value: bool) -> None: self.app.suppressOpen = value @property @@ -97,7 +107,7 @@ def suppressProfile(self) -> bool: return self.app.suppressProfile @suppressProfile.setter - def suppressProfile(self, value: bool): + def suppressProfile(self, value: bool) -> None: self.app.suppressProfile = value @property @@ -106,7 +116,7 @@ def unixCompatible(self) -> bool: return self.app.unixCompatible @unixCompatible.setter - def unixCompatible(self, value: bool): + def unixCompatible(self, value: bool) -> None: self.app.unixCompatible = value @property @@ -115,5 +125,5 @@ def windowsCompatible(self) -> bool: return self.app.windowsCompatible @windowsCompatible.setter - def windowsCompatible(self, value: bool): + def windowsCompatible(self, value: bool) -> None: self.app.windowsCompatible = value diff --git a/photoshop/api/collections.py b/photoshop/api/collections.py new file mode 100644 index 00000000..d31e2e5d --- /dev/null +++ b/photoshop/api/collections.py @@ -0,0 +1,97 @@ +# Import built-in modules +from typing import Generic +from typing import Iterator +from typing import Protocol +from typing import TypeVar + +# Import third-party modules +from comtypes import ArgumentError + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.errors import PhotoshopPythonAPIError + + +class _PhotoshopObject(Protocol): + def __init__(self, parent: Photoshop | None = None) -> None: + ... + + +class NamedPhotoshopObject(_PhotoshopObject, Protocol): + @property + def name(self) -> str: + ... + + +T = TypeVar("T", bound=_PhotoshopObject) +N = TypeVar("N", bound=NamedPhotoshopObject) +G = TypeVar("G", bound=int | str) + + +class BaseCollection(Photoshop, Generic[T, G]): + """A collection of Photoshop objects.""" + + def __init__(self, type: type[T], parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) + self.type = type + self._flag_as_method("item") + + @property + def length(self) -> int: + return len(list(self.app)) + + def __len__(self) -> int: + return self.length + + def __getitem__(self, key: G) -> T: + try: + return self.type(self.app[key]) + except ArgumentError as exc: + raise PhotoshopPythonAPIError(f"Couldn't find an item with key '{key}' in {type(self)}") from exc + + def __iter__(self) -> Iterator[T]: + for item in self.app: + yield self.type(item) + + def item(self, index: int) -> T: + return self.type(self.app.item(index)) + + def getByIndex(self, index: int) -> T: + for idx, item in enumerate(self.app): + if idx == index: + return self.type(item) + raise IndexError(f"Index {index} is out of range in {type(self)}") + + +class CollectionWithAdd(BaseCollection[T, G]): + """Collection that has a add method that takes no arguments.""" + + def __init__(self, type: type[T], parent: Photoshop | None = None) -> None: + super().__init__(type, parent) + self._flag_as_method("add") + + def add(self) -> T: + return self.type(self.app.add()) + + +class CollectionOfNamedObjects(BaseCollection[N, G]): + """A collection of named Photoshop objects.""" + + def getByName(self, name: str) -> N | None: + """Get the first element in the collection with the provided name.""" + for item in self.app: + if item.name == name: + return self.type(item) + return None + + +class CollectionOfRemovables(BaseCollection[T, G]): + """A collection of removable Photoshop objects.""" + + def __init__(self, type: type[T], parent: Photoshop | None = None) -> None: + super().__init__(type, parent) + self._flag_as_method("removeAll") + + def removeAll(self) -> None: + """Deletes all items.""" + self.app.removeAll() diff --git a/photoshop/api/colors/cmyk.py b/photoshop/api/colors/cmyk.py index dbe260b8..585b7027 100644 --- a/photoshop/api/colors/cmyk.py +++ b/photoshop/api/colors/cmyk.py @@ -9,41 +9,41 @@ class CMYKColor(Photoshop): object_name = "CMYKColor" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) @property - def black(self) -> int: + def black(self) -> float: """The black color value. Range: 0.0 to 100.0.""" - return round(self.app.black) + return self.app.black @black.setter - def black(self, value: int): + def black(self, value: float) -> None: self.app.black = value @property - def cyan(self) -> int: + def cyan(self) -> float: """The cyan color value. Range: 0.0 to 100.0.""" - return round(self.app.cyan) + return self.app.cyan @cyan.setter - def cyan(self, value: int): + def cyan(self, value: float) -> None: self.app.cyan = value @property - def magenta(self) -> int: + def magenta(self) -> float: """The magenta color value. Range: 0.0 to 100.0.""" - return round(self.app.magenta) + return self.app.magenta @magenta.setter - def magenta(self, value: int): + def magenta(self, value: float) -> None: self.app.magenta = value @property - def yellow(self) -> int: + def yellow(self) -> float: """The yellow color value. Range: 0.0 to 100.0.""" - return round(self.app.yellow) + return self.app.yellow @yellow.setter - def yellow(self, value: int): + def yellow(self, value: float) -> None: self.app.yellow = value diff --git a/photoshop/api/colors/gray.py b/photoshop/api/colors/gray.py index b2321a02..155a9db8 100644 --- a/photoshop/api/colors/gray.py +++ b/photoshop/api/colors/gray.py @@ -9,7 +9,7 @@ class GrayColor(Photoshop): object_name = "GrayColor" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) @property @@ -18,6 +18,6 @@ def gray(self) -> float: return self.app.gray @gray.setter - def gray(self, value: float): + def gray(self, value: float) -> None: """The gray value.""" self.app.gray = value diff --git a/photoshop/api/colors/hsb.py b/photoshop/api/colors/hsb.py index 76ead8c8..9e28c900 100644 --- a/photoshop/api/colors/hsb.py +++ b/photoshop/api/colors/hsb.py @@ -9,30 +9,30 @@ class HSBColor(Photoshop): object_name = "HSBColor" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) @property - def brightness(self): - return round(self.app.brightness) + def brightness(self) -> float: + return self.app.brightness @brightness.setter - def brightness(self, value): + def brightness(self, value: float) -> None: self.app.brightness = value @property - def saturation(self): - return round(self.app.saturation) + def saturation(self) -> float: + return self.app.saturation @saturation.setter - def saturation(self, value): + def saturation(self, value: float) -> None: self.app.saturation = value @property - def hue(self): + def hue(self) -> float: """The hue value. Range: 0.0 to 360.0.""" - return round(self.app.hue) + return self.app.hue @hue.setter - def hue(self, value): + def hue(self, value: float) -> None: self.app.hue = value diff --git a/photoshop/api/colors/lab.py b/photoshop/api/colors/lab.py index 282493d9..a0b1b866 100644 --- a/photoshop/api/colors/lab.py +++ b/photoshop/api/colors/lab.py @@ -7,29 +7,29 @@ class LabColor(Photoshop): object_name = "LabColor" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) @property - def A(self): - return round(self.app.A) + def A(self) -> float: + return self.app.A @A.setter - def A(self, value): + def A(self, value: float) -> None: self.app.A = value @property - def B(self): - return round(self.app.B) + def B(self) -> float: + return self.app.B @B.setter - def B(self, value): + def B(self, value: float) -> None: self.app.B = value @property - def L(self): - return round(self.app.L) + def L(self) -> float: + return self.app.L @L.setter - def L(self, value): + def L(self, value: float) -> None: self.app.L = value diff --git a/photoshop/api/colors/rgb.py b/photoshop/api/colors/rgb.py index 2d6475bd..d1f2575f 100644 --- a/photoshop/api/colors/rgb.py +++ b/photoshop/api/colors/rgb.py @@ -7,43 +7,43 @@ class RGBColor(Photoshop): object_name = "RGBColor" - def __init__(self, parent): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self.blue = self.app.blue self.green = self.app.green self.red = self.app.red @property - def blue(self) -> int: - return round(self.app.blue) + def blue(self) -> float: + return self.app.blue @blue.setter - def blue(self, value: int): + def blue(self, value: float) -> None: self.app.blue = value @property - def green(self) -> int: - return round(self.app.green) + def green(self) -> float: + return self.app.green @green.setter - def green(self, value: int): + def green(self, value: float) -> None: self.app.green = value @property - def red(self) -> int: - return round(self.app.red) + def red(self) -> float: + return self.app.red @red.setter - def red(self, value: int): + def red(self, value: float) -> None: self.app.red = value @property - def hexValue(self): + def hexValue(self) -> str: return self.app.hexValue @hexValue.setter - def hexValue(self, value): + def hexValue(self, value: str) -> None: self.app.hexValue = value - def __str__(self): + def __str__(self) -> str: return f"[red: {self.red}, green:{self.green}, blue:{self.blue})]" diff --git a/photoshop/api/enumerations.py b/photoshop/api/enumerations.py index f1b40a3d..97fbfd01 100644 --- a/photoshop/api/enumerations.py +++ b/photoshop/api/enumerations.py @@ -1,6 +1,7 @@ """constants type of enum for Photoshop.""" # Import built-in modules from enum import IntEnum +from enum import StrEnum class LensType(IntEnum): @@ -383,6 +384,12 @@ class FontPreviewType(IntEnum): FontPreviewSmall = 1 +class FontSize(StrEnum): + Large = "FontSize.LARGE" + Medium = "FontSize.MEDIUM" + Small = "FontSize.SMALL" + + class ForcedColors(IntEnum): BlackWhite = 2 NoForced = 1 @@ -1086,7 +1093,7 @@ class Urgency(IntEnum): Two = 2 -class Wartyle(IntEnum): +class WarpStyle(IntEnum): Arc = 2 ArcLower = 3 ArcUpper = 4 @@ -1256,7 +1263,7 @@ class ZigZagType(IntEnum): "UnderlineType", "Units", "Urgency", - "Wartyle", + "WarpStyle", "WaveType", "WhiteBalanceType", "ZigZagType", diff --git a/photoshop/api/open_options/eps.py b/photoshop/api/open_options/eps.py index fee488c9..090964a3 100644 --- a/photoshop/api/open_options/eps.py +++ b/photoshop/api/open_options/eps.py @@ -1,5 +1,6 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import OpenDocumentMode class EPSOpenOptions(Photoshop): @@ -11,37 +12,53 @@ class EPSOpenOptions(Photoshop): object_name = "EPSOpenOptions" - def __init__(self): + def __init__(self) -> None: super().__init__() @property - def antiAlias(self): + def antiAlias(self) -> bool: return self.app.antiAlias + @antiAlias.setter + def antiAlias(self, value: bool) -> None: + self.app.antiAlias = value + @property - def constrainProportions(self): + def constrainProportions(self) -> bool: return self.app.constrainProportions + @constrainProportions.setter + def constrainProportions(self, value: bool) -> None: + self.app.constrainProportions = value + @property - def height(self): + def height(self) -> float: return self.app.height + @height.setter + def height(self, value: float) -> None: + self.app.height = value + @property - def mode(self): - return self.app.mode + def mode(self) -> OpenDocumentMode: + return OpenDocumentMode(self.app.mode) + + @mode.setter + def mode(self, value: OpenDocumentMode) -> None: + self.app.mode = value @property - def resolution(self): + def resolution(self) -> float: return self.app.resolution - @property - def width(self): - return self.app.width + @resolution.setter + def resolution(self, value: float) -> None: + self.app.resolution = value @property - def embedColorProfile(self): - return self.app.embedColorProfile + def width(self) -> float: + return self.app.width - @embedColorProfile.setter - def embedColorProfile(self, boolean): - self.app.embedColorProfile = boolean + @width.setter + def width(self, value: float) -> None: + self.app.width = value diff --git a/photoshop/api/path_item.py b/photoshop/api/path_item.py new file mode 100644 index 00000000..cbfa66aa --- /dev/null +++ b/photoshop/api/path_item.py @@ -0,0 +1,103 @@ +# Import built-in modules +from typing import TYPE_CHECKING, Iterator + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.enumerations import ColorBlendMode, PathKind, SelectionType, ToolType +from photoshop.api.solid_color import SolidColor +from photoshop.api.sub_path_item import SubPathItem + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api._document import Document + + +class PathItem(Photoshop): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) + self._flag_as_method( + "deselect", + "duplicate", + "fillPath", + "makeClippingPath", + "makeSelection", + "delete", + "select", + "strokePath", + ) + + @property + def kind(self) -> PathKind: + return PathKind(self.app.kind) + + @kind.setter + def kind(self, value: PathKind) -> None: + self.app.kind = value + + @property + def name(self) -> str: + return self.app.name + + @name.setter + def name(self, value: str) -> None: + self.app.name = value + + @property + def parent(self) -> "Document": + # Import local modules + from photoshop.api._document import Document + + return Document(self.app.parent) + + @property + def subPathItems(self) -> Iterator[SubPathItem]: + for sub_path in self.app.subPathItems: + yield SubPathItem(sub_path) + + def deselect(self) -> None: + self.app.deselect() + + def duplicate(self, name: str | None = None) -> None: + name = name if name else f"Duplicate of {self.name}" + return self.app.duplicate(name) + + def fillPath( + self, + fill_color: SolidColor, + mode: ColorBlendMode | None = None, + opacity: float = 100, + preserve_transparency: bool = False, + feather: float = 0, + whole_path: bool = True, + anti_alias: bool = True, + ) -> None: + return self.app.fillPath( + fill_color, + mode, + opacity, + preserve_transparency, + feather, + whole_path, + anti_alias, + ) + + def makeClippingPath(self, flatness: float) -> None: + return self.app.makeClippingPath(flatness) + + def makeSelection( + self, + feather: float = 0, + anti_alias: bool = True, + operation: SelectionType | None = None, + ) -> None: + return self.app.makeSelection(feather, anti_alias, operation) + + def remove(self) -> None: + return self.app.delete() + + def select(self) -> None: + return self.app.select() + + def strokePath(self, tool: ToolType, simulate_pressure: bool = False) -> None: + return self.app.strokePath(tool, simulate_pressure) diff --git a/photoshop/api/path_items.py b/photoshop/api/path_items.py new file mode 100644 index 00000000..aa59ff22 --- /dev/null +++ b/photoshop/api/path_items.py @@ -0,0 +1,34 @@ +# Import built-in modules +from typing import Sequence +from typing import TYPE_CHECKING + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.collections import CollectionOfNamedObjects +from photoshop.api.collections import CollectionOfRemovables +from photoshop.api.path_item import PathItem +from photoshop.api.sub_path_info import SubPathInfo + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api._document import Document + + +class PathItems( + CollectionOfRemovables[PathItem, int | str], + CollectionOfNamedObjects[PathItem, int | str], +): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(PathItem, parent) + self._flag_as_method("add") + + def add(self, name: str, entire_path: Sequence[SubPathInfo]) -> PathItem: + return PathItem(self.app.add(name, entire_path)) + + @property + def parent(self) -> "Document": + # Import local modules + from photoshop.api._document import Document + + return Document(self.app.parent) diff --git a/photoshop/api/path_point.py b/photoshop/api/path_point.py new file mode 100644 index 00000000..9a09984d --- /dev/null +++ b/photoshop/api/path_point.py @@ -0,0 +1,39 @@ +# Import built-in modules +from typing import TYPE_CHECKING + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.enumerations import PointKind + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api.sub_path_item import SubPathItem + + +class PathPoint(Photoshop): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) + + @property + def anchor(self) -> tuple[float, float]: + return self.app.anchor + + @property + def kind(self) -> PointKind: + return PointKind(self.app.kind) + + @property + def leftDirection(self) -> tuple[float, float]: + return self.app.leftDirection + + @property + def parent(self) -> "SubPathItem": + # Import local modules + from photoshop.api.sub_path_item import SubPathItem + + return SubPathItem(self.app.parent) + + @property + def rightDirection(self) -> tuple[float, float]: + return self.app.rightDirection diff --git a/photoshop/api/path_point_info.py b/photoshop/api/path_point_info.py new file mode 100644 index 00000000..5dad6bf0 --- /dev/null +++ b/photoshop/api/path_point_info.py @@ -0,0 +1,74 @@ +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.enumerations import PointKind + + +class PathPointInfo(Photoshop): + # The COM API somehow uses "object_name" to recognize this as an object of + # that type, which allows instantiating this object in Python and then passing + # it to COM API functions that use objects of that type. + object_name = "PathPointInfo" + + def __init__( + self, + parent: Photoshop | None = None, + anchor: tuple[float, float] | None = None, + left_direction: tuple[float, float] | None = None, + right_direction: tuple[float, float] | None = None, + kind: PointKind | None = None, + ): + super().__init__(parent=parent) + + # Each of anchor, leftDirection and rightDirection have to be set + # for the point to be valid. + if anchor: + self.anchor = anchor + + if left_direction: + self.leftDirection = left_direction + elif anchor: + self.leftDirection = anchor + + if right_direction: + self.rightDirection = right_direction + elif anchor: + self.rightDirection = anchor + + if kind: + self.kind = kind + elif left_direction or right_direction: + self.kind = PointKind.SmoothPoint + elif anchor: + self.kind = PointKind.CornerPoint + + @property + def anchor(self) -> tuple[float, float]: + return self.app.anchor + + @anchor.setter + def anchor(self, value: tuple[float, float]) -> None: + self.app.anchor = value + + @property + def kind(self) -> PointKind: + return PointKind(self.app.kind) + + @kind.setter + def kind(self, value: PointKind) -> None: + self.app.kind = value + + @property + def leftDirection(self) -> tuple[float, float]: + return self.app.leftDirection + + @leftDirection.setter + def leftDirection(self, value: tuple[float, float]) -> None: + self.app.leftDirection = value + + @property + def rightDirection(self) -> tuple[float, float]: + return self.app.rightDirection + + @rightDirection.setter + def rightDirection(self, value: tuple[float, float]) -> None: + self.app.rightDirection = value diff --git a/photoshop/api/protocols.py b/photoshop/api/protocols.py new file mode 100644 index 00000000..989b155c --- /dev/null +++ b/photoshop/api/protocols.py @@ -0,0 +1,42 @@ +# Import built-in modules +from typing import Protocol +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api._document import Document + + +class BaseProtocol(Protocol): + @property + def typename(self) -> str: + ... + + +class HistoryState(BaseProtocol, Protocol): + @property + def name(self) -> str: + ... + + @property + def parent(self) -> "Document": + ... + + @property + def snapshot(self) -> bool: + ... + + +class MeasurementScale(BaseProtocol, Protocol): + logicalLength: float + logicalUnits: str + pixelLength: int + + +class XMPMetadata(BaseProtocol, Protocol): + @property + def parent(self) -> "Document": + ... + + rawData: str diff --git a/photoshop/api/save_options/bmp.py b/photoshop/api/save_options/bmp.py index 8a1c1ae8..6493e00a 100644 --- a/photoshop/api/save_options/bmp.py +++ b/photoshop/api/save_options/bmp.py @@ -2,6 +2,8 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import BMPDepthType +from photoshop.api.enumerations import OperatingSystem class BMPSaveOptions(Photoshop): @@ -9,19 +11,51 @@ class BMPSaveOptions(Photoshop): object_name = "BMPSaveOptions" - def __init__(self): + def __init__(self) -> None: super().__init__() @property - def alphaChannels(self): + def alphaChannels(self) -> bool: """State to save the alpha channels.""" return self.app.alphaChannels @alphaChannels.setter - def alphaChannels(self, value): + def alphaChannels(self, value: bool) -> None: """Sets whether to save the alpha channels or not. Args: """ self.app.alphaChannels = value + + @property + def depth(self) -> BMPDepthType: + return BMPDepthType(self.app.depth) + + @depth.setter + def depth(self, value: BMPDepthType) -> None: + self.app.depth = value + + @property + def flipRowOrder(self) -> bool: + return self.app.flipRowOrder + + @flipRowOrder.setter + def flipRowOrder(self, value: bool) -> None: + self.app.flipRowOrder = value + + @property + def osType(self) -> OperatingSystem: + return OperatingSystem(self.app.osType) + + @osType.setter + def osType(self, value: OperatingSystem) -> None: + self.app.osType = value + + @property + def rleCompression(self) -> bool: + return self.app.rleCompression + + @rleCompression.setter + def rleCompression(self, value: bool) -> None: + self.app.rleCompression = value diff --git a/photoshop/api/save_options/eps.py b/photoshop/api/save_options/eps.py index d44aefb6..ba69fa25 100644 --- a/photoshop/api/save_options/eps.py +++ b/photoshop/api/save_options/eps.py @@ -1,5 +1,7 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import PreviewType +from photoshop.api.enumerations import SaveEncoding class EPSSaveOptions(Photoshop): @@ -11,7 +13,7 @@ class EPSSaveOptions(Photoshop): object_name = "EPSSaveOptions" - def __init__(self): + def __init__(self) -> None: super().__init__() @property @@ -20,56 +22,56 @@ def embedColorProfile(self) -> bool: return self.app.embedColorProfile @embedColorProfile.setter - def embedColorProfile(self, boolean: bool): + def embedColorProfile(self, boolean: bool) -> None: """True to embed the color profile in this document.""" self.app.embedColorProfile = boolean @property - def encoding(self): - return self.app.encoding + def encoding(self) -> SaveEncoding: + return SaveEncoding(self.app.encoding) @encoding.setter - def encoding(self, value: bool): + def encoding(self, value: SaveEncoding) -> None: self.app.encoding = value @property - def halftoneScreen(self): + def halftoneScreen(self) -> bool: return self.app.halftoneScreen @halftoneScreen.setter - def halftoneScreen(self, value: bool): + def halftoneScreen(self, value: bool) -> None: self.app.halftoneScreen = value @property - def interpolation(self): + def interpolation(self) -> bool: return self.app.interpolation @interpolation.setter - def interpolation(self, value: bool): + def interpolation(self, value: bool) -> None: self.app.interpolation = value @property - def preview(self): - return self.app.preview + def preview(self) -> PreviewType: + return PreviewType(self.app.preview) @preview.setter - def preview(self, value: bool): + def preview(self, value: PreviewType) -> None: self.app.preview = value @property - def psColorManagement(self): + def psColorManagement(self) -> bool: return self.app.psColorManagement @psColorManagement.setter - def psColorManagement(self, value: bool): + def psColorManagement(self, value: bool) -> None: self.app.psColorManagement = value @property - def transferFunction(self): + def transferFunction(self) -> bool: return self.app.transferFunction @transferFunction.setter - def transferFunction(self, value: bool): + def transferFunction(self, value: bool) -> None: self.app.transferFunction = value @property @@ -78,17 +80,17 @@ def transparentWhites(self) -> bool: return self.app.transparentWhites @transparentWhites.setter - def transparentWhites(self, value: bool): + def transparentWhites(self, value: bool) -> None: """True to display white areas as transparent""" self.app.transparentWhites = value @property - def vectorData(self): + def vectorData(self) -> bool: """True to include vector data.""" return self.app.vectorData @vectorData.setter - def vectorData(self, value: bool): + def vectorData(self, value: bool) -> None: """True to include vector data. Valid only if the document includes vector data (text). diff --git a/photoshop/api/save_options/gif.py b/photoshop/api/save_options/gif.py index df667305..3eaff8fb 100644 --- a/photoshop/api/save_options/gif.py +++ b/photoshop/api/save_options/gif.py @@ -1,5 +1,9 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import DitherType +from photoshop.api.enumerations import ForcedColors +from photoshop.api.enumerations import MatteType +from photoshop.api.enumerations import PaletteType class GIFSaveOptions(Photoshop): @@ -7,77 +11,77 @@ class GIFSaveOptions(Photoshop): object_name = "GIFSaveOptions" - def __init__(self): + def __init__(self) -> None: super().__init__() @property - def colors(self): + def colors(self) -> int: return self.app.color @colors.setter - def colors(self, value): + def colors(self, value: int) -> None: self.app.colors = value @property - def dither(self): - return self.app.dither + def dither(self) -> DitherType: + return DitherType(self.app.dither) @dither.setter - def dither(self, value): + def dither(self, value: DitherType) -> None: self.app.dither = value @property - def ditherAmount(self): + def ditherAmount(self) -> int: return self.app.ditherAmount @ditherAmount.setter - def ditherAmount(self, value): + def ditherAmount(self, value: int) -> None: self.app.ditherAmount = value @property - def forced(self): - return self.app.forced + def forced(self) -> ForcedColors: + return ForcedColors(self.app.forced) @forced.setter - def forced(self, value): + def forced(self, value: ForcedColors) -> None: self.app.forced = value @property - def interlaced(self): + def interlaced(self) -> bool: return self.app.interlaced @interlaced.setter - def interlaced(self, value): + def interlaced(self, value: bool) -> None: self.app.interlaced = value @property - def matte(self): - return self.app.matte + def matte(self) -> MatteType: + return MatteType(self.app.matte) @matte.setter - def matte(self, value): + def matte(self, value: MatteType) -> None: self.app.matte = value @property - def palette(self): - return self.app.palette + def palette(self) -> PaletteType: + return PaletteType(self.app.palette) @palette.setter - def palette(self, value): + def palette(self, value: PaletteType) -> None: self.app.palette = value @property - def preserveExactColors(self): + def preserveExactColors(self) -> bool: return self.app.preserveExactColors @preserveExactColors.setter - def preserveExactColors(self, value): + def preserveExactColors(self, value: bool) -> None: self.app.preserveExactColors = value @property - def transparency(self): + def transparency(self) -> bool: return self.app.transparency @transparency.setter - def transparency(self, value): + def transparency(self, value: bool) -> None: self.app.transparency = value diff --git a/photoshop/api/save_options/jpg.py b/photoshop/api/save_options/jpg.py index b54a3353..c2d7d0e9 100644 --- a/photoshop/api/save_options/jpg.py +++ b/photoshop/api/save_options/jpg.py @@ -1,5 +1,6 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import FormatOptionsType from photoshop.api.enumerations import MatteType @@ -8,51 +9,56 @@ class JPEGSaveOptions(Photoshop): object_name = "JPEGSaveOptions" - def __init__(self, quality=5, embedColorProfile=True, matte=MatteType.NoMatte): + def __init__( + self, + quality: int = 5, + embedColorProfile: bool = True, + matte: MatteType = MatteType.NoMatte, + ): super().__init__() self.quality = quality self.embedColorProfile = embedColorProfile self.matte = matte @property - def quality(self): + def quality(self) -> int: return self.app.quality @quality.setter - def quality(self, value): + def quality(self, value: int) -> None: self.app.quality = value @property - def formatOptions(self): + def formatOptions(self) -> FormatOptionsType: """The download format to use.""" - return self.app.formatOptions + return FormatOptionsType(self.app.formatOptions) @formatOptions.setter - def formatOptions(self, value): + def formatOptions(self, value: FormatOptionsType) -> None: self.app.formatOptions = value @property - def embedColorProfile(self): + def embedColorProfile(self) -> bool: return self.app.embedColorProfile @embedColorProfile.setter - def embedColorProfile(self, value): + def embedColorProfile(self, value: bool) -> None: self.app.embedColorProfile = value @property - def matte(self): + def matte(self) -> MatteType: """The color to use to fill anti-aliased edges adjacent to transparent""" - return self.app.matte + return MatteType(self.app.matte) @matte.setter - def matte(self, value): + def matte(self, value: MatteType) -> None: self.app.matte = value @property - def scans(self): + def scans(self) -> int: return self.app.scans @scans.setter - def scans(self, value): + def scans(self, value: int) -> None: self.app.scans = value diff --git a/photoshop/api/save_options/pdf.py b/photoshop/api/save_options/pdf.py index fdc4fb18..28dc772a 100644 --- a/photoshop/api/save_options/pdf.py +++ b/photoshop/api/save_options/pdf.py @@ -6,8 +6,10 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import PDFCompatibilityType from photoshop.api.enumerations import PDFEncodingType from photoshop.api.enumerations import PDFResampleType +from photoshop.api.enumerations import PDFStandardType from photoshop.api.errors import COMError @@ -17,80 +19,129 @@ class PDFSaveOptions(Photoshop): object_name = "PDFSaveOptions" - def __init__(self, **kwargs): + def __init__( + self, + alphaChannels: bool = False, + annotations: bool = True, + colorConversion: bool = False, + convertToEightBit: bool = True, + description: str = "No description.", + destinationProfile: str | None = None, + downSample: PDFResampleType = PDFResampleType.NoResample, + downSampleSize: float | None = None, + downSampleSizeLimit: float | None = None, + embedColorProfile: bool = True, + embedThumbnail: bool = False, + encoding: PDFEncodingType = PDFEncodingType.PDFZip, + jpegQuality: int = 12, + layers: bool = False, + optimizeForWeb: bool = False, + outputCondition: str | None = None, + outputConditionID: str | None = None, + PDFCompatibility: PDFCompatibilityType | None = None, + PDFStandard: PDFStandardType | None = None, + preserveEditing: bool = False, + presetFile: str | None = None, + profileInclusionPolicy: bool | None = None, + registryName: str | None = None, + spotColors: bool | None = None, + tileSize: int | None = None, + view: bool = False, + ): super().__init__() - self.layers = False - self.jpegQuality = 12 - self.alphaChannels = False - self.embedThumbnail = True - self.view = False - self.annotations = True - self.colorConversion = False - self.convertToEightBit = True - self.description = "No description." - self.encoding_types = PDFEncodingType - self.downSample = PDFResampleType.NoResample - self.embedColorProfile = True - if kwargs: - if "encoding" in kwargs: - self.encoding = kwargs.get("encoding", self.encoding_types.PDFJPEG) - for key, value in kwargs.items(): - setattr(self, key, value) + self.alphaChannels = alphaChannels + self.annotations = annotations + self.colorConversion = colorConversion + self.convertToEightBit = convertToEightBit + self.description = description + if destinationProfile is not None: + self.destinationProfile + self.downSample = downSample + if downSampleSize is not None: + self.downSampleSize = downSampleSize + if downSampleSizeLimit is not None: + self.downSampleSizeLimit = downSampleSizeLimit + self.embedColorProfile = embedColorProfile + self.embedThumbnail = embedThumbnail + self.encoding = encoding + self.jpegQuality = jpegQuality + self.layers = layers + self.optimizeForWeb = optimizeForWeb + if outputCondition is not None: + self.outputCondition = outputCondition + if outputConditionID is not None: + self.outputConditionID = outputConditionID + if PDFCompatibility is not None: + self.PDFCompatibility = PDFCompatibility + if PDFStandard is not None: + self.PDFStandard = PDFStandard + self.preserveEditing = preserveEditing + if presetFile is not None: + self.presetFile = presetFile + if profileInclusionPolicy is not None: + self.profileInclusionPolicy = profileInclusionPolicy + if registryName is not None: + self.registryName = registryName + if spotColors is not None: + self.spotColors = spotColors + if tileSize is not None: + self.tileSize = tileSize + self.view = view @property - def alphaChannels(self): + def alphaChannels(self) -> bool: """True to save the alpha channels with the file.""" return self.app.alphaChannels @alphaChannels.setter - def alphaChannels(self, value): + def alphaChannels(self, value: bool) -> None: """True to save the alpha channels with the file.""" self.app.alphaChannels = value @property - def annotations(self): + def annotations(self) -> bool: """If true, the annotations are saved.""" return self.app.anotations @annotations.setter - def annotations(self, value): + def annotations(self, value: bool) -> None: """If true, the annotations are saved.""" self.app.annotations = value @property - def colorConversion(self): + def colorConversion(self) -> bool: """If true, converts the color profile to the destination profile.""" return self.app.colorConversion @colorConversion.setter - def colorConversion(self, value): + def colorConversion(self, value: bool) -> None: """If true, converts the color profile to the destination profile.""" self.app.colorConversion = value @property - def convertToEightBit(self): + def convertToEightBit(self) -> bool: """If true, converts a 16-bit image to 8-bit for better compatibility with other applications.""" return self.app.convertToEightBit @convertToEightBit.setter - def convertToEightBit(self, value): + def convertToEightBit(self, value: bool) -> None: """If true, converts a 16-bit image to 8-bit for better compatibility with other applications.""" self.app.convertToEightBit = value @property - def description(self): + def description(self) -> str: """Description of the save options in use.""" return self.app.description @description.setter - def description(self, text): + def description(self, text: str) -> None: """Description of the save options in use.""" self.app.description = text @property - def destinationProfile(self): + def destinationProfile(self) -> str: """Describes the final RGB or CMYK output device, such as a monitor or press standard.""" try: @@ -101,23 +152,23 @@ def destinationProfile(self): ) @destinationProfile.setter - def destinationProfile(self, value): + def destinationProfile(self, value: str) -> None: """Describes the final RGB or CMYK output device, such as a monitor or press standard.""" self.app.destinationProfile = value @property - def downSample(self): + def downSample(self) -> PDFResampleType: """The downsample method to use.""" - return self.app.downSample + return PDFResampleType(self.app.downSample) @downSample.setter - def downSample(self, value): + def downSample(self, value: PDFResampleType) -> None: """The downsample method to use.""" self.app.downSample = value @property - def downSampleSize(self): + def downSampleSize(self) -> float: """The size (in pixels per inch) to downsample images to if they exceed the value specified for down sample size limit.""" try: @@ -128,13 +179,13 @@ def downSampleSize(self): ) @downSampleSize.setter - def downSampleSize(self, value): + def downSampleSize(self, value: float) -> None: """The size (in pixels per inch) to downsample images to if they exceed the value specified for ‘down sample size limit’.""" self.app.downSampleSize = value @property - def downSampleSizeLimit(self): + def downSampleSizeLimit(self) -> float: """Limits downsampling or subsampling to images that exceed this value (in pixels per inch).""" try: @@ -145,51 +196,51 @@ def downSampleSizeLimit(self): ) @downSampleSizeLimit.setter - def downSampleSizeLimit(self, value: float): + def downSampleSizeLimit(self, value: float) -> None: """Limits downsampling or subsampling to images that exceed this value (in pixels per inch).""" self.app.downSampleSizeLimit = value @property - def embedColorProfile(self): + def embedColorProfile(self) -> bool: """If true, the color profile is embedded in the document.""" return self.app.embedColorProfile @embedColorProfile.setter - def embedColorProfile(self, value: bool): + def embedColorProfile(self, value: bool) -> None: """If true, the color profile is embedded in the document.""" self.app.embedColorProfile = value @property - def embedThumbnail(self): + def embedThumbnail(self) -> bool: """If true, includes a small preview image in Acrobat.""" return self.app.embedThumbnail @embedThumbnail.setter - def embedThumbnail(self, value: bool): + def embedThumbnail(self, value: bool) -> None: """If true, includes a small preview image in Acrobat.""" self.app.embedThumbnail = value @property - def encoding(self): + def encoding(self) -> PDFEncodingType: """The encoding method to use.""" try: - return self.app.encoding + return PDFEncodingType(self.app.encoding) except COMError: - return self.encoding_types.PDFJPEG + return PDFEncodingType.PDFJPEG @encoding.setter - def encoding(self, value: str): + def encoding(self, value: PDFEncodingType) -> None: """The encoding method to use.""" self.app.encoding = value @property - def jpegQuality(self): + def jpegQuality(self) -> int: """Get the quality of the produced image.""" return self.app.jpegQuality @jpegQuality.setter - def jpegQuality(self, quality: int): + def jpegQuality(self, quality: int) -> None: """Set the quality of the produced image. Valid only for JPEG-encoded PDF documents. Range: 0 to 12. @@ -198,39 +249,39 @@ def jpegQuality(self, quality: int): self.app.jpegQuality = quality @property - def layers(self): + def layers(self) -> bool: """If true, the layers are saved.""" return self.app.layers @layers.setter - def layers(self, value: bool): + def layers(self, value: bool) -> None: """If true, the layers are saved.""" self.app.layers = value @property - def optimizeForWeb(self): + def optimizeForWeb(self) -> bool: """If true, improves performance of PDFs on Web servers.""" return self.app.optimizeForWeb @optimizeForWeb.setter - def optimizeForWeb(self, value: bool): + def optimizeForWeb(self, value: bool) -> None: """If true, improves performance of PDFs on Web servers.""" self.app.optimizeForWeb = value @property - def outputCondition(self): + def outputCondition(self) -> str: """An optional comment field for inserting descriptions of the output condition. The text is stored in the PDF/X file.""" return self.app.outputCondition @outputCondition.setter - def outputCondition(self, value): + def outputCondition(self, value: str) -> None: """An optional comment field for inserting descriptions of the output condition. The text is stored in the PDF/X file.""" self.app.outputCondition = value @property - def outputConditionID(self): + def outputConditionID(self) -> str: """The identifier for the output condition.""" try: return self.app.outputConditionID @@ -240,12 +291,28 @@ def outputConditionID(self): ) @outputConditionID.setter - def outputConditionID(self, value): + def outputConditionID(self, value: str) -> None: """The identifier for the output condition.""" self.app.outputConditionID = value @property - def preserveEditing(self): + def PDFCompatibility(self) -> PDFCompatibilityType: + return PDFCompatibilityType(self.app.PDFCompatibility) + + @PDFCompatibility.setter + def PDFCompatibility(self, value: PDFCompatibilityType) -> None: + self.app.PDFCompatibility = value + + @property + def PDFStandard(self) -> PDFStandardType: + return PDFStandardType(self.app.PDFStandard) + + @PDFStandard.setter + def PDFStandard(self, value: PDFStandardType) -> None: + self.app.PDFStandard = value + + @property + def preserveEditing(self) -> bool: """If true, allows users to reopen the PDF in Photoshop with native Photoshop data intact.""" try: @@ -256,13 +323,13 @@ def preserveEditing(self): ) @preserveEditing.setter - def preserveEditing(self, value): + def preserveEditing(self, value: bool) -> None: """If true, allows users to reopen the PDF in Photoshop with native Photoshop data intact.""" self.app.preserveEditing = value @property - def presetFile(self): + def presetFile(self) -> str: """The preset file to use for settings; overrides other settings.""" try: return self.app.presetFile @@ -272,12 +339,12 @@ def presetFile(self): ) @presetFile.setter - def presetFile(self, file_name): + def presetFile(self, file_name: str) -> None: """The preset file to use for settings; overrides other settings.""" self.app.presetFile = file_name @property - def profileInclusionPolicy(self): + def profileInclusionPolicy(self) -> bool: """If true, shows which profiles to include.""" try: return self.app.profileInclusionPolicy @@ -287,12 +354,12 @@ def profileInclusionPolicy(self): ) @profileInclusionPolicy.setter - def profileInclusionPolicy(self, value): + def profileInclusionPolicy(self, value: bool) -> None: """If true, shows which profiles to include.""" self.app.profileInclusionPolicy = value @property - def registryName(self): + def registryName(self) -> str: """The URL where the output condition is registered.""" try: return self.app.registryName @@ -302,12 +369,12 @@ def registryName(self): ) @registryName.setter - def registryName(self, value): + def registryName(self, value: str) -> None: """The URL where the output condition is registered.""" self.app.registryName = value @property - def spotColors(self): + def spotColors(self) -> bool: """If true, the spot colors are saved.""" try: return self.app.spotColors @@ -317,12 +384,12 @@ def spotColors(self): ) @spotColors.setter - def spotColors(self, value): + def spotColors(self, value: bool) -> None: """If true, the spot colors are saved.""" self.app.spotColors = value @property - def tileSize(self): + def tileSize(self) -> int: """The compression option. Valid only when encoding is JPEG2000.""" try: return self.app.tileSize @@ -332,25 +399,25 @@ def tileSize(self): ) @tileSize.setter - def tileSize(self, value): + def tileSize(self, value: int) -> None: """The compression option. Valid only when encoding is JPEG2000.""" if self.encoding not in ( - self.encoding_types.PDFJPEG2000HIGH, - self.encoding_types.PDFJPEG2000LOSSLESS, - self.encoding_types.PDFJPEG2000MED, - self.encoding_types.PDFJPEG2000MEDLOW, - self.encoding_types.PDFJPEG2000LOW, - self.encoding_types.PDFJPEG2000MEDHIGH, + PDFEncodingType.PDFJPEG2000HIGH, + PDFEncodingType.PDFJPEG2000LOSSLESS, + PDFEncodingType.PDFJPEG2000MED, + PDFEncodingType.PDFJPEG2000MEDLOW, + PDFEncodingType.PDFJPEG2000LOW, + PDFEncodingType.PDFJPEG2000MEDHIGH, ): raise ValueError("tileSize only work in JPEG2000. Please " "change PDFSaveOptions.encoding to JPEG2000.") self.app.tileSize = value @property - def view(self): + def view(self) -> bool: """If true, opens the saved PDF in Acrobat.""" return self.app.view @view.setter - def view(self, value): + def view(self, value: bool) -> None: """If true, opens the saved PDF in Acrobat.""" self.app.view = value diff --git a/photoshop/api/save_options/png.py b/photoshop/api/save_options/png.py index acf3481a..bd5c45ad 100644 --- a/photoshop/api/save_options/png.py +++ b/photoshop/api/save_options/png.py @@ -1,5 +1,6 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.colors.rgb import RGBColor from photoshop.api.enumerations import ColorReductionType from photoshop.api.enumerations import DitherType from photoshop.api.enumerations import SaveDocumentType @@ -10,20 +11,19 @@ class ExportOptionsSaveForWeb(Photoshop): object_name = "ExportOptionsSaveForWeb" - def __init__(self): + def __init__(self) -> None: super().__init__() - self._format = SaveDocumentType.PNGSave # Default to PNG self.PNG8 = False # Sets it to PNG-24 bit @property def format(self) -> SaveDocumentType: """The file format to use. One of the SaveDocumentType constants.""" - return self._format + return SaveDocumentType(self.app.format) @format.setter - def format(self, value: SaveDocumentType): + def format(self, value: SaveDocumentType) -> None: """Set the file format to use.""" - self._format = value + self.app.format = value @property def PNG8(self) -> bool: @@ -31,7 +31,7 @@ def PNG8(self) -> bool: return self.app.PNG8 @PNG8.setter - def PNG8(self, value: bool): + def PNG8(self, value: bool) -> None: self.app.PNG8 = value @property @@ -40,16 +40,16 @@ def blur(self) -> float: return self.app.blur @blur.setter - def blur(self, value: float): + def blur(self, value: float) -> None: self.app.blur = value @property def colorReduction(self) -> ColorReductionType: """The color reduction algorithm.""" - return self.app.colorReduction + return ColorReductionType(self.app.colorReduction) @colorReduction.setter - def colorReduction(self, value: ColorReductionType): + def colorReduction(self, value: ColorReductionType) -> None: self.app.colorReduction = value @property @@ -58,25 +58,65 @@ def colors(self) -> int: return self.app.colors @colors.setter - def colors(self, value: int): + def colors(self, value: int) -> None: self.app.colors = value @property def dither(self) -> DitherType: """The type of dither to use.""" - return self.app.dither + return DitherType(self.app.dither) @dither.setter - def dither(self, value: DitherType): + def dither(self, value: DitherType) -> None: self.app.dither = value + @property + def ditherAmount(self) -> int: + return self.app.ditherAmount + + @ditherAmount.setter + def ditherAmount(self, value: int) -> None: + self.app.ditherAmount = value + + @property + def includeProfile(self) -> bool: + return self.app.includeProfile + + @includeProfile.setter + def includeProfile(self, value: bool) -> None: + self.app.includeProfile = value + + @property + def interlaced(self) -> bool: + return self.app.interlaced + + @interlaced.setter + def interlaced(self, value: bool) -> None: + self.app.interlaced = value + + @property + def lossy(self) -> int: + return self.app.lossy + + @lossy.setter + def lossy(self, value: int) -> None: + self.app.lossy = value + + @property + def matteColor(self) -> RGBColor: + return self.app.matteColor + + @matteColor.setter + def matteColor(self, value: RGBColor) -> None: + self.app.matteColor = value + @property def optimized(self) -> bool: """If true, optimization is enabled.""" return self.app.optimized @optimized.setter - def optimized(self, value: bool): + def optimized(self, value: bool) -> None: self.app.optimized = value @property @@ -85,16 +125,48 @@ def quality(self) -> int: return self.app.quality @quality.setter - def quality(self, value: int): + def quality(self, value: int) -> None: self.app.quality = value + @property + def transparency(self) -> bool: + return self.app.transparency + + @transparency.setter + def transparency(self, value: bool) -> None: + self.app.transparency = value + + @property + def transparencyAmount(self) -> int: + return self.app.transparencyAmount + + @transparencyAmount.setter + def transparencyAmount(self, value: int) -> None: + self.app.transparencyAmount = value + + @property + def transparencyDither(self) -> DitherType: + return DitherType(self.app.transparencyDither) + + @transparencyDither.setter + def transparencyDither(self, value: DitherType) -> None: + self.app.transparencyDither = value + + @property + def webSnap(self) -> int: + return self.app.webSnap + + @webSnap.setter + def webSnap(self, value: int) -> None: + self.app.webSnap = value + class PNGSaveOptions(Photoshop): """Options for saving file as PNG.""" object_name = "PNGSaveOptions" - def __init__(self, interlaced: bool = False, compression: int = 6): + def __init__(self, interlaced: bool = False, compression: int = 6) -> None: super().__init__() self.interlaced = interlaced self.compression = compression @@ -104,7 +176,7 @@ def interlaced(self) -> bool: return self.app.interlaced @interlaced.setter - def interlaced(self, value: bool): + def interlaced(self, value: bool) -> None: self.app.interlaced = value @property @@ -112,5 +184,5 @@ def compression(self) -> int: return self.app.compression @compression.setter - def compression(self, value: int): + def compression(self, value: int) -> None: self.app.compression = value diff --git a/photoshop/api/save_options/psd.py b/photoshop/api/save_options/psd.py index 2cb79233..dd90e2e8 100644 --- a/photoshop/api/save_options/psd.py +++ b/photoshop/api/save_options/psd.py @@ -7,50 +7,50 @@ class PhotoshopSaveOptions(Photoshop): object_name = "PhotoshopSaveOptions" - def __int__(self): + def __int__(self) -> None: super().__init__() @property - def alphaChannels(self): + def alphaChannels(self) -> bool: """If true, the alpha channels are saved.""" return self.app.alphaChannels() @alphaChannels.setter - def alphaChannels(self, value): + def alphaChannels(self, value: bool) -> None: self.app.alphaChannels = value @property - def annotations(self): + def annotations(self) -> bool: """If true, the annotations are saved.""" return self.app.annotations() @annotations.setter - def annotations(self, value): + def annotations(self, value: bool) -> None: self.app.annotations = value @property - def embedColorProfile(self): + def embedColorProfile(self) -> bool: """If true, the color profile is embedded in the document.""" return self.app.embedColorProfile() @embedColorProfile.setter - def embedColorProfile(self, value): + def embedColorProfile(self, value: bool) -> None: self.app.embedColorProfile = value @property - def layers(self): + def layers(self) -> bool: """If true, the layers are saved.""" return self.app.layers() @layers.setter - def layers(self, value): + def layers(self, value: bool) -> None: self.app.layers = value @property - def spotColors(self): + def spotColors(self) -> bool: """If true, spot colors are saved.""" return self.app.spotColors() @spotColors.setter - def spotColors(self, value): + def spotColors(self, value: bool) -> None: self.app.spotColors = value diff --git a/photoshop/api/save_options/tag.py b/photoshop/api/save_options/tag.py index 622170b6..16babd36 100644 --- a/photoshop/api/save_options/tag.py +++ b/photoshop/api/save_options/tag.py @@ -8,30 +8,30 @@ class TargaSaveOptions(Photoshop): object_name = "TargaSaveOptions" - def __int__(self): + def __int__(self) -> None: super().__init__() @property - def alphaChannels(self): + def alphaChannels(self) -> bool: """If true, the alpha channels are saved.""" return self.app.alphaChannels @alphaChannels.setter - def alphaChannels(self, value): + def alphaChannels(self, value: bool) -> None: self.app.alphaChannels = value @property - def resolution(self): - return self.app.resolution + def resolution(self) -> TargaBitsPerPixels: + return TargaBitsPerPixels(self.app.resolution) @resolution.setter - def resolution(self, value: TargaBitsPerPixels = TargaBitsPerPixels.Targa24Bits): + def resolution(self, value: TargaBitsPerPixels) -> None: self.app.resolution = value @property - def rleCompression(self): + def rleCompression(self) -> bool: return self.app.rleCompression @rleCompression.setter - def rleCompression(self, value): + def rleCompression(self, value: bool) -> None: self.app.rleCompression = value diff --git a/photoshop/api/save_options/tif.py b/photoshop/api/save_options/tif.py index 79f5379c..566dcf1f 100644 --- a/photoshop/api/save_options/tif.py +++ b/photoshop/api/save_options/tif.py @@ -1,5 +1,8 @@ # Import local modules from photoshop.api._core import Photoshop +from photoshop.api.enumerations import ByteOrderType +from photoshop.api.enumerations import LayerCompressionType +from photoshop.api.enumerations import TiffEncodingType class TiffSaveOptions(Photoshop): @@ -7,68 +10,68 @@ class TiffSaveOptions(Photoshop): object_name = "TiffSaveOptions" - def __int__(self): + def __int__(self) -> None: super().__init__() @property - def alphaChannels(self): + def alphaChannels(self) -> bool: """If true, the alpha channels are saved.""" return self.app.alphaChannels @alphaChannels.setter - def alphaChannels(self, value): + def alphaChannels(self, value: bool) -> None: self.app.alphaChannels = value @property - def annotations(self): + def annotations(self) -> bool: """If true, the annotations are saved.""" return self.app.annotations @annotations.setter - def annotations(self, value): + def annotations(self, value: bool) -> None: self.app.annotations = value @property - def byteOrder(self): + def byteOrder(self) -> ByteOrderType: """The order in which the bytes will be read. Default: Mac OS when running in Mac OS, and IBM PC when running in Windows. """ - return self.app.byteOrder + return ByteOrderType(self.app.byteOrder) @byteOrder.setter - def byteOrder(self, value): + def byteOrder(self, value: ByteOrderType) -> None: self.app.byteOrder = value @property - def embedColorProfile(self): + def embedColorProfile(self) -> bool: """If true, the color profile is embedded in the document.""" return self.app.embedColorProfile @embedColorProfile.setter - def embedColorProfile(self, value): + def embedColorProfile(self, value: bool) -> None: self.app.embedColorProfile = value @property - def imageCompression(self): + def imageCompression(self) -> TiffEncodingType: """The compression type.""" - return self.app.imageCompression + return TiffEncodingType(self.app.imageCompression) @imageCompression.setter - def imageCompression(self, value): + def imageCompression(self, value: TiffEncodingType) -> None: self.app.imageCompression = value @property - def interleaveChannels(self): + def interleaveChannels(self) -> bool: """If true, the channels in the image are interleaved.""" return self.app.interleaveChannels @interleaveChannels.setter - def interleaveChannels(self, value): + def interleaveChannels(self, value: bool) -> None: self.app.interleaveChannels = value @property - def jpegQuality(self): + def jpegQuality(self) -> int: """The quality of the produced image, which is inversely proportionate to the amount of JPEG compression. Valid only for JPEG compressed TIFF documents. Range: 0 to 12. @@ -76,15 +79,15 @@ def jpegQuality(self): return self.app.jpegQuality @jpegQuality.setter - def jpegQuality(self, value): + def jpegQuality(self, value: int) -> None: self.app.jpegQuality = value @property - def layerCompression(self): - return self.app.layerCompression + def layerCompression(self) -> LayerCompressionType: + return LayerCompressionType(self.app.layerCompression) @layerCompression.setter - def layerCompression(self, value): + def layerCompression(self, value: LayerCompressionType) -> None: """The method of compression to use when saving layers (as opposed to saving composite data). Valid only when `layers` = true. @@ -92,38 +95,38 @@ def layerCompression(self, value): self.app.layerCompression = value @property - def layers(self): + def layers(self) -> bool: """If true, the layers are saved.""" return self.app.layers @layers.setter - def layers(self, value): + def layers(self, value: bool) -> None: self.app.layers = value @property - def saveImagePyramid(self): + def saveImagePyramid(self) -> bool: """If true, preserves multi-resolution information.""" return self.app.saveImagePyramid @saveImagePyramid.setter - def saveImagePyramid(self, value): + def saveImagePyramid(self, value: bool) -> None: self.app.saveImagePyramid = value @property - def spotColors(self): + def spotColors(self) -> bool: """If true, spot colors are saved.""" return self.app.spotColors @spotColors.setter - def spotColors(self, value): + def spotColors(self, value: bool) -> None: self.app.spotColors = value @property - def transparency(self): + def transparency(self) -> bool: return self.app.transparency @transparency.setter - def transparency(self, value): + def transparency(self, value: bool) -> None: """If true, saves the transparency as an additional alpha channel when the file is opened in another application.""" self.app.transparency = value diff --git a/photoshop/api/solid_color.py b/photoshop/api/solid_color.py index c0ed302c..8eca4291 100644 --- a/photoshop/api/solid_color.py +++ b/photoshop/api/solid_color.py @@ -23,7 +23,7 @@ class SolidColor(Photoshop): object_name = "SolidColor" - def __init__(self, parent=None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) self._flag_as_method( "isEqual", @@ -40,19 +40,23 @@ def cmyk(self) -> CMYKColor: return CMYKColor(self.app.cmyk) @cmyk.setter - def cmyk(self, value: CMYKColor): + def cmyk(self, value: CMYKColor) -> None: self.app.cmyk = value @property def gray(self) -> GrayColor: return GrayColor(self.app.gray) + @gray.setter + def gray(self, value: GrayColor) -> None: + self.app.gray = value + @property def hsb(self) -> HSBColor: return HSBColor(self.app.hsb) @hsb.setter - def hsb(self, value: HSBColor): + def hsb(self, value: HSBColor) -> None: self.app.hsb = value @property @@ -60,7 +64,7 @@ def lab(self) -> LabColor: return LabColor(self.app.lab) @lab.setter - def lab(self, value: LabColor): + def lab(self, value: LabColor) -> None: self.app.lab = value @property @@ -69,7 +73,7 @@ def model(self) -> ColorModel: return ColorModel(self.app.model) @model.setter - def model(self, value: ColorModel): + def model(self, value: ColorModel) -> None: """The color model.""" self.app.model = value @@ -84,9 +88,9 @@ def rgb(self) -> RGBColor: return RGBColor(self.app.rgb) @rgb.setter - def rgb(self, value: RGBColor): + def rgb(self, value: RGBColor) -> None: self.app.rgb = value - def isEqual(self, color: RGBColor): + def isEqual(self, color: RGBColor) -> bool: """`SolidColor` object is visually equal to the specified color.""" return self.app.isEqual(color) diff --git a/photoshop/api/sub_path_info.py b/photoshop/api/sub_path_info.py new file mode 100644 index 00000000..e13aa5c5 --- /dev/null +++ b/photoshop/api/sub_path_info.py @@ -0,0 +1,51 @@ +# Import built-in modules +from typing import Iterator +from typing import Sequence + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.enumerations import ShapeOperation +from photoshop.api.path_point_info import PathPointInfo + + +class SubPathInfo(Photoshop): + object_name = "SubPathInfo" + + def __init__( + self, + parent: Photoshop | None = None, + entire_sub_path: Sequence[PathPointInfo] | None = None, + closed: bool = True, + operation: ShapeOperation = ShapeOperation.ShapeAdd, + ): + super().__init__(parent=parent) + + if entire_sub_path: + self.entireSubPath = entire_sub_path + self.closed = closed + self.operation = operation + + @property + def closed(self) -> bool: + return self.app.closed + + @closed.setter + def closed(self, value: bool) -> None: + self.app.closed = value + + @property + def entireSubPath(self) -> Iterator[PathPointInfo]: + for point in self.app.entireSubPath: + yield PathPointInfo(point) + + @entireSubPath.setter + def entireSubPath(self, value: Sequence[PathPointInfo]) -> None: + self.app.entireSubPath = value + + @property + def operation(self) -> ShapeOperation: + return ShapeOperation(self.app.operation) + + @operation.setter + def operation(self, value: ShapeOperation) -> None: + self.app.operation = value diff --git a/photoshop/api/sub_path_item.py b/photoshop/api/sub_path_item.py new file mode 100644 index 00000000..95e62dff --- /dev/null +++ b/photoshop/api/sub_path_item.py @@ -0,0 +1,38 @@ +# Import built-in modules +from typing import Iterator +from typing import TYPE_CHECKING + +# Import local modules +from photoshop.api._core import Photoshop +from photoshop.api.enumerations import ShapeOperation +from photoshop.api.path_point import PathPoint + + +if TYPE_CHECKING: + # Import local modules + from photoshop.api.path_item import PathItem + + +class SubPathItem(Photoshop): + def __init__(self, parent: Photoshop | None = None) -> None: + super().__init__(parent=parent) + + @property + def closed(self) -> bool: + return self.app.closed + + @property + def operation(self) -> ShapeOperation: + return ShapeOperation(self.app.operation) + + @property + def parent(self) -> "PathItem": + # Import local modules + from photoshop.api.path_item import PathItem + + return PathItem(self.app.parent) + + @property + def pathPoints(self) -> Iterator[PathPoint]: + for point in self.app.pathPoints: + yield PathPoint(point) diff --git a/photoshop/api/text_font.py b/photoshop/api/text_font.py index c5e83f82..1bc81488 100644 --- a/photoshop/api/text_font.py +++ b/photoshop/api/text_font.py @@ -5,7 +5,7 @@ class TextFont(Photoshop): """An installed font.""" - def __init__(self, parent=None): + def __init__(self, parent: Photoshop | None = None) -> None: super().__init__(parent=parent) @property @@ -14,16 +14,16 @@ def family(self) -> str: return self.app.family @property - def name(self): + def name(self) -> str: """The name of the font.""" return self.app.name @property - def postScriptName(self): + def postScriptName(self) -> str: """The PostScript name of the font.""" return self.app.postScriptName @property - def style(self): + def style(self) -> str: """The font style.""" return self.app.style diff --git a/photoshop/api/text_item.py b/photoshop/api/text_item.py index 38b338d3..2a0dba85 100644 --- a/photoshop/api/text_item.py +++ b/photoshop/api/text_item.py @@ -1,22 +1,33 @@ +# Import built-in modules +from typing import TYPE_CHECKING + # Import local modules from photoshop.api._core import Photoshop from photoshop.api.enumerations import AntiAlias from photoshop.api.enumerations import AutoKernType +from photoshop.api.enumerations import Case from photoshop.api.enumerations import Direction from photoshop.api.enumerations import Justification from photoshop.api.enumerations import Language from photoshop.api.enumerations import StrikeThruType from photoshop.api.enumerations import TextComposer from photoshop.api.enumerations import TextType +from photoshop.api.enumerations import UnderlineType +from photoshop.api.enumerations import WarpStyle from photoshop.api.solid_color import SolidColor +if TYPE_CHECKING: + # Import local modules + from photoshop.api._artlayer import ArtLayer + + class TextItem(Photoshop): """The text that is associated with the layer. Valid only when ‘kind’ is text layer.""" object_name = "Application" - def __init__(self, parent): + def __init__(self, parent: Photoshop) -> None: super().__init__(parent=parent) self._flag_as_method( "convertToShape", @@ -24,11 +35,11 @@ def __init__(self, parent): ) @property - def alternateLigatures(self): + def alternateLigatures(self) -> bool: return self.app.alternateLigatures @alternateLigatures.setter - def alternateLigatures(self, value): + def alternateLigatures(self, value: bool) -> None: self.app.alternateLigatures = value @property @@ -37,7 +48,7 @@ def antiAliasMethod(self) -> AntiAlias: return AntiAlias(self.app.antiAliasMethod) @antiAliasMethod.setter - def antiAliasMethod(self, value): + def antiAliasMethod(self, value: AntiAlias) -> None: self.app.antiAliasMethod = value @property @@ -46,15 +57,15 @@ def autoKerning(self) -> AutoKernType: return AutoKernType(self.app.autoKerning) @autoKerning.setter - def autoKerning(self, value): + def autoKerning(self, value: AutoKernType) -> None: self.app.autoKerning = value @property - def autoLeadingAmount(self): + def autoLeadingAmount(self) -> float: return self.app.autoLeadingAmount @autoLeadingAmount.setter - def autoLeadingAmount(self, value): + def autoLeadingAmount(self, value: float) -> None: """The percentage to use for auto (default) leading (in points). Valid only when useAutoLeading = True. @@ -64,21 +75,21 @@ def autoLeadingAmount(self, value): self.app.autoLeadingAmount = value @property - def baselineShift(self): + def baselineShift(self) -> float: """The unit value to use in the baseline offset of text.""" return self.app.baselineShift @baselineShift.setter - def baselineShift(self, value): + def baselineShift(self, value: float) -> None: self.app.baselineShift = value @property - def capitalization(self): + def capitalization(self) -> Case: """Gets text case.""" - return self.app.capitalization + return Case(self.app.capitalization) @capitalization.setter - def capitalization(self, value): + def capitalization(self, value: Case) -> None: """Sets text case.""" self.app.capitalization = value @@ -88,7 +99,7 @@ def color(self) -> SolidColor: return SolidColor(self.app.color) @color.setter - def color(self, color_value): + def color(self, color_value: SolidColor) -> None: """The color of textItem.""" self.app.color = color_value @@ -98,7 +109,7 @@ def contents(self) -> str: return self.app.contents @contents.setter - def contents(self, text: str): + def contents(self, text: str) -> None: """Set the actual text in the layer. Args: @@ -108,18 +119,18 @@ def contents(self, text: str): self.app.contents = text @property - def desiredGlyphScaling(self): + def desiredGlyphScaling(self) -> float: """The desired amount by which to scale the horizontal size of the text letters. A percentage value; at 100, the width of characters is not scaled.""" return self.app.desiredGlyphScaling @desiredGlyphScaling.setter - def desiredGlyphScaling(self, value): + def desiredGlyphScaling(self, value: float) -> None: self.app.desiredGlyphScaling = value @property - def desiredLetterScaling(self): + def desiredLetterScaling(self) -> float: """The amount of space between letters . (at 0, no space is added between letters). Equivalent to Letter Spacing in the Justification @@ -135,11 +146,11 @@ def desiredLetterScaling(self): return self.app.desiredLetterScaling @desiredLetterScaling.setter - def desiredLetterScaling(self, value): + def desiredLetterScaling(self, value: float) -> None: self.app.desiredGlyphScaling = value @property - def desiredWordScaling(self): + def desiredWordScaling(self) -> float: """ The amount (percentage) of space between words (at 100, no additional space is added @@ -158,20 +169,20 @@ def desiredWordScaling(self): return self.app.desiredWordScaling @desiredWordScaling.setter - def desiredWordScaling(self, value): + def desiredWordScaling(self, value: float) -> None: self.app.desiredWordScaling = value @property - def direction(self): + def direction(self) -> Direction: """The text orientation.""" return Direction(self.app.direction) @direction.setter - def direction(self, value): + def direction(self, value: Direction) -> None: self.app.direction = value @property - def fauxBold(self): + def fauxBold(self) -> bool: """True to use faux bold (default: false). Setting this to true is equivalent to selecting text and @@ -181,11 +192,11 @@ def fauxBold(self): return self.app.fauxBold @fauxBold.setter - def fauxBold(self, value): + def fauxBold(self, value: bool) -> None: self.app.fauxBold = value @property - def fauxItalic(self): + def fauxItalic(self) -> bool: """True to use faux italic (default: false). Setting this to true is equivalent to selecting text and @@ -195,16 +206,16 @@ def fauxItalic(self): return self.app.fauxItalic @fauxItalic.setter - def fauxItalic(self, value): + def fauxItalic(self, value: bool) -> None: self.app.fauxItalic = value @property - def firstLineIndent(self): + def firstLineIndent(self) -> float: """The amount (unit value) to indent the first line of paragraphs.""" return self.app.firstLineIndent @firstLineIndent.setter - def firstLineIndent(self, value): + def firstLineIndent(self, value: float) -> None: self.app.firstLineIndent = value @property @@ -213,7 +224,7 @@ def font(self) -> str: return self.app.font @font.setter - def font(self, text_font: str): + def font(self, text_font: str) -> None: """Set the font of this TextItem. Args: @@ -227,16 +238,16 @@ def hangingPunctuation(self) -> bool: return self.app.hangingPunctuation @hangingPunctuation.setter - def hangingPunctuation(self, value: bool): + def hangingPunctuation(self, value: bool) -> None: self.app.hangingPunctuation = value @property - def height(self): + def height(self) -> float: """int: The height of the bounding box for paragraph text.""" return self.app.height @height.setter - def height(self, value): + def height(self, value: float) -> None: self.app.height = value @property @@ -245,7 +256,7 @@ def horizontalScale(self) -> int: return self.app.horizontalScale @horizontalScale.setter - def horizontalScale(self, value: int): + def horizontalScale(self, value: int) -> None: """Set the horizontalScale of this TextItem. Args: @@ -254,122 +265,122 @@ def horizontalScale(self, value: int): self.app.horizontalScale = value @property - def hyphenateAfterFirst(self): + def hyphenateAfterFirst(self) -> int: """The number of letters after which hyphenation in word wrap is allowed.""" return self.app.hyphenateAfterFirst @hyphenateAfterFirst.setter - def hyphenateAfterFirst(self, value): + def hyphenateAfterFirst(self, value: int) -> None: self.app.hyphenateAfterFirst = value @property - def hyphenateBeforeLast(self): + def hyphenateBeforeLast(self) -> int: """The number of letters before which hyphenation in word wrap is allowed.""" return self.app.hyphenateBeforeLast @hyphenateBeforeLast.setter - def hyphenateBeforeLast(self, value): + def hyphenateBeforeLast(self, value: int) -> None: self.app.hyphenateBeforeLast = value @property - def hyphenateCapitalWords(self): + def hyphenateCapitalWords(self) -> bool: """True to allow hyphenation in word wrap of capitalized words""" return self.app.hyphenateCapitalWords @hyphenateCapitalWords.setter - def hyphenateCapitalWords(self, value): + def hyphenateCapitalWords(self, value: bool) -> None: self.app.hyphenateCapitalWords = value @property - def hyphenateWordsLongerThan(self): + def hyphenateWordsLongerThan(self) -> int: """The minimum number of letters a word must have in order for hyphenation in word wrap to be allowed.""" return self.app.hyphenateWordsLongerThan @hyphenateWordsLongerThan.setter - def hyphenateWordsLongerThan(self, value): + def hyphenateWordsLongerThan(self, value: int) -> None: self.app.hyphenateWordsLongerThan = value @property - def hyphenation(self): + def hyphenation(self) -> bool: """True to use hyphenation in word wrap.""" return self.app.hyphenation @hyphenation.setter - def hyphenation(self, value): + def hyphenation(self, value: bool) -> None: self.app.hyphenation = value @property - def hyphenationZone(self): + def hyphenationZone(self) -> float: """The distance at the end of a line that will cause a word to break in unjustified type.""" return self.app.hyphenationZone @hyphenationZone.setter - def hyphenationZone(self, value): + def hyphenationZone(self, value: float) -> None: self.app.hyphenationZone = value @property - def hyphenLimit(self): + def hyphenLimit(self) -> int: return self.app.hyphenLimit @hyphenLimit.setter - def hyphenLimit(self, value): + def hyphenLimit(self, value: int) -> None: self.app.hyphenLimit = value @property - def justification(self): + def justification(self) -> Justification: """The paragraph justification.""" return Justification(self.app.justification) @justification.setter - def justification(self, value): + def justification(self, value: Justification) -> None: self.app.justification = value @property - def kind(self): + def kind(self) -> TextType: return TextType(self.app.kind) @kind.setter - def kind(self, kind_type): + def kind(self, kind_type: TextType) -> None: self.app.kind = kind_type @property - def language(self): + def language(self) -> Language: return Language(self.app.language) @language.setter - def language(self, text): + def language(self, text: Language) -> None: self.app.language = text @property - def leading(self): + def leading(self) -> float: return self.app.leading @leading.setter - def leading(self, value): + def leading(self, value: float) -> None: self.app.leading = value @property - def leftIndent(self): + def leftIndent(self) -> float: """The amoun of space to indent text from the left.""" return self.app.leftIndent @leftIndent.setter - def leftIndent(self, value): + def leftIndent(self, value: float) -> None: self.app.leftIndent = value @property - def ligatures(self): + def ligatures(self) -> bool: """True to use ligatures.""" return self.app.ligatures @ligatures.setter - def ligatures(self, value): + def ligatures(self, value: bool) -> None: self.app.ligatures = value @property - def maximumGlyphScaling(self): + def maximumGlyphScaling(self) -> float: """The maximum amount to scale the horizontal size of the text letters (a percentage value; at 100, the width of characters is not scaled). @@ -384,11 +395,11 @@ def maximumGlyphScaling(self): return self.app.maximumGlyphScaling @maximumGlyphScaling.setter - def maximumGlyphScaling(self, value): + def maximumGlyphScaling(self, value: float) -> None: self.app.maximumGlyphScaling = value @property - def maximumLetterScaling(self): + def maximumLetterScaling(self) -> float: """The maximum amount of space to allow between letters (at 0, no space is added between letters). @@ -406,19 +417,19 @@ def maximumLetterScaling(self): return self.app.maximumLetterScaling @maximumLetterScaling.setter - def maximumLetterScaling(self, value): + def maximumLetterScaling(self, value: float) -> None: self.app.maximumLetterScaling = value @property - def maximumWordScaling(self): + def maximumWordScaling(self) -> float: return self.app.maximumWordScaling @maximumWordScaling.setter - def maximumWordScaling(self, value): + def maximumWordScaling(self, value: float) -> None: self.app.maximumWordScaling = value @property - def minimumGlyphScaling(self): + def minimumGlyphScaling(self) -> float: """The minimum amount to scale the horizontal size of the text letters (a percentage value; at 100, the width of characters is not scaled). @@ -433,11 +444,11 @@ def minimumGlyphScaling(self): return self.app.minimumGlyphScaling @minimumGlyphScaling.setter - def minimumGlyphScaling(self, value): + def minimumGlyphScaling(self, value: float) -> None: self.app.minimumGlyphScaling = value @property - def minimumLetterScaling(self): + def minimumLetterScaling(self) -> float: """The minimum amount of space to allow between letters (a percentage value; at 0, no space is removed between letters). @@ -456,11 +467,11 @@ def minimumLetterScaling(self): return self.app.minimumLetterScaling @minimumLetterScaling.setter - def minimumLetterScaling(self, value): + def minimumLetterScaling(self, value: float) -> None: self.app.minimumLetterScaling = value @property - def minimumWordScaling(self): + def minimumWordScaling(self) -> float: """The minimum amount of space to allow between words (a percentage value; at 100, no additional space is removed between words). @@ -479,11 +490,11 @@ def minimumWordScaling(self): return self.app.minimumWordScaling @minimumWordScaling.setter - def minimumWordScaling(self, value): + def minimumWordScaling(self, value: float) -> None: self.app.minimumWordScaling = value @property - def noBreak(self): + def noBreak(self) -> bool: """True to disallow line breaks in this text. Tip: When true for many consecutive characters, can @@ -494,31 +505,33 @@ def noBreak(self): return self.app.noBreak @noBreak.setter - def noBreak(self, value): + def noBreak(self, value: bool) -> None: self.app.noBreak = value @property - def oldStyle(self): + def oldStyle(self) -> bool: return self.app.oldStyle @oldStyle.setter - def oldStyle(self, value): + def oldStyle(self, value: bool) -> None: self.app.oldStyle = value @property - def parent(self): - return self.app.parent + def parent(self) -> "ArtLayer": + from ._artlayer import ArtLayer + + return ArtLayer(self.app.parent) @parent.setter - def parent(self, value): + def parent(self, value: "ArtLayer") -> None: self.app.parent = value @property - def position(self): + def position(self) -> tuple[int, int]: return self.app.position @position.setter - def position(self, array): + def position(self, array: tuple[int, int]) -> None: """The position of the origin for the text. The array must contain two values. Setting this property is basically @@ -529,15 +542,15 @@ def position(self, array): self.app.position = array @property - def rightIndent(self): + def rightIndent(self) -> float: return self.app.rightIndent @rightIndent.setter - def rightIndent(self, value): + def rightIndent(self, value: float) -> None: self.app.rightIndent = value @property - def size(self): + def size(self) -> float: """The font size in UnitValue. NOTE: Type was points for CS3 and older. @@ -546,83 +559,83 @@ def size(self): return self.app.size @size.setter - def size(self, value): + def size(self, value: float) -> None: self.app.size = value @property - def spaceAfter(self): + def spaceAfter(self) -> float: """The amount of space to use after each paragraph.""" return self.app.spaceAfter @spaceAfter.setter - def spaceAfter(self, value): + def spaceAfter(self, value: float) -> None: self.app.spaceAfter = value @property - def spaceBefore(self): + def spaceBefore(self) -> float: return self.app.spaceBefore @spaceBefore.setter - def spaceBefore(self, value): + def spaceBefore(self, value: float) -> None: self.app.spaceBefore = value @property - def strikeThru(self): + def strikeThru(self) -> StrikeThruType: """The text strike-through option to use.""" return StrikeThruType(self.app.strikeThru) @strikeThru.setter - def strikeThru(self, value): + def strikeThru(self, value: StrikeThruType) -> None: self.app.strikeThru = value @property - def textComposer(self): + def textComposer(self) -> TextComposer: return TextComposer(self.app.textComposer) @textComposer.setter - def textComposer(self, value): + def textComposer(self, value: TextComposer) -> None: self.app.textComposer = value @property - def tracking(self): + def tracking(self) -> float: return self.app.tracking @tracking.setter - def tracking(self, value): + def tracking(self, value: float) -> None: self.app.tracking = value @property - def underline(self): + def underline(self) -> UnderlineType: """The text underlining options.""" - return self.app.underline + return UnderlineType(self.app.underline) @underline.setter - def underline(self, value): + def underline(self, value: UnderlineType) -> None: self.app.underline = value @property - def useAutoLeading(self): + def useAutoLeading(self) -> bool: return self.app.useAutoLeading @useAutoLeading.setter - def useAutoLeading(self, value): + def useAutoLeading(self, value: bool) -> None: self.app.useAutoLeading = value @property - def verticalScale(self): + def verticalScale(self) -> int: return self.app.verticalScale @verticalScale.setter - def verticalScale(self, value): + def verticalScale(self, value: int) -> None: self.app.verticalScale = value @property - def warpBend(self): + def warpBend(self) -> float: """The warp bend percentage.""" return self.app.warpBend @warpBend.setter - def warpBend(self, value): + def warpBend(self, value: float) -> None: self.app.warpBend = value @property @@ -631,36 +644,36 @@ def warpDirection(self) -> Direction: return Direction(self.app.warpDirection) @warpDirection.setter - def warpDirection(self, value): + def warpDirection(self, value: Direction) -> None: self.app.warpDirection = value @property - def warpHorizontalDistortion(self): + def warpHorizontalDistortion(self) -> float: return self.app.warpHorizontalDistortion @warpHorizontalDistortion.setter - def warpHorizontalDistortion(self, value): + def warpHorizontalDistortion(self, value: float) -> None: self.app.warpHorizontalDistortion = value @property - def warpStyle(self): + def warpStyle(self) -> WarpStyle: """The style of warp to use.""" - return self.app.warpStyle + return WarpStyle(self.app.warpStyle) @warpStyle.setter - def warpStyle(self, value): + def warpStyle(self, value: WarpStyle) -> None: self.app.warpStyle = value @property - def warpVerticalDistortion(self): + def warpVerticalDistortion(self) -> float: return self.app.warpVerticalDistortion @warpVerticalDistortion.setter - def warpVerticalDistortion(self, value): + def warpVerticalDistortion(self, value: float) -> None: self.app.warpVerticalDistortion = value @property - def width(self): + def width(self) -> float: """The width of the bounding box for paragraph text. @@ -670,7 +683,7 @@ def width(self): return self.app.width @width.setter - def width(self, value: float): + def width(self, value: float) -> None: """The width of the bounding box for paragraph text. @@ -679,15 +692,15 @@ def width(self, value: float): """ self.app.width = value - def convertToShape(self): + def convertToShape(self) -> None: """Converts the text item and its containing layer to a fill layer with the text changed to a clipping path.""" - return self.app.convertToShape() + self.app.convertToShape() - def createPath(self): + def createPath(self) -> None: """Creates a clipping path from the outlines of the actual text items (such as letters or words). """ - return self.app.createPath() + self.app.createPath() diff --git a/photoshop/py.typed b/photoshop/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/photoshop/session.py b/photoshop/session.py index d5976e1b..eb5f0588 100644 --- a/photoshop/session.py +++ b/photoshop/session.py @@ -27,7 +27,11 @@ """ # Import built-in modules +from contextlib import AbstractContextManager +from os import PathLike +from types import TracebackType from typing import Any +from typing import Literal # Import local modules from photoshop.api import ActionDescriptor @@ -55,10 +59,11 @@ from photoshop.api import TiffSaveOptions from photoshop.api import enumerations from photoshop.api import errors +from photoshop.api._document import Document # pylint: disable=too-many-arguments -class Session: +class Session(AbstractContextManager["Session"]): """Session of photoshop. We can control active documents in this Session. @@ -72,11 +77,11 @@ class Session: def __init__( self, - file_path: str = None, - action: str = None, + file_path: str | PathLike[str] | None = None, + action: Literal["open", "new_document", "document_duplicate"] | None = None, callback: Any = None, auto_close: bool = False, - ps_version: str = None, + ps_version: str | None = None, ): """Session of Photoshop. @@ -87,7 +92,7 @@ def __init__( from photoshop import Session with Session("your/psd/or/psb/file_path.psd", action="open") as ps: - ps.echo(ps.active_document.name) + print(ps.active_document.name) ``` Args: @@ -116,11 +121,11 @@ def __init__( """ super().__init__() - self.path = file_path + self.path: str | PathLike[str] | None = file_path self._auto_close = auto_close self._callback = callback self._action = action - self._active_document = None + self._active_document: Document | None = None self.app: Application = Application(version=ps_version) self.ActionReference: ActionReference = ActionReference() @@ -277,17 +282,17 @@ def __init__( self.UnderlineType = enumerations.UnderlineType self.Units = enumerations.Units self.Urgency = enumerations.Urgency - self.Wartyle = enumerations.Wartyle + self.WarpStyle = enumerations.WarpStyle self.WaveType = enumerations.WaveType self.WhiteBalanceType = enumerations.WhiteBalanceType self.ZigZagType = enumerations.ZigZagType @property - def active_document(self): + def active_document(self) -> Document: """Get current active document. Raises: - - PhotoshopPythonAPICOMError: No active document available. + - PhotoshopPythonAPIError: No active document available. """ try: @@ -297,12 +302,7 @@ def active_document(self): except errors.PhotoshopPythonAPICOMError: raise errors.PhotoshopPythonAPIError("No active document available.") - @staticmethod - def echo(*args, **kwargs): - """Print message.""" - print(*args, **kwargs) - - def alert(self, text: str): + def alert(self, text: str) -> None: """Alert message box in photoshop. Args: @@ -312,36 +312,44 @@ def alert(self, text: str): self.app.doJavaScript(f"alert('{text}')") @active_document.setter - def active_document(self, active_document): + def active_document(self, active_document: Document) -> None: """Set active document.""" self._active_document = active_document - def _action_open(self): - self.active_document = self.app.open(self.path) + def _action_open(self) -> None: + if self.path: + self.active_document = self.app.open(self.path) + else: + print("Specify the document's path before trying to open it.") - def _action_new_document(self): + def _action_new_document(self) -> None: self.active_document = self.app.documents.add() - def _action_document_duplicate(self): + def _action_document_duplicate(self) -> None: self.active_document = self.active_document.duplicate() - def run_action(self): + def run_action(self) -> None: try: _action = getattr(self, f"_action_{self._action}") _action() except AttributeError: pass - def close(self): + def close(self) -> None: """closing current session.""" if self._auto_close: self.active_document.close() - def __enter__(self): + def __enter__(self) -> "Session": self.run_action() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: try: if self._callback: self._callback(self) diff --git a/test/manual_test/manual_test_application.py b/test/manual_test/manual_test_application.py index 26296062..b77426ab 100644 --- a/test/manual_test/manual_test_application.py +++ b/test/manual_test/manual_test_application.py @@ -1,4 +1,5 @@ """""" + # Import third-party modules import pytest @@ -6,6 +7,7 @@ from photoshop.api import Application from photoshop.api import EventID from photoshop.api import SolidColor +from photoshop.api.enumerations import FontSize class TestApplication: @@ -45,7 +47,10 @@ def test_get_background_color(self): def test_set_background_color(self, photoshop_app): self.app.backgroundColor.rgb.green = 0 - assert self.app.backgroundColor.rgb.green == photoshop_app.backgroundColor.rgb.green + assert ( + self.app.backgroundColor.rgb.green + == photoshop_app.backgroundColor.rgb.green + ) def test_build(self): assert self.app.build == "21.0 (20191018.r.37 2019/10/18: 614690fb487)" @@ -151,6 +156,7 @@ def test_get_version(self): assert self.app.version == "21.1.2" def test_windowsFileTypes(self): + assert isinstance(self.app.windowsFileTypes, tuple) assert len(self.app.windowsFileTypes) >= 100 def test_batch(self): @@ -168,9 +174,6 @@ def test_changeProgressText(self): def test_charIDToTypeID(self): assert self.app.charIDToTypeID("Type") == "1417244773" - def test_compareWithNumbers(self): - assert self.app.compareWithNumbers(20, 1) - def test_do_action(self): self.app.doAction("Vignette (selection)", "Default Actions") @@ -219,3 +222,16 @@ def test_typeIDToStringID(self): def test_updateProgress(self): assert self.app.updateProgress("Done", "total") + + def test_ui_text_font_size(self): + prefs = self.app.preferences + initial_font_size = prefs.textFontSize + try: + prefs.textFontSize = FontSize.Large + assert prefs.textFontSize == FontSize.Large + prefs.textFontSize = FontSize.Medium + assert prefs.textFontSize == FontSize.Medium + prefs.textFontSize = FontSize.Small + assert prefs.textFontSize == FontSize.Small + finally: + prefs.textFontSize = initial_font_size diff --git a/test/manual_test/manual_test_documents.py b/test/manual_test/manual_test_documents.py new file mode 100644 index 00000000..e73234e0 --- /dev/null +++ b/test/manual_test/manual_test_documents.py @@ -0,0 +1,25 @@ +from math import isclose +import pytest + +from photoshop.api.application import Application + + +class TestNewDocument: + """Test various parts of the API in a new document.""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup for current test.""" + self.app = Application() + self.docs = self.app.documents + + def test_create_document(self): + doc_name = "test_document" + w = 22.3 + h = 47 + res = 55.8 + doc = self.docs.add(22.3, 47, resolution=55.8, name=doc_name) + assert doc.name == doc_name + assert doc.width == int(w) + assert doc.height == h + assert isclose(doc.resolution, res, abs_tol=0.01) diff --git a/test/manual_test/manual_test_new_document.py b/test/manual_test/manual_test_new_document.py new file mode 100644 index 00000000..6487b54b --- /dev/null +++ b/test/manual_test/manual_test_new_document.py @@ -0,0 +1,90 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + +from photoshop import Session +from photoshop.api._artlayer import ArtLayer +from photoshop.api._document import Document +from photoshop.api._layerSet import LayerSet +from photoshop.api.enumerations import LayerKind + + +class TestNewDocument: + """Test various parts of the API in a new document.""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup for current test.""" + self.session = Session(action="new_document", auto_close=True) + self.session.run_action() + self.app = self.session.app + self.doc = self.session.active_document + yield + self.session.close() + + def test_channel_histogram(self): + channel = self.doc.activeChannels[0] + assert isinstance(channel.histogram, tuple) + + def test_document_info_keywords(self): + doc_info = self.doc.info + assert doc_info.keywords is None + doc_info.keywords = ["foo", "bar"] + assert isinstance(doc_info.keywords, tuple) + assert doc_info.keywords == ("foo", "bar") + tuple_keywords = ("key", "word", "?") + doc_info.keywords = tuple_keywords + assert doc_info.keywords == tuple_keywords + + def test_selection(self): + selection = self.doc.selection + selection.selectAll() + bounds = selection.bounds + assert isinstance(bounds, tuple) + assert isinstance(bounds[0], float) + selection.contract(20.5) + contracted_bounds = selection.bounds + assert bounds[0] < contracted_bounds[0] + selection.expand(10) + expanded_bounds = selection.bounds + assert expanded_bounds[0] < contracted_bounds[0] + + def test_paths(self): + with TemporaryDirectory(prefix="photoshop_python_api_") as tmpdir: + doc_path = Path(tmpdir, "test_doc.psd") + self.doc.saveAs(str(doc_path)) + assert isinstance(self.doc.saved, bool) + assert self.doc.saved + assert self.doc.fullName == doc_path + + def test_layer_kind(self): + background_layer = self.doc.artLayers[0] + assert isinstance(background_layer, ArtLayer) + layer_kind = background_layer.kind + layer_set_1 = self.doc.layerSets.add() + self.doc.activeLayer = layer_set_1 + + # This used to fail in version 0.24.1 + assert background_layer.kind == layer_kind + + layer_1 = self.doc.artLayers.add() + layer_1.kind = LayerKind.TextLayer + assert layer_1.kind == LayerKind.TextLayer + + def test_layer_iteration(self): + layer_set = self.doc.layerSets.add() + layers = list(self.doc.layers) + assert any((layer for layer in layers if isinstance(layer, LayerSet))) + assert any((layer for layer in layers if isinstance(layer, ArtLayer))) + + layer_set.artLayers.add() + layer_set.layerSets.add() + assert any((layer for layer in layer_set if isinstance(layer, LayerSet))) + assert any((layer for layer in layer_set if isinstance(layer, ArtLayer))) + + def test_layer_parent(self): + layer_set = self.doc.layerSets.add() + assert isinstance(layer_set.parent, Document) + sub_layer = layer_set.artLayers.add() + assert isinstance(sub_layer.parent, LayerSet) diff --git a/test/manual_test/manual_test_path_items.py b/test/manual_test/manual_test_path_items.py new file mode 100644 index 00000000..1e812b37 --- /dev/null +++ b/test/manual_test/manual_test_path_items.py @@ -0,0 +1,78 @@ +from typing import Sequence + +import pytest + +from photoshop.api.enumerations import PointKind, ShapeOperation, ToolType +from photoshop.api.path_item import PathItem +from photoshop.api.path_point_info import PathPointInfo +from photoshop.api.solid_color import SolidColor +from photoshop.api.sub_path_info import SubPathInfo +from photoshop.session import Session + + +class TestPathItems: + """Test path items.""" + + @pytest.fixture(autouse=True) + def setup(self): + self.session = Session(action="new_document", auto_close=True) + self.session.run_action() + self.app = self.session.app + self.doc = self.session.active_document + + self.point_anchors = ((0, 0.0), (10.1, 0.0), (5, 5)) + + self.path_item = self._create_path("test_path", self.point_anchors) + + yield + self.session.close() + + def _create_path( + self, name: str, point_anchors: Sequence[tuple[float, float]] + ) -> PathItem: + point_infos: list[PathPointInfo] = [ + PathPointInfo(anchor=anchor) for anchor in point_anchors + ] + + last_anchor = point_anchors[-1] + left = (last_anchor[0] + 1, last_anchor[1]) + right = (last_anchor[0], last_anchor[1] - 1) + + last_point = point_infos[-1] + last_point.kind = PointKind.SmoothPoint + last_point.leftDirection = left + last_point.rightDirection = right + + sub_path = SubPathInfo(entire_sub_path=point_infos) + + return self.doc.pathItems.add(name=name, entire_path=(sub_path,)) + + def test_path_item(self) -> None: + for sub_path in self.path_item.subPathItems: + assert ShapeOperation(sub_path.operation) + assert isinstance(sub_path.closed, bool) + for point in sub_path.pathPoints: + assert isinstance(point.anchor, tuple) + assert len(point.anchor) == 2 + assert PointKind(point.kind) + solid_color = SolidColor() + solid_color.rgb.red = 100 + solid_color.rgb.green = 100 + solid_color.rgb.blue = 100 + self.path_item.fillPath(solid_color) + self.path_item.strokePath(ToolType.Blur) + + def test_path_removal(self) -> None: + n_paths = 5 + for i in range(n_paths): + self._create_path(f"test_{i}", self.point_anchors) + + path_items = self.doc.pathItems + + path_items_len = len(path_items) + assert path_items_len >= n_paths + path_items[0].remove() + path_items["test_3"].remove() + assert len(path_items) == path_items_len - 2 + path_items.removeAll() + assert len(path_items) == 0 diff --git a/test/manual_test/manual_test_text_item.py b/test/manual_test/manual_test_text_item.py index 08f1b120..62fdf8a4 100644 --- a/test/manual_test/manual_test_text_item.py +++ b/test/manual_test/manual_test_text_item.py @@ -1,10 +1,12 @@ """""" + # Import third-party modules import pytest # Import local modules from photoshop import Session -from photoshop.api.enumerations import TextType +from photoshop.api._artlayer import ArtLayer +from photoshop.api.enumerations import Justification, TextType class TestTextItem: @@ -14,11 +16,14 @@ class TestTextItem: @pytest.fixture(autouse=True) def setup(self, psd_file): """Setup for current test.""" - self.session = Session(file_path=psd_file("textitem"), action="open", auto_close=True) + self.session = Session( + file_path=psd_file("textitem"), action="open", auto_close=True + ) self.session.run_action() doc = self.session.active_document layer = doc.activeLayer - self.text_item = layer.textItem() # -> TextItem + assert isinstance(layer, ArtLayer) + self.text_item = layer.textItem # -> TextItem yield # self.session.close() @@ -72,8 +77,8 @@ def test_justification(self): assert self.text_item.justification == 1 def test_set_justification(self): - self.text_item.justification = 2 - assert self.text_item.justification == 2 + self.text_item.justification = Justification.Center + assert self.text_item.justification == Justification.Center def test_kind(self): assert self.text_item.kind == 1 @@ -95,3 +100,6 @@ def test_size(self): def test_change_size(self): self.text_item.size = 20 assert self.text_item.size == 20.0 + + def test_width(self): + assert isinstance(self.text_item.width, float)