Code Monkey home page Code Monkey logo

photoshopapi's Introduction

PhotoshopAPI

ko-fi

CPP Version PyPI - Version PyPi - Downloads Documentation Status CI Status Test Status Python Wheels

Note

The PhotoshopAPI is still in early development status which means it is subject to change and will likely include bugs. If you find any please report them to the issues page

About

PhotoshopAPI is a C++20 Library with Python bindings for reading and writing of Photoshop Files (*.psd and *.psb) based on previous works from psd_sdk, pytoshop and psd-tools. As well as the official Photoshop File Format Specification, where applicable. The library is continuously tested for correctness in its core functionality. If you do find a bug please submit an issue to the github page.

The motivation to create another library despite all the other works present is that there isn't a library which has layer editing as a first class citizen while also supporting all bit-depths known to Photoshop (8-bits, 16-bits, 32-bits). This Library aims to create an abstraction between the raw binary file format and the structure that the user interfaces against to provide a more intuitive approach to the editing of Photoshop Files.

Why should you care?

Photoshop itself is unfortunately often slow to read/write files and the built-in tools for automatically/programmatically modifying files suffer this same issue. On top of this, due to the extensive history of the Photoshop File Format, Photoshop files written out by Photoshop itself are often unnecessarily bloated to add backwards compatibility or cross-software compatibility.

The PhotoshopAPI tries to address these issue by allowing the user to read/write/modify Photoshop Files without ever having to enter Photoshop itself which additionally means, no license is required. It is roughly 5-10x faster in reads and 20x faster in writes than photoshop while producing files that are consistently 20-50% lower in size (see the benchmarks section on readthedocs for details). The cost of parsing is paid up front either on read or on write so modifying the layer structure itself is almost instantaneous (except for adding new layers).

Features

Supported:

  • Read and write of *.psd and *.psb files
  • Creating and modifying simple and complex nested layer structures
  • Pixel Masks
  • Modifying layer attributes (name, blend mode etc.)
  • Setting the Display ICC Profile
  • Setting the DPI of the document
  • 8-, 16- and 32-bit files
  • RGB Color Mode
  • All compression modes known to Photoshop

Planned:

  • Support for Adjustment Layers
  • Support for Vector Masks
  • Support for Text Layers
  • Support for Smart Object Layers
  • CMYK, Indexed, Duotone and Greyscale Color Modes

Not Supported:

  • Files written by the PhotoshopAPI do not contain a valid merged image in order to save size meaning they will not behave properly when opened in third party apps requiring these (such as Lightroom)
  • Lab and Multichannel Color Modes

Python

The PhotoshopAPI comes with fully fledged Python bindings which can be simply installed using

$ py -m pip install PhotoshopAPI

alternatively the wheels can be downloaded from the Releases page. For examples on how to use the python bindings please refer to the Python Bindings section on Readthedocs or check out the PhotoshopExamples/ directory on the github page which includes examples for Python as well as C++.

For an even quicker way of getting started check out the Quickstart section!

Documentation

The full documentation with benchmarks, build instructions and code reference is hosted on the PhotoshopAPI readthedocs page.

Requirements

This goes over requirements for usage, for development requirements please visit the docs.

  • A CPU with AVX2 support (this is most CPUs after 2014) will greatly increase performance, if we detect this to not be there we disable this optimization
  • A 64-bit system
  • C++ Library: Linux, Windows or MacOS
  • Python Library1: Linux, Windows, MacOS

The python bindings support python >=3.7 (except for ARM-based MacOS machines which raise this to >=3.10)

1 Currently Linux is supported only as manylinux build and has some features disabled such as timestamps on logging.

Performance

The PhotoshopAPI is built with performance as one of its foremost concerns. Using it should enable you to optimize your pipeline rather than slow it down. It runs fully multithreaded with SIMD instructions to leverage all the computing power your computer can afford.

As the feature set increases this will keep being one of the key requirements. For detailed benchmarks running on a variety of different configurations please visit the docs

Below you can find some of the benchmarks comparing the PhotoshopAPI ('PSAPI') against Photoshop in read/write performance

8-bit

Quickstart

The primary struct to familiarize yourself with when using the PhotoshopAPI is the LayeredFile as well as all its Layer derivatives (such as ImageLayer and GroupLayer), all of these are template structs for each of the available bit depths.

To get a feel of what is possible with the API as well as how to use it please refer to PhotoshopExample/ directory. To familiarize yourself with the main concepts, as well as recommended workflows check out the docs or the examples.

If more fine grained control over the binary structure is necessary, one can modify the PhotoshopFile which is what is parsed by the API internally. Do keep in mind that this requires a deep understanding of how the Photoshop File Format works.

Below is a minimal example to get started with opening a PhotoshopFile, removing some layer, and writing the file back out to disk:

C++

using namespace PhotoshopAPI;

// Initialize an 8-bit layeredFile. This must match the bit depth of the PhotoshopFile.
// To initialize this programmatically please refer to the ExtendedSignature example
LayeredFile<bpp8_t> layeredFile = LayeredFile<bpp8_t>::read("InputFile.psd");

// Do some operation, in this case delete
layeredFile.removeLayer("SomeGroup/SomeNestedLayer");	

// One could write out to .psb instead if wanted and the PhotoshopAPI will take 
// care of any conversion internally
LayeredFile<bpp8_t>::write(std::move(layeredFile), "OutputFile.psd");

The same code for reading and writing can also be used to for example LayeredFile::moveLayer or LayeredFile::addLayer as well as extracting any image data

Python

import psapi

# Read the layered_file using the LayeredFile helper class, this returns a 
# psapi.LayeredFile_*bit object with the appropriate bit-depth
layered_file = psapi.LayeredFile.read("InputFile.psd")

# Do some operation, in this case delete
layered_file.remove_layer()

# Write back out to disk
layered_file.write("OutFile.psd")

We can also do much more advanced things such as taking image data from one file and transferring it to another file, this can be across file sizes, psd/psb and even bit-depth!

import psapi
import numpy as np
import os


def main() -> None:
    # Read both our files, they can be open at the same time or we can also read one file,
    # extract the layer and return just that layer if we want to save on RAM.
    file_src = psapi.LayeredFile.read("GraftSource_16.psb")
    file_dest = psapi.LayeredFile.read("GraftDestination_8.psd")

    # Extract the image data and convert to 8-bit.
    lr_src: psapi.ImageLayer_16bit = file_src["GraftSource"]
    img_data_src = lr_src.get_image_data()
    img_data_8bit = {}
    for key, value in img_data_src.items():
        value = value / 256 # Convert from 0-65535 -> 0-255
        img_data_8bit[key] = value.astype(np.uint8)

    # Reconstruct an 8bit converted layer
    img_layer_8bit = psapi.ImageLayer_8bit(
        img_data_8bit, 
        layer_name=lr_src.name, 
        width=lr_src.width, 
        height=lr_src.height, 
        blend_mode=lr_src.blend_mode, 
        opacity=lr_src.opacity
        )

    # add the layer and write out to file!
    file_dest.add_layer(img_layer_8bit)
    file_dest.write("GraftDestination_8_Edited.psd")


if __name__ == "__main__":
    main()

photoshopapi's People

Contributors

emildohne avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

photoshopapi's Issues

When creating a Photoshop file with only group layers we cannot open the file

This appears to be due to the way we calculate the amount of channels in a file when going from LayeredFile<T> -> PhotoshopFile. It doesnt detect any channels and therefore writes out 0 which isnt valid for Photoshop. What it appears to actually do is just write out 4 channels in RGB mode and write that into the merged image data section

Create python bindings for the PSAPI

This is the main issue for adding bindings for the PhotoshopAPI. Likely this will be as a module called psapi.

The general workflow can then be:

import psapi

file = psapi.LayeredFile_8bit.read("in_path.psd")
psapi.LayeredFile_8bit.write(file, "out_path.psd")

The initial goal is to support all items referenced in the docs

Weird behaviour when writing PSB

When writing PSB files for some reason they wont open unless we add padding bytes at the end of the LayerAndMaskInformation section. I have inspected the binaries of both Photoshop generated binaries as well as our generated binaries and cant really see a reason why this would happen. The commit in question that "fixes" this is 5223b4d and the relevant code can be found below

void LayerAndMaskInformation::write(File& document, const FileHeader& header)
{
	....
	// I genuinely have no idea why this is required but when we dont add this the files wont open in Photoshop
	if (header.m_Version == Enum::Version::Psb)
	{
		WritePadddingBytes(document, 2u);	// 4 also appears to be a valid number here
	}
}

While this does temporarily fix the issue I would like for these bytes to have a reason :)

Writing uneven files fails to decompress

When writing uneven files likely due to how we go from bbox coords to center coords (division) It writes out the extents incorrectly. e.g. when using the following code:

using namespace NAMESPACE_PSAPI;

std::filesystem::path psb_path = std::filesystem::current_path();
psb_path += "\\Document.psb";

const uint32_t width = 63;
const uint32_t height = 32;
LayeredFile<bpp32_t> document = { Enum::ColorMode::RGB, width, height };

std::unordered_map <Enum::ChannelID, std::vector<bpp32_t>> channelMap;
channelMap[Enum::ChannelID::Red] = std::vector<bpp32_t>(width * height, .25f);
channelMap[Enum::ChannelID::Green] = std::vector<bpp32_t>(width * height, .25f);
channelMap[Enum::ChannelID::Blue] = std::vector<bpp32_t>(width * height, .25f);

ImageLayer<bpp32_t>::Params layerParams = {};
layerParams.layerName = "Layer";
layerParams.width = width;
layerParams.height = height;

auto layer = std::make_shared<ImageLayer<bpp32_t>>(
	std::move(channelMap),
	layerParams
);
document.addLayer(layer);

File::FileParams params = { .doRead = false, .forceOverwrite = true };
auto outputFile = File(psb_path, params);
auto psdDocumentPtr = LayeredToPhotoshopFile(std::move(document));
psdDocumentPtr->write(outputFile);

It writes out the bbox as [0, 0, 32, 62] which is incorrect as it should be [0, 0, 32, 63] Please investigate

provide alternative signature to addLayer() moveLayer() and removeLayer()

Currently a call to those functions might look like this:

auto nestedGroupLayer = layeredFile.findLayer("Group/NestedGroup");
if (nestedGroupLayer)
{
	layeredFile.moveLayer(nestedGroupLayer);
}

which isnt inherently bad but there should also be the option to call these functions with a path instead as that would simplify the code some more.

layeredFile.moveLayer("Group/Nested");

The original signature is still going to be it is useful if we already have a ptr to the layer itself

Compile PhotoshopAPI with address sanitizers and wall

In order to get the PhotoshopAPI closer to being production ready we should always compile it with /Wall and /fsanitize=address (This is the only sanitizer currently supported out of the box with MSVC). Especially since our test cases cant and wont ever be able to cover all possible issues

When submoduling via CMake we get the error "Cannot include blosc2.h"

This is due to the way we construct our include for the PhotoshopAPI target

target_include_directories(PhotoshopAPI PUBLIC include ${CMAKE_SOURCE_DIR}/thirdparty/c-blosc2/include src src/Util)

where a better approach would be to add an INTERFACE target for the include dirs in our top level build and not rely on ${CMAKE_SOURCE_DIR} but instead ${CMAKE_PROJECT_DIR}

This could look a little something like this:

add_library(blosc2_include INTERFACE)
target_include_directories(blosc2_include INTERFACE thirdparty/c-blosc_2/include )

Add support for locked layers

This information seems to be stored on the 'lspf' tagged block on bit 8 as well as perhaps on the 'flags' of the layer record but this difference might be unrelated

Create automatic CMake Doxygen documentation pipeline

The goal of this endeavour would be to have PhotoshopAPI be hosted on ReadTheDocs where every commit automatically pushes the latest docs. The tech stack would likely be Doxygen + Breathe + Sphinx as outlined in this blog. The comment structure itself likely needs to be updated to reflect this

Investigate switching out zlib-ng for libdeflate

From the benchmarks presented here zlib-ng/zlib-ng#1486 it appears that even for larger buffers libdeflate outperforms zlib-ng on both compression ratios as well as speed.

It does provide a different api than zlib-ng and also doesnt support streaming which are both not necessarily concerns for our use cases.

Add Support for Adjustment Layers

This is a rather big issue as Photoshop has a wide range of adjustment layers, all with their different implementations and different levels of completeness in the official documentation.

This change would have to be two-fold; adding support for the tagged blocks in the PhotoshopFile under AdditionalLayerInformation and then actually implementing these as an abstraction of Layer<T> in the LayeredFile<T>.

For this issue support would be greatly appreciated as its a rather large issue but at the same time each adjustment layer is its individual separate object. I will attempt to provide a reference implementation once I get started but feel free to contribute!

Add support for the specifying of DPI of a Photoshop file

This internally seems to be handled by the 1005 ResolutionInfo ImageResource which is documented in the Photoshop API Guide from Adobe. The structure is as following

struct ResolutionInfo
{
   uint32_t fixedPointHRes;  
   uint16_t hResUnit;    // 1 for Pixels per inch and 2 for pixels per cm
   uint16_t widthUnit;  // 1 = inches, 2 = cm, 3 = points, 4 = picas, 5 = columns
   uint32_t fixedPointVRes;  
   uint16_t VResUnit;    // 1 for Pixels per inch and 2 for pixels per cm
   uint16_t heightUnit;  // 1 = inches, 2 = cm, 3 = points, 4 = picas, 5 = columns
}

You can see the structure for FixedPoint values below

struct FixedPoint
{
   uint16_t value;
   uint16_t fraction;   // .5 would be 65536/2 for example
}

These would require us to fundamentally implement a system for ImageResources similar to the ones present for TaggedBlocks. This would be required either way for #18

Add Blosc2 Compression to ImageData

Currently our program uses an excessive amount of memory as it stores all the image data in an uncompressed manner which leads to big files (a focus of what this API should be able to handle) taking up loads of memory.

An example file which unfortunately cannot be shared is ~6GB on disk but takes up >80GB of memory when decompressing it in release mode. The file contains 170 Image layers (and some more mask layers) and has a bit depth of 16 with a resolution of 9124x6082

The goal of this change would be to bring memory usage on par with, or slightly above what photoshop files store but without sacrificing on performance too much.

Parsing files with vector masks breaks PhotoshopAPI

When trying to parse files with Vector masks the PhotoshopAPI breaks down in c-blosc2 due to the PhotoshopAPI expecting to have e.g. 4096 bytes of data for a 64*64 image for the layer upon decompression but we instead get 0 because the data is stored elsewhere in the additional layer info of the layer record. While we dont have support for vector masks we can simply add a check if the channels size does not match what we expect and raise a warning and continue.

CMake and Ninja versions necessary

I'm using Python 3.10, cmake 3.29.1 and ninja 1.11.1. I usually try first with my own versions to avoid env balkanization.

With meson 1.3.0:

running build_ext
Traceback (most recent call last):
  File "./PhotoshopAPI/setup.py", line 153, in <module>
    setup(
[..]
    ninja_executable_path = Path(ninja.BIN_DIR) / "ninja"
AttributeError: module 'ninja' has no attribute 'BIN_DIR'

After updating to meson 1.4.0:

ERROR Missing dependencies:
	ninja
	cmake>=3.12

Which versions do you recommend?

When running roundtripping test 8- and 32-bit files store their masks as black

With 16-bit files we have the expected white mask but for the other color depths that isnt the case. The input files producing this unexpected output:

PhotoshopTest/documents/Groups/Groups_8bit.psb
PhotoshopTest/documents/Groups/Groups_8bit.psd
PhotoshopTest/documents/Groups/Groups_32bit.psb
PhotoshopTest/documents/Groups/Groups_32bit.psd

when running the test case TEST_CASE("Check Roundtripping Groups") in TestRoundtripping.cpp

Add Support for CMYK

CMYK is currently only partially supported but shouldnt be far off from actually fully working within the PhotoshopAPI. Testing this is required and ironing out any kinks + raising warnings if writing a file that would not be Photoshop compliant (i.e. not all adjustment layers are supported in CMYK)

When parsing files AdditionalLayerInfo raises warnings despite parsing just fine

Often times when parsing the AdditionalLayerInfo section a warning like the one below gets raised but reading it does actually still work without problems.

[AdditionalLayerInfo]Read too much data for the additional layer info, was allowed 712 but read 716 instead

It also appears that it is consistently saying it read 4 bytes too many stemming from 16- and 32- bit files with Lr16 and Lr32 Tagged Blocks. This is especially apparent when building the PhotoshopTest target

When loading files with locked layers PhotoshopAPI doesnt know what to do

Steps to reproduce:

  • Create a PhotoshopDocument which will create a "Background" locked layer which isnt stored as actual pixel values
  • Try to roundtrip the file i.e. read -> write using the LayeredFile Struct like seen below
    auto inputFile = File(fullInPath);
    auto psDocumentPtr = std::make_unique<PhotoshopFile>();
    psDocumentPtr->read(inputFile);
    LayeredFile<bpp8_t> layeredFile = { std::move(psDocumentPtr) };
    
    // Write to disk
    File::FileParams params = { .doRead = false, .forceOverwrite = true };
    auto outputFile = File(fullOutPath, params);
    auto psdOutDocumentPtr = LayeredToPhotoshopFile(std::move(layeredFile));
    psdOutDocumentPtr->write(outputFile);
  • This will raise the following error:
    [LayerInfo] Invalid Document encountered. Photoshop files must contain at least one layer

This happens because Photoshop stores this single layer as merged image data rather than a layer

A possible fix would be to warn about this on read already by adding a similar check giving the user some more information

Add Support for Greyscale

Greyscale is currently only partially supported but shouldnt be far off from actually fully working within the PhotoshopAPI. Testing this is required and ironing out any kinks + raising warnings if writing a file that would not be Photoshop compliant (i.e. not all adjustment layers are supported in Greyscale)

Create Benchmarking Suite

Create a general benchmarking suite that we can use to continuously test the PhotoshopAPI to its limits. This benchmark suite is not meant to test the library but should provide a rather broad range of use cases such as:

  • Writing very large files (>2GB) in PSD and PSB with all bit depths
  • Reading very large files (>2GB) in PSD and PSB with all bit depths
  • Read Complex nested hierarchies
  • Create Complex nested hierarchies
  • Benchmark Modifying complex nested hierarchies

All of these test cases should profile their speed, ram usage and file size on disk against Photoshop itself and we will run these benchmarks on all the PCs available at home. Their specs can be found below:

  • AMD Ryzen 9 5950x || 96GB DDR4 3200MHz
  • AMD Ryzen 5 2600x || 32GB DDR4 3200MHz
  • ...

Investigate Performance gains from adding a dedicated ThreadPool to the PhotoshopAPI

PhotoshopAPI could potentially gain from having a custom threadpool struct rather than using std::for_each with std::execution::par to more efficiently manage resource especially in cases where we actually have nested threaded calls.

For example, when decoding the LayerInfo Section we currently parse each layer in sequence with each sub-channel in parallel (usually 4 channels). This means we leave a lot of potential performance on the table by not utilizing cpus with more than 4 cores very efficiently.

In an ideal scenario we would submit tasks to a thread_queue as well as having some utility function which splits the works into discrete blocks such as a parallelizeLoop() function which will split the work across the available cores. This would likely be especially handy for our decompression functions which, in part, contain such highly multithreaded work

Add support for ARM based Mac chips

This appears to only fail on compilation of our AVX2 intrinsics which we can simply opt out of if we detect arm based machines and in the future we can add arm based intrinsics

Fix Zlib-NG build errors with CMake

When initially pulling the repo, Zlib-NG needs to first be manually built using cmake in the commandline as PhotoshopAPI wouldnt build otherwise. This is likely some conflict with the way zlib-ng is built.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.