From 74e3e1ebd2fe1c056d38af7fe9ac7ba14719890d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sat, 24 Oct 2026 21:11:45 +0200 Subject: [PATCH 01/22] feat(vips): a libvips loader's skeleton, registered by the program (S7.3, 1) libvips-plugin/ holds psdload, psdload_buffer and psdload_source, laid out as libvips' own loaders, with minipsd compiled in privately (MINIPSD_STATIC). minipsd_vips_init() registers them after VIPS_INIT(): libvips' prebuilt Windows builds load no modules. The header is read from the mapped file: size, bands, format, interpretation, resolution, the ICC profile, EXIF, XMP, IPTC and psd-* fields; pixels come next. libvips' fetch moves to cmake/minipsd_libvips.cmake, shared with examples/vips_composite.c; MINIPSD_VIPS_PLUGIN builds the plugin and its test, vips.plugin, which CI's msvc job runs. --- .github/workflows/ci.yml | 9 +- cmake/minipsd_libvips.cmake | 41 ++++ examples/CMakeLists.txt | 41 +--- libvips-plugin/CMakeLists.txt | 31 +++ libvips-plugin/minipsd_vips.c | 374 ++++++++++++++++++++++++++++++++++ libvips-plugin/minipsd_vips.h | 31 +++ libvips-plugin/test_vips.c | 215 +++++++++++++++++++ tests/CMakeLists.txt | 14 ++ 8 files changed, 715 insertions(+), 41 deletions(-) create mode 100644 cmake/minipsd_libvips.cmake create mode 100644 libvips-plugin/CMakeLists.txt create mode 100644 libvips-plugin/minipsd_vips.c create mode 100644 libvips-plugin/minipsd_vips.h create mode 100644 libvips-plugin/test_vips.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f31d8f3..1e7d358 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,9 +29,10 @@ jobs: matrix: include: # windows-2022, not windows-latest: the msvc preset's generator is "Visual Studio 17 2022". - # msvc also builds and runs examples/vips_composite.c on libvips' prebuilt Windows build - # (fetched, pinned; MSVC is what Platemaker builds with). - - { os: windows-2022, preset: msvc, vips_example: ON } + # msvc also builds and runs examples/vips_composite.c and the libvips plugin + # (libvips-plugin/) on libvips' prebuilt Windows build (fetched, pinned; MSVC is what + # Platemaker builds with). + - { os: windows-2022, preset: msvc, vips: ON } - { os: windows-latest, preset: mingw } # linux-gcc also reads psd-tools' 315 files (22 MB, pinned by hash): every comparison # with psd-tools, Photoshop's ZIP files through zlib among them. @@ -77,7 +78,7 @@ jobs: - name: Configure, build, test if: matrix.preset != 'mingw' run: | - cmake --preset ${{ matrix.preset }} -DMINIPSD_REQUIRE_PSD_TOOLS=ON -DMINIPSD_PSD_TOOLS_FILES=${{ matrix.psd_tools_files || 'OFF' }} -DMINIPSD_VIPS_EXAMPLE=${{ matrix.vips_example || 'OFF' }} "-DPython3_EXECUTABLE=${{ steps.python.outputs.python-path }}" + cmake --preset ${{ matrix.preset }} -DMINIPSD_REQUIRE_PSD_TOOLS=ON -DMINIPSD_PSD_TOOLS_FILES=${{ matrix.psd_tools_files || 'OFF' }} -DMINIPSD_VIPS_EXAMPLE=${{ matrix.vips || 'OFF' }} -DMINIPSD_VIPS_PLUGIN=${{ matrix.vips || 'OFF' }} "-DPython3_EXECUTABLE=${{ steps.python.outputs.python-path }}" cmake --build --preset ${{ matrix.preset }} ctest --preset ${{ matrix.preset }} diff --git a/cmake/minipsd_libvips.cmake b/cmake/minipsd_libvips.cmake new file mode 100644 index 0000000..c4d15db --- /dev/null +++ b/cmake/minipsd_libvips.cmake @@ -0,0 +1,41 @@ +# --------------------------------------------------------------------------- +# libvips for what uses it here: examples/vips_composite.c (MINIPSD_VIPS_EXAMPLE) and the plugin +# in libvips-plugin/ (MINIPSD_VIPS_PLUGIN). minipsd itself never depends on libvips. +# +# Windows fetches libvips' official prebuilt "web" build, pinned (the newest release when set); +# elsewhere pkg-config finds the installed one. Gives: +# +# minipsd_libvips an imported target: libvips' C API, glib and gobject +# MINIPSD_LIBVIPS_ENV the environment its tests run in (Windows: the DLLs on PATH), or "" +# --------------------------------------------------------------------------- + +if(WIN32) + set(MINIPSD_VIPS_VERSION 8.18.7) + include(FetchContent) + FetchContent_Declare(minipsd_vips + URL https://github.com/libvips/build-win64-mxe/releases/download/v${MINIPSD_VIPS_VERSION}/vips-dev-x64-web-${MINIPSD_VIPS_VERSION}.zip + URL_HASH SHA256=3a122eb3d588690008216f786338e2f9329dba3c3c2768b5933cf7299d549241 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + SOURCE_SUBDIR no-cmake-project) # files only: nothing to add as a subproject + FetchContent_MakeAvailable(minipsd_vips) + set(_vips "${minipsd_vips_SOURCE_DIR}") + add_library(minipsd_libvips INTERFACE IMPORTED) + if(MSVC) # the zip's import libraries + target_link_libraries(minipsd_libvips INTERFACE + "${_vips}/lib/libvips.lib" "${_vips}/lib/libglib-2.0.lib" "${_vips}/lib/libgobject-2.0.lib") + else() # MinGW links the DLLs themselves + target_link_libraries(minipsd_libvips INTERFACE + "${_vips}/bin/libvips-42.dll" "${_vips}/bin/libglib-2.0-0.dll" "${_vips}/bin/libgobject-2.0-0.dll") + endif() + target_include_directories(minipsd_libvips SYSTEM INTERFACE + "${_vips}/include" "${_vips}/include/glib-2.0" "${_vips}/lib/glib-2.0/include") + set(_vips_path "${_vips}/bin;$ENV{PATH}") # the DLLs, for the tests + string(REPLACE ";" "\\;" _vips_path "${_vips_path}") + set(MINIPSD_LIBVIPS_ENV "PATH=${_vips_path}") +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(VIPS REQUIRED IMPORTED_TARGET vips) + add_library(minipsd_libvips INTERFACE IMPORTED) + target_link_libraries(minipsd_libvips INTERFACE PkgConfig::VIPS) + set(MINIPSD_LIBVIPS_ENV "") +endif() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 55414ce..5ac8bb2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -117,42 +117,9 @@ foreach(_psd IN LISTS _other_psds) endforeach() # vips_composite: libvips renders the composite, minipsd writes the file with it (MINIPSD_VIPS_EXAMPLE, -# off by default: it needs libvips). minipsd never depends on libvips; only this example does. -# Windows fetches libvips' official prebuilt "web" build, pinned (the newest release when set); -# elsewhere pkg-config finds the installed one. The example links the C API only. -option(MINIPSD_VIPS_EXAMPLE "Build and run examples/vips_composite.c against libvips (fetched on Windows)" OFF) +# off by default: it needs libvips, from cmake/minipsd_libvips.cmake). minipsd never depends on +# libvips; only this example does. It links the C API only. if(MINIPSD_VIPS_EXAMPLE) - if(WIN32) - set(MINIPSD_VIPS_VERSION 8.18.7) - include(FetchContent) - FetchContent_Declare(minipsd_vips - URL https://github.com/libvips/build-win64-mxe/releases/download/v${MINIPSD_VIPS_VERSION}/vips-dev-x64-web-${MINIPSD_VIPS_VERSION}.zip - URL_HASH SHA256=3a122eb3d588690008216f786338e2f9329dba3c3c2768b5933cf7299d549241 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE - SOURCE_SUBDIR no-cmake-project) # files only: nothing to add as a subproject - FetchContent_MakeAvailable(minipsd_vips) - set(_vips "${minipsd_vips_SOURCE_DIR}") - add_library(minipsd_libvips INTERFACE IMPORTED) - if(MSVC) # the zip's import libraries - target_link_libraries(minipsd_libvips INTERFACE - "${_vips}/lib/libvips.lib" "${_vips}/lib/libglib-2.0.lib" "${_vips}/lib/libgobject-2.0.lib") - else() # MinGW links the DLLs themselves - target_link_libraries(minipsd_libvips INTERFACE - "${_vips}/bin/libvips-42.dll" "${_vips}/bin/libglib-2.0-0.dll" "${_vips}/bin/libgobject-2.0-0.dll") - endif() - target_include_directories(minipsd_libvips SYSTEM INTERFACE - "${_vips}/include" "${_vips}/include/glib-2.0" "${_vips}/lib/glib-2.0/include") - set(_vips_path "${_vips}/bin;$ENV{PATH}") # the DLLs, for the tests - string(REPLACE ";" "\\;" _vips_path "${_vips_path}") - set(_vips_env "PATH=${_vips_path}") - else() - find_package(PkgConfig REQUIRED) - pkg_check_modules(VIPS REQUIRED IMPORTED_TARGET vips) - add_library(minipsd_libvips INTERFACE IMPORTED) - target_link_libraries(minipsd_libvips INTERFACE PkgConfig::VIPS) - set(_vips_env "") - endif() - add_executable(vips_composite vips_composite.c) target_link_libraries(vips_composite PRIVATE minipsd::minipsd minipsd_libvips) target_compile_options(vips_composite PRIVATE ${MINIPSD_WARNINGS}) @@ -173,7 +140,7 @@ if(MINIPSD_VIPS_EXAMPLE) endif() # TIMEOUT: libvips' exit has once hung on Windows, after the result; a hang fails fast. set_tests_properties(${_vips_tests} PROPERTIES PASS_REGULAR_EXPRESSION "composite read back: as rendered" TIMEOUT 120) - if(_vips_env) - set_tests_properties(${_vips_tests} PROPERTIES ENVIRONMENT "${_vips_env}") + if(MINIPSD_LIBVIPS_ENV) + set_tests_properties(${_vips_tests} PROPERTIES ENVIRONMENT "${MINIPSD_LIBVIPS_ENV}") endif() endif() diff --git a/libvips-plugin/CMakeLists.txt b/libvips-plugin/CMakeLists.txt new file mode 100644 index 0000000..b8d5e1f --- /dev/null +++ b/libvips-plugin/CMakeLists.txt @@ -0,0 +1,31 @@ +# --------------------------------------------------------------------------- +# The libvips plugin: minipsd as libvips' psdload (MINIPSD_VIPS_PLUGIN). A static library the +# program links, then registers with minipsd_vips_init() after VIPS_INIT(); minipsd is compiled +# into it, private (MINIPSD_STATIC). libvips comes from cmake/minipsd_libvips.cmake. +# +# minipsd::vips the plugin +# vips.plugin documents minipsd writes, loaded through libvips (test_vips.c) +# --------------------------------------------------------------------------- + +add_library(minipsd_vips STATIC minipsd_vips.c minipsd_vips.h) +add_library(minipsd::vips ALIAS minipsd_vips) +target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd::minipsd) +target_compile_options(minipsd_vips PRIVATE ${MINIPSD_WARNINGS}) +set_target_properties(minipsd_vips PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + +add_executable(minipsd-vips-test test_vips.c) +target_link_libraries(minipsd-vips-test PRIVATE minipsd::vips minipsd::minipsd) +target_compile_options(minipsd-vips-test PRIVATE ${MINIPSD_WARNINGS}) +set_target_properties(minipsd-vips-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) +if(MSVC) + target_compile_definitions(minipsd-vips-test PRIVATE _CRT_SECURE_NO_WARNINGS) # plain fopen +endif() + +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/out") +add_test(NAME vips.plugin COMMAND minipsd-vips-test "${CMAKE_CURRENT_BINARY_DIR}/out") +# TIMEOUT: libvips' exit has once hung on Windows (examples/), so a hang fails fast. +set_tests_properties(vips.plugin PROPERTIES PASS_REGULAR_EXPRESSION "vips plugin: all passed" TIMEOUT 120) +if(MINIPSD_LIBVIPS_ENV) + set_tests_properties(vips.plugin PROPERTIES ENVIRONMENT "${MINIPSD_LIBVIPS_ENV}") +endif() diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c new file mode 100644 index 0000000..4d43bce --- /dev/null +++ b/libvips-plugin/minipsd_vips.c @@ -0,0 +1,374 @@ +/** + * \file + * \brief minipsd as a libvips loader: a PSD or PSB as a libvips image. + * + * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer + * variants make one), so the file can move into libvips/foreign/ as it is. The file is mapped + * (vips_source_map) and read in place by minipsd's reader; nothing is copied. + * + * The image is the composite as the file stores it, no colour converted: the mode's colour + * channels, then its transparency. What libvips has no interpretation for (indexed, Lab, + * multichannel) is MULTIBAND, the colour mode data as "psd-color-mode-data". + * + * minipsd is compiled in here, private (MINIPSD_STATIC), so a program that uses minipsd itself + * keeps its own. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#define MINIPSD_STATIC +#define MINIPSD_IMPLEMENTATION +#include "minipsd.h" + +#include "minipsd_vips.h" + +#include + +#include + +#define PSD_SIGNATURE "8BPS" +#define PSD_SIGNATURE_SIZE 4 +#define PSD_SNIFF_SIZE 6 /* the signature and the version (1 PSD, 2 PSB), big-endian */ +#define PSD_UNIT_CM 2 /* resolution info's hResUnit: pixels per cm, shown in cm */ +#define MM_PER_INCH 25.4 + +/* libvips' VIPS_ARG_* macros pass vips_argument_get_id()'s int as a guint property id, and + G_STRUCT_OFFSET's glong as a guint offset, which -Wsign-conversion and -Wconversion report in + our code; only around them are they off. */ +#if defined(__GNUC__) +# define PSD_ARGS_BEGIN \ + _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wsign-conversion\"") \ + _Pragma("GCC diagnostic ignored \"-Wconversion\"") +# define PSD_ARGS_END _Pragma("GCC diagnostic pop") +#else +# define PSD_ARGS_BEGIN +# define PSD_ARGS_END +#endif + +/* --- The base class: a PSD read from a source --------------------------------------------- */ + +typedef struct _VipsForeignLoadPsd { + VipsForeignLoad parent_object; + VipsSource *source; /* set by the subclass's build */ + minipsd_reader *reader; + minipsd_info info; +} VipsForeignLoadPsd; + +typedef VipsForeignLoadClass VipsForeignLoadPsdClass; + +GType vips_foreign_load_psd_get_type(void); +G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadPsd, vips_foreign_load_psd, VIPS_TYPE_FOREIGN_LOAD) + +/* Do these bytes start a PSD or a PSB? */ +static int psd_is_a_bytes(const unsigned char *data, size_t size) +{ + return size >= PSD_SNIFF_SIZE && memcmp(data, PSD_SIGNATURE, PSD_SIGNATURE_SIZE) == 0 && data[4] == 0 + && (data[5] == MINIPSD_PSD || data[5] == MINIPSD_PSB); +} + +static gboolean psd_is_a_source(VipsSource *source) +{ + const unsigned char *data = vips_source_sniff(source, PSD_SNIFF_SIZE); + return data && psd_is_a_bytes(data, PSD_SNIFF_SIZE); +} + +/* GObject resets set_property and get_property in every class (g_object_base_class_init), so + each class with arguments of its own sets libvips' again. */ +static void psd_class_properties(void *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; +} + +static void vips_foreign_load_psd_dispose(GObject *gobject) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)gobject; + VIPS_UNREF(psd->source); + G_OBJECT_CLASS(vips_foreign_load_psd_parent_class)->dispose(gobject); +} + +static VipsForeignFlags vips_foreign_load_psd_get_flags(VipsForeignLoad *load) +{ + (void)load; + return 0; +} + +/* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ +static VipsBandFormat psd_format(const minipsd_info *info) +{ + return info->depth == 32 ? VIPS_FORMAT_FLOAT : info->depth == 16 ? VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR; +} + +/* What the samples are, as libvips names it; MULTIBAND where it has no name for the mode. */ +static VipsInterpretation psd_interpretation(const minipsd_info *info) +{ + switch (info->color_mode) { + case MINIPSD_MODE_RGB: + return info->depth == 32 ? VIPS_INTERPRETATION_scRGB + : info->depth == 16 ? VIPS_INTERPRETATION_RGB16 : VIPS_INTERPRETATION_sRGB; + case MINIPSD_MODE_BITMAP: + case MINIPSD_MODE_GREY: + case MINIPSD_MODE_DUOTONE: + return info->depth == 16 ? VIPS_INTERPRETATION_GREY16 : VIPS_INTERPRETATION_B_W; + case MINIPSD_MODE_CMYK: + return VIPS_INTERPRETATION_CMYK; + default: + return VIPS_INTERPRETATION_MULTIBAND; + } +} + +/* A resource's bytes as a blob of the image, when the file has it. */ +static void psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, const char *name) +{ + minipsd_resource res; + if (minipsd_reader_find_resource(psd->reader, id, &res) == MINIPSD_OK && res.data.size) + vips_image_set_blob_copy(((VipsForeignLoad *)psd)->out, name, res.data.data, res.data.size); +} + +static int vips_foreign_load_psd_header(VipsForeignLoad *load) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(load); + minipsd_resource res; + minipsd_resolution_info resolution; + minipsd_version_info version; + double xres = psd->info.dpi, yres; + const char *unit = "in"; + const void *data; + void *workspace; + size_t size, workspace_size; + int rc; + + if (!(data = vips_source_map(psd->source, &size))) return -1; + if (minipsd_reader_workspace_size(&workspace_size) != MINIPSD_OK + || !(workspace = vips_malloc(VIPS_OBJECT(load), workspace_size))) + return -1; + rc = minipsd_reader_init(&psd->reader, data, size, workspace, workspace_size, NULL, &psd->info); + if (rc != MINIPSD_OK) { + vips_error(class->nickname, "%s", minipsd_error_string(rc)); + return -1; + } + + /* The resolution: resource 1005 whole, or none (libvips' default). */ + yres = xres; + if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_RESOLUTION_INFO, &res) == MINIPSD_OK + && minipsd_resource_resolution_info(&res, &resolution) == MINIPSD_OK) { + xres = resolution.horizontal_resolution; + yres = resolution.vertical_resolution; + if (resolution.horizontal_resolution_unit == PSD_UNIT_CM) unit = "cm"; + } + + vips_image_init_fields(load->out, (int)psd->info.width, (int)psd->info.height, + psd->info.color_channels + (psd->info.composite_has_alpha ? 1 : 0), + psd_format(&psd->info), VIPS_CODING_NONE, psd_interpretation(&psd->info), + xres > 0.0 ? xres / MM_PER_INCH : 1.0, yres > 0.0 ? yres / MM_PER_INCH : 1.0); + if (vips_image_pipelinev(load->out, VIPS_DEMAND_STYLE_THINSTRIP, NULL)) return -1; + if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); + + if (psd->info.icc.size) + vips_image_set_blob_copy(load->out, VIPS_META_ICC_NAME, psd->info.icc.data, psd->info.icc.size); + psd_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); + psd_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); + psd_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); + if (psd->info.color_mode_data.size) + vips_image_set_blob_copy(load->out, "psd-color-mode-data", psd->info.color_mode_data.data, + psd->info.color_mode_data.size); + vips_image_set_double(load->out, "psd-pixel-aspect-ratio", psd->info.pixel_aspect_ratio); + /* Without version info (before Photoshop 6) the composite is real. */ + vips_image_set_int(load->out, "psd-has-real-merged-data", + minipsd_reader_find_resource(psd->reader, MINIPSD_RES_VERSION_INFO, &res) != MINIPSD_OK + || minipsd_resource_version_info(&res, &version) != MINIPSD_OK + || version.has_real_merged_data); + return 0; +} + +static int vips_foreign_load_psd_load(VipsForeignLoad *load) +{ + vips_error(VIPS_OBJECT_GET_CLASS(load)->nickname, "%s", "pixels are not decoded yet"); + return -1; +} + +static void vips_foreign_load_psd_class_init(VipsForeignLoadPsdClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + gobject_class->dispose = vips_foreign_load_psd_dispose; + psd_class_properties(class); + + object_class->nickname = "psdload_base"; + object_class->description = "load PSD and PSB with minipsd"; + + load_class->get_flags = vips_foreign_load_psd_get_flags; + load_class->header = vips_foreign_load_psd_header; + load_class->load = vips_foreign_load_psd_load; +} + +static void vips_foreign_load_psd_init(VipsForeignLoadPsd *psd) +{ + (void)psd; +} + +static const char *psd_suffs[] = { ".psd", ".psb", NULL }; + +/* --- psdload_source ----------------------------------------------------------------------- */ + +typedef struct _VipsForeignLoadPsdSource { + VipsForeignLoadPsd parent_object; + VipsSource *source; +} VipsForeignLoadPsdSource; + +typedef VipsForeignLoadPsdClass VipsForeignLoadPsdSourceClass; + +GType vips_foreign_load_psd_source_get_type(void); +G_DEFINE_TYPE(VipsForeignLoadPsdSource, vips_foreign_load_psd_source, vips_foreign_load_psd_get_type()) + +static int vips_foreign_load_psd_source_build(VipsObject *object) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; + VipsForeignLoadPsdSource *source = (VipsForeignLoadPsdSource *)object; + if (source->source) { + psd->source = source->source; + g_object_ref(psd->source); + } + return VIPS_OBJECT_CLASS(vips_foreign_load_psd_source_parent_class)->build(object); +} + +static void vips_foreign_load_psd_source_class_init(VipsForeignLoadPsdSourceClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class); + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + psd_class_properties(class); + object_class->nickname = "psdload_source"; + object_class->description = "load PSD and PSB from a source with minipsd"; + object_class->build = vips_foreign_load_psd_source_build; + operation_class->flags |= VIPS_OPERATION_NOCACHE; + load_class->is_a_source = psd_is_a_source; + + PSD_ARGS_BEGIN + VIPS_ARG_OBJECT(class, "source", 1, "Source", "Source to load from", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignLoadPsdSource, source), VIPS_TYPE_SOURCE); + PSD_ARGS_END +} + +static void vips_foreign_load_psd_source_init(VipsForeignLoadPsdSource *source) +{ + (void)source; +} + +/* --- psdload (a file) ---------------------------------------------------------------------- */ + +typedef struct _VipsForeignLoadPsdFile { + VipsForeignLoadPsd parent_object; + char *filename; +} VipsForeignLoadPsdFile; + +typedef VipsForeignLoadPsdClass VipsForeignLoadPsdFileClass; + +GType vips_foreign_load_psd_file_get_type(void); +G_DEFINE_TYPE(VipsForeignLoadPsdFile, vips_foreign_load_psd_file, vips_foreign_load_psd_get_type()) + +static gboolean psd_is_a_file(const char *filename) +{ + VipsSource *source = vips_source_new_from_file(filename); + gboolean is_a; + if (!source) return FALSE; + is_a = psd_is_a_source(source); + g_object_unref(source); + return is_a; +} + +static int vips_foreign_load_psd_file_build(VipsObject *object) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; + VipsForeignLoadPsdFile *file = (VipsForeignLoadPsdFile *)object; + if (file->filename && !(psd->source = vips_source_new_from_file(file->filename))) return -1; + return VIPS_OBJECT_CLASS(vips_foreign_load_psd_file_parent_class)->build(object); +} + +static void vips_foreign_load_psd_file_class_init(VipsForeignLoadPsdFileClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + psd_class_properties(class); + object_class->nickname = "psdload"; + object_class->description = "load PSD and PSB with minipsd"; + object_class->build = vips_foreign_load_psd_file_build; + foreign_class->suffs = psd_suffs; + load_class->is_a = psd_is_a_file; + + PSD_ARGS_BEGIN + VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to load from", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignLoadPsdFile, filename), NULL); + PSD_ARGS_END +} + +static void vips_foreign_load_psd_file_init(VipsForeignLoadPsdFile *file) +{ + (void)file; +} + +/* --- psdload_buffer ------------------------------------------------------------------------ */ + +typedef struct _VipsForeignLoadPsdBuffer { + VipsForeignLoadPsd parent_object; + VipsBlob *blob; +} VipsForeignLoadPsdBuffer; + +typedef VipsForeignLoadPsdClass VipsForeignLoadPsdBufferClass; + +GType vips_foreign_load_psd_buffer_get_type(void); +G_DEFINE_TYPE(VipsForeignLoadPsdBuffer, vips_foreign_load_psd_buffer, vips_foreign_load_psd_get_type()) + +static gboolean psd_is_a_buffer(const void *data, size_t size) +{ + return psd_is_a_bytes((const unsigned char *)data, size); +} + +static int vips_foreign_load_psd_buffer_build(VipsObject *object) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; + VipsForeignLoadPsdBuffer *buffer = (VipsForeignLoadPsdBuffer *)object; + if (buffer->blob && !(psd->source = vips_source_new_from_blob(buffer->blob))) return -1; + return VIPS_OBJECT_CLASS(vips_foreign_load_psd_buffer_parent_class)->build(object); +} + +static void vips_foreign_load_psd_buffer_class_init(VipsForeignLoadPsdBufferClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + psd_class_properties(class); + object_class->nickname = "psdload_buffer"; + object_class->description = "load PSD and PSB from a buffer with minipsd"; + object_class->build = vips_foreign_load_psd_buffer_build; + load_class->is_a_buffer = psd_is_a_buffer; + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to load from", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignLoadPsdBuffer, blob), VIPS_TYPE_BLOB); + PSD_ARGS_END +} + +static void vips_foreign_load_psd_buffer_init(VipsForeignLoadPsdBuffer *buffer) +{ + (void)buffer; +} + +/* --- Registration -------------------------------------------------------------------------- */ + +int minipsd_vips_init(void) +{ + /* Registering a type is enough: libvips finds loaders among VipsForeignLoad's subclasses. */ + (void)vips_foreign_load_psd_file_get_type(); + (void)vips_foreign_load_psd_buffer_get_type(); + (void)vips_foreign_load_psd_source_get_type(); + return 0; +} diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h new file mode 100644 index 0000000..509ac8c --- /dev/null +++ b/libvips-plugin/minipsd_vips.h @@ -0,0 +1,31 @@ +/** + * \file + * \brief minipsd as a libvips loader of PSD and PSB: psdload, psdload_buffer, psdload_source. + * + * libvips' prebuilt Windows builds load no modules, so a program registers the loader itself: call + * minipsd_vips_init() once, after VIPS_INIT(). vips_image_new_from_file("x.psd") and the other + * ways libvips finds a loader then find this one. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#ifndef MINIPSD_VIPS_H +#define MINIPSD_VIPS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \brief Registers minipsd's operations with libvips. Safe to call more than once. + * + * \return 0. + */ +int minipsd_vips_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* MINIPSD_VIPS_H */ diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c new file mode 100644 index 0000000..c5e776c --- /dev/null +++ b/libvips-plugin/test_vips.c @@ -0,0 +1,215 @@ +/** + * \file + * \brief The libvips plugin's test: documents minipsd writes, loaded through libvips. + * + * Usage: minipsd-vips-test out-dir. Each document is written into out-dir, then loaded as a file, + * from a buffer and from a source; the image libvips gives must say what the document does. + * Exits with 1 when anything fails. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#define MINIPSD_IMPLEMENTATION +#include "minipsd.h" + +#include "minipsd_vips.h" + +#include + +#include +#include +#include +#include + +#define WIDTH 4 +#define HEIGHT 3 +#define PIXELS (WIDTH * HEIGHT) +#define MAX_PLANES 5 /* CMYK and transparency */ +#define DPI 300.0 +#define ASPECT 0.9 +#define MM_PER_INCH 25.4 +#define EPSILON 1e-9 +#define PATH_MAX_SIZE 1024 + +static int failures; + +static void check(int ok, const char *what, const char *name) +{ + if (!ok) { + printf("FAIL %s: %s\n", name, what); + ++failures; + } +} + +/* A growing buffer the writer appends to. */ +typedef struct out_buffer { + uint8_t *data; + size_t size, cap; +} out_buffer; + +static int to_buffer(void *user, const void *data, size_t size) +{ + out_buffer *b = (out_buffer *)user; + if (b->size + size > b->cap) { + size_t cap = (b->size + size) * 2; + uint8_t *grow = (uint8_t *)realloc(b->data, cap); + if (!grow) return 0; + b->data = grow; + b->cap = cap; + } + memcpy(b->data + b->size, data, size); + b->size += size; + return 1; +} + +typedef struct expected { + const char *name; + int bands; + VipsBandFormat format; + VipsInterpretation interpretation; +} expected; + +/* What libvips says of one loaded image, against the document's. */ +static void check_image(VipsImage *image, const minipsd_doc *doc, const expected *e, const char *how) +{ + char name[PATH_MAX_SIZE]; + const void *icc; + size_t icc_size; + double ratio = 0.0; + snprintf(name, sizeof name, "%s (%s)", e->name, how); + if (!image) { + printf("FAIL %s: not loaded: %s\n", name, vips_error_buffer()); + vips_error_clear(); + ++failures; + return; + } + check(vips_image_get_width(image) == (int)doc->width && vips_image_get_height(image) == (int)doc->height, + "size", name); + check(vips_image_get_bands(image) == e->bands, "bands", name); + check(vips_image_get_format(image) == e->format, "format", name); + check(vips_image_get_interpretation(image) == e->interpretation, "interpretation", name); + check(fabs(vips_image_get_xres(image) - doc->dpi / MM_PER_INCH) < EPSILON + && fabs(vips_image_get_yres(image) - doc->dpi / MM_PER_INCH) < EPSILON, + "resolution", name); + if (doc->icc) + check(vips_image_get_blob(image, VIPS_META_ICC_NAME, &icc, &icc_size) == 0 && icc_size == doc->icc_size + && memcmp(icc, doc->icc, icc_size) == 0, + "ICC profile", name); + check(vips_image_get_double(image, "psd-pixel-aspect-ratio", &ratio) == 0 + && fabs(ratio - (doc->pixel_aspect_ratio > 0.0 ? doc->pixel_aspect_ratio : 1.0)) < EPSILON, + "pixel aspect ratio", name); + g_object_unref(image); +} + +/* Writes doc into dir, then loads it the three ways. */ +static void check_document(const minipsd_doc *doc, const expected *e, const char *dir) +{ + out_buffer b = { NULL, 0, 0 }; + char path[PATH_MAX_SIZE]; + FILE *f; + const char *loader; + VipsSource *source; + int rc = minipsd_write(doc, NULL, to_buffer, &b); + if (rc != MINIPSD_OK) { + printf("FAIL %s: not written: %s\n", e->name, minipsd_error_string(rc)); + ++failures; + free(b.data); + return; + } + snprintf(path, sizeof path, "%s/%s.%s", dir, e->name, doc->format == MINIPSD_PSB ? "psb" : "psd"); + f = fopen(path, "wb"); + check(f && fwrite(b.data, 1, b.size, f) == b.size, "file written", e->name); + if (f) fclose(f); + + loader = vips_foreign_find_load(path); + check(loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0, "found as psdload", e->name); + vips_error_clear(); + check_image(vips_image_new_from_file(path, NULL), doc, e, "file"); + check_image(vips_image_new_from_buffer(b.data, b.size, "", NULL), doc, e, "buffer"); + source = vips_source_new_from_memory(b.data, b.size); + check_image(source ? vips_image_new_from_source(source, "", NULL) : NULL, doc, e, "source"); + if (source) g_object_unref(source); + free(b.data); +} + +int main(int argc, char **argv) +{ + static const uint8_t icc[] = "not a real ICC profile"; + static uint8_t rgba[PIXELS * 4], cmyka[PIXELS * MAX_PLANES], bits[PIXELS]; + static uint16_t grey16[PIXELS * 2]; + static const char junk[] = "8BPX, not a PSD"; + minipsd_layer layer; + minipsd_doc doc; + const char *loader; + size_t i; + + if (argc != 2) { + fprintf(stderr, "usage: %s out-dir\n", argv[0]); + return 2; + } + if (VIPS_INIT(argv[0])) vips_error_exit(NULL); + minipsd_vips_init(); + minipsd_vips_init(); /* twice is harmless */ + + for (i = 0; i < sizeof rgba; ++i) rgba[i] = (uint8_t)(i * 7); + for (i = 0; i < sizeof cmyka; ++i) cmyka[i] = (uint8_t)(i * 5); + for (i = 0; i < PIXELS * 2; ++i) grey16[i] = (uint16_t)(i * 4099); + for (i = 0; i < PIXELS; ++i) bits[i] = (uint8_t)(i % 2); + + { /* 8-bit RGB with a layer, a profile and non-square pixels; as PSD and as PSB */ + static const expected psd = { "rgb8", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB }; + static const expected psb = { "rgb8-psb", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB }; + memset(&layer, 0, sizeof layer); + layer.name = "Layer"; + layer.pixels = rgba; + layer.opacity = 255; + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = &layer, doc.layer_count = 1; + doc.icc = icc, doc.icc_size = sizeof icc; + doc.pixel_aspect_ratio = ASPECT; + check_document(&doc, &psd, argv[1]); + doc.format = MINIPSD_PSB; + check_document(&doc, &psb, argv[1]); + } + /* Grey and CMYK: a layer and its composite (the writer composes RGB only); with a layer, the + negative layer count says the composite's transparency is there. */ + { /* 16-bit grey */ + static const expected e = { "grey16", 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16 }; + layer.pixels = (const uint8_t *)grey16; + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = &layer, doc.layer_count = 1; + doc.color_mode = MINIPSD_MODE_GREY, doc.depth = 16; + doc.composite = (const uint8_t *)grey16; + check_document(&doc, &e, argv[1]); + } + { /* 8-bit CMYK */ + static const expected e = { "cmyk8", 5, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_CMYK }; + layer.pixels = cmyka; + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = &layer, doc.layer_count = 1; + doc.color_mode = MINIPSD_MODE_CMYK; + doc.composite = cmyka; + check_document(&doc, &e, argv[1]); + } + { /* bitmap: one band, no transparency */ + static const expected e = { "bitmap", 1, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_B_W }; + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.color_mode = MINIPSD_MODE_BITMAP, doc.depth = 1; + doc.composite = bits; + check_document(&doc, &e, argv[1]); + } + + /* Not a PSD: psdload does not claim it. */ + loader = vips_foreign_find_load_buffer(junk, sizeof junk); + check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); + vips_error_clear(); + + vips_shutdown(); + printf(failures ? "%d failed\n" : "vips plugin: all passed\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7b97054..8e9045a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,6 +11,7 @@ # example. each program in examples/, its output checked against the golden bytes # fuzz.replay. the reader's fuzz target on each input in fuzz/regressions/ (see fuzz/) # consumer- a tiny project using minipsd each supported way (see consumer/) +# vips. the libvips plugin (libvips-plugin/), with MINIPSD_VIPS_PLUGIN # --------------------------------------------------------------------------- # GoogleTest: a system install if there is one, otherwise fetched. @@ -216,9 +217,22 @@ endforeach() add_test(NAME fuzz.replay.example COMMAND minipsd-fuzz-replay "${PROJECT_BINARY_DIR}/examples/out-write_memory/out.psd") set_tests_properties(fuzz.replay.example PROPERTIES FIXTURES_REQUIRED minipsd-example-psd) +# libvips, for what uses it (cmake/minipsd_libvips.cmake): both off by default, as libvips is +# fetched (Windows) or must be installed (pkg-config). +option(MINIPSD_VIPS_EXAMPLE "Build and run examples/vips_composite.c against libvips (fetched on Windows)" OFF) +option(MINIPSD_VIPS_PLUGIN "Build and test the libvips plugin in libvips-plugin/ (libvips fetched on Windows)" OFF) +if(MINIPSD_VIPS_EXAMPLE OR MINIPSD_VIPS_PLUGIN) + include("${PROJECT_SOURCE_DIR}/cmake/minipsd_libvips.cmake") +endif() + # The examples (examples/), built with the same warnings and run as tests. add_subdirectory("${PROJECT_SOURCE_DIR}/examples" "${PROJECT_BINARY_DIR}/examples") +# The libvips plugin: psdload and psdsave registered by the program that links it. +if(MINIPSD_VIPS_PLUGIN) + add_subdirectory("${PROJECT_SOURCE_DIR}/libvips-plugin" "${PROJECT_BINARY_DIR}/libvips-plugin") +endif() + # clang-tidy (the linux-tidy preset) on our own targets only, not on a fetched GoogleTest. # Checks and options come from .clang-tidy at the repository root. set(MINIPSD_CLANG_TIDY "" CACHE STRING "clang-tidy command to run on the test sources; empty = off") From 3fc9ab3dba22cad42c9167a139db28bd928385c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sat, 24 Oct 2026 21:29:14 +0200 Subject: [PATCH 02/22] feat(vips): the composite decoded in bands of rows (S7.3, 2) psdload's pixels: vips_image_generate, then vips_sequential, as libvips' own spngload; each band is one minipsd_decode_composite straight into the region, from libvips' threads at once on the one reader. Bitmap comes as B_W (black 0, white 255); bands stop at MINIPSD_PLANES. vips.plugin compares every pixel with minipsd's own decode: documents of 301 x 203 in RGB 8 and 16 (PSD and PSB), grey 16, CMYK 8 and bitmap, loaded randomly, sequentially, from a buffer and from a source, then tests/data, tests/fixtures and psd-tools' set when configured (318 of 319 the same, one truncated file refused by both). --- libvips-plugin/CMakeLists.txt | 17 +++- libvips-plugin/minipsd_vips.c | 71 +++++++++++++-- libvips-plugin/test_vips.c | 157 ++++++++++++++++++++++++++++++---- 3 files changed, 219 insertions(+), 26 deletions(-) diff --git a/libvips-plugin/CMakeLists.txt b/libvips-plugin/CMakeLists.txt index b8d5e1f..33545f1 100644 --- a/libvips-plugin/CMakeLists.txt +++ b/libvips-plugin/CMakeLists.txt @@ -4,7 +4,8 @@ # into it, private (MINIPSD_STATIC). libvips comes from cmake/minipsd_libvips.cmake. # # minipsd::vips the plugin -# vips.plugin documents minipsd writes, loaded through libvips (test_vips.c) +# vips.plugin documents minipsd writes and the PSDs at hand, loaded through libvips and +# compared with minipsd's own decode (test_vips.c) # --------------------------------------------------------------------------- add_library(minipsd_vips STATIC minipsd_vips.c minipsd_vips.h) @@ -22,8 +23,20 @@ if(MSVC) target_compile_definitions(minipsd-vips-test PRIVATE _CRT_SECURE_NO_WARNINGS) # plain fopen endif() +# The files compared beside the documents: tests/data, then, when there, tests/fixtures and +# psd-tools' set (MINIPSD_PSD_TOOLS_FILES), one path a line. +file(GLOB _vips_files "${PROJECT_SOURCE_DIR}/tests/data/*.psd") +foreach(_dir IN ITEMS "${_minipsd_fixtures}" "${_minipsd_psd_tools_files}") + if(_dir) + file(GLOB_RECURSE _found "${_dir}/*.psd" "${_dir}/*.psb") + list(APPEND _vips_files ${_found}) + endif() +endforeach() +list(JOIN _vips_files "\n" _vips_list) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/files.txt" "${_vips_list}\n") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/out") -add_test(NAME vips.plugin COMMAND minipsd-vips-test "${CMAKE_CURRENT_BINARY_DIR}/out") +add_test(NAME vips.plugin COMMAND minipsd-vips-test "${CMAKE_CURRENT_BINARY_DIR}/out" "${CMAKE_CURRENT_BINARY_DIR}/files.txt") # TIMEOUT: libvips' exit has once hung on Windows (examples/), so a hang fails fast. set_tests_properties(vips.plugin PROPERTIES PASS_REGULAR_EXPRESSION "vips plugin: all passed" TIMEOUT 120) if(MINIPSD_LIBVIPS_ENV) diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index 4d43bce..485b505 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -4,7 +4,8 @@ * * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer * variants make one), so the file can move into libvips/foreign/ as it is. The file is mapped - * (vips_source_map) and read in place by minipsd's reader; nothing is copied. + * (vips_source_map) and read in place by minipsd's reader; nothing is copied. Pixels are decoded + * in bands of rows as libvips asks for them, from its threads at once. * * The image is the composite as the file stores it, no colour converted: the mode's colour * channels, then its transparency. What libvips has no interpretation for (indexed, Lab, @@ -25,6 +26,7 @@ #include +#include #include #define PSD_SIGNATURE "8BPS" @@ -53,6 +55,8 @@ typedef struct _VipsForeignLoadPsd { VipsSource *source; /* set by the subclass's build */ minipsd_reader *reader; minipsd_info info; + unsigned bands; /* the colour channels and transparency, up to MINIPSD_PLANES */ + double xres, yres; /* pixels per mm */ } VipsForeignLoadPsd; typedef VipsForeignLoadClass VipsForeignLoadPsdClass; @@ -92,7 +96,7 @@ static void vips_foreign_load_psd_dispose(GObject *gobject) static VipsForeignFlags vips_foreign_load_psd_get_flags(VipsForeignLoad *load) { (void)load; - return 0; + return VIPS_FOREIGN_SEQUENTIAL; /* bands of rows; libvips keeps the image for random access */ } /* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ @@ -119,6 +123,14 @@ static VipsInterpretation psd_interpretation(const minipsd_info *info) } } +/* The image's fields: on out, and on the image load() generates. */ +static int psd_fields(VipsForeignLoadPsd *psd, VipsImage *image) +{ + vips_image_init_fields(image, (int)psd->info.width, (int)psd->info.height, (int)psd->bands, psd_format(&psd->info), + VIPS_CODING_NONE, psd_interpretation(&psd->info), psd->xres, psd->yres); + return vips_image_pipelinev(image, VIPS_DEMAND_STYLE_THINSTRIP, NULL); +} + /* A resource's bytes as a blob of the image, when the file has it. */ static void psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, const char *name) { @@ -160,11 +172,11 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) if (resolution.horizontal_resolution_unit == PSD_UNIT_CM) unit = "cm"; } - vips_image_init_fields(load->out, (int)psd->info.width, (int)psd->info.height, - psd->info.color_channels + (psd->info.composite_has_alpha ? 1 : 0), - psd_format(&psd->info), VIPS_CODING_NONE, psd_interpretation(&psd->info), - xres > 0.0 ? xres / MM_PER_INCH : 1.0, yres > 0.0 ? yres / MM_PER_INCH : 1.0); - if (vips_image_pipelinev(load->out, VIPS_DEMAND_STYLE_THINSTRIP, NULL)) return -1; + /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ + psd->bands = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); + psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; + psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; + if (psd_fields(psd, load->out)) return -1; if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); if (psd->info.icc.size) @@ -184,10 +196,51 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) return 0; } +/* One band of rows, decoded straight into the region. vips_sequential asks for whole rows; calls + run at once from libvips' threads on the one reader, which minipsd allows on a whole file. */ +static int psd_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; + const VipsRect *rect = ®ion->valid; + minipsd_decode_opts opts; + minipsd_target target; + VipsPel *top = VIPS_REGION_ADDR_TOPLEFT(region), *row; + const size_t skip = VIPS_REGION_LSKIP(region); + int rc, x, y; + (void)seq, (void)b, (void)stop; + + if (rect->left != 0 || rect->width != region->im->Xsize) { + vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", "only whole rows are decoded"); + return -1; + } + memset(&opts, 0, sizeof opts); + opts.y_first = (uint32_t)rect->top; + opts.y_count = (uint32_t)rect->height; + opts.depth = psd->info.depth; + minipsd_target_interleaved(&target, top, skip, psd->bands, psd->info.depth); + rc = minipsd_decode_composite(psd->reader, &target, &opts); + if (rc != MINIPSD_OK) { + vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", minipsd_error_string(rc)); + return -1; + } + + /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ + if (psd->info.color_mode == MINIPSD_MODE_BITMAP) + for (y = 0; y < rect->height; ++y) + for (row = top + (size_t)y * skip, x = 0; x < rect->width; ++x) + row[x] = row[x] ? 0 : UCHAR_MAX; + return 0; +} + static int vips_foreign_load_psd_load(VipsForeignLoad *load) { - vips_error(VIPS_OBJECT_GET_CLASS(load)->nickname, "%s", "pixels are not decoded yet"); - return -1; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; + VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(load), 2); + t[0] = vips_image_new(); + if (psd_fields(psd, t[0]) || vips_image_generate(t[0], NULL, psd_generate, NULL, psd, NULL) + || vips_sequential(t[0], &t[1], NULL) || vips_image_write(t[1], load->real)) + return -1; + return 0; } static void vips_foreign_load_psd_class_init(VipsForeignLoadPsdClass *class) diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index c5e776c..3a0af56 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -2,9 +2,12 @@ * \file * \brief The libvips plugin's test: documents minipsd writes, loaded through libvips. * - * Usage: minipsd-vips-test out-dir. Each document is written into out-dir, then loaded as a file, - * from a buffer and from a source; the image libvips gives must say what the document does. - * Exits with 1 when anything fails. + * Usage: minipsd-vips-test out-dir [list]. Each document is written into out-dir, then loaded as a + * file (randomly and sequentially), from a buffer and from a source; the image libvips gives must + * say what the document does, and its pixels be what minipsd decodes. list names other files, one + * path a line, each loaded as a file and compared with minipsd's decode the same way (or refused by + * both). libvips runs on several threads, so bands are decoded at once. Exits with 1 when + * anything fails. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha @@ -17,13 +20,14 @@ #include +#include #include #include #include #include -#define WIDTH 4 -#define HEIGHT 3 +#define WIDTH 301 /* odd sizes, and rows enough for several bands */ +#define HEIGHT 203 #define PIXELS (WIDTH * HEIGHT) #define MAX_PLANES 5 /* CMYK and transparency */ #define DPI 300.0 @@ -31,6 +35,8 @@ #define MM_PER_INCH 25.4 #define EPSILON 1e-9 #define PATH_MAX_SIZE 1024 +#define WORKSPACE_SIZE 256 +#define THREADS 4 static int failures; @@ -70,8 +76,69 @@ typedef struct expected { VipsInterpretation interpretation; } expected; -/* What libvips says of one loaded image, against the document's. */ -static void check_image(VipsImage *image, const minipsd_doc *doc, const expected *e, const char *how) +/* The composite as minipsd decodes it, laid out as the loader gives it: interleaved, the colour + channels and transparency up to MINIPSD_PLANES, bitmap's black 0 and white 255. NULL with the + error in *rc when minipsd refuses the file. */ +static uint8_t *decode_composite(const void *file, size_t size, size_t *bytes, int *rc) +{ + uint8_t workspace[WORKSPACE_SIZE], *pixels; + minipsd_reader *r; + minipsd_info info; + minipsd_target target; + minipsd_decode_opts opts; + size_t need, i, row; + unsigned bands; + if ((*rc = minipsd_reader_workspace_size(&need)) != MINIPSD_OK) return NULL; + if (need > sizeof workspace) { + *rc = MINIPSD_ERR_ARG; + return NULL; + } + if ((*rc = minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info)) != MINIPSD_OK) return NULL; + bands = info.color_channels + (info.composite_has_alpha ? 1u : 0u); + if (bands > MINIPSD_PLANES) bands = MINIPSD_PLANES; + row = (size_t)info.width * bands * (info.depth == 32 ? sizeof(float) : info.depth == 16 ? sizeof(uint16_t) : 1); + *bytes = row * info.height; + if (!(pixels = (uint8_t *)malloc(*bytes ? *bytes : 1))) { + *rc = MINIPSD_ERR_ALLOC; + return NULL; + } + minipsd_target_interleaved(&target, pixels, row, bands, info.depth); + memset(&opts, 0, sizeof opts); + opts.depth = info.depth; + if ((*rc = minipsd_decode_composite(r, &target, &opts)) != MINIPSD_OK) { + free(pixels); + return NULL; + } + if (info.color_mode == MINIPSD_MODE_BITMAP) + for (i = 0; i < *bytes; ++i) pixels[i] = pixels[i] ? 0 : UCHAR_MAX; + return pixels; +} + +/* The loaded image's pixels against minipsd's own decode of the file: the same bytes, or both + refuse it. Takes the image. Returns 1 when both decoded, 0 when both refused. */ +static int check_pixels(VipsImage *image, const void *file, size_t size, const char *name) +{ + size_t bytes = 0, got_bytes = 0; + int rc, both = 0; + uint8_t *want = decode_composite(file, size, &bytes, &rc); + void *got = image ? vips_image_write_to_memory(image, &got_bytes) : NULL; + if (want) { + both = 1; + if (!got) printf("FAIL %s: libvips refused it: %s\n", name, vips_error_buffer()); + check(got && got_bytes == bytes && memcmp(got, want, bytes) == 0, "pixels as minipsd decodes them", name); + } else { + check(!got, "refused as minipsd refuses it", name); + } + vips_error_clear(); + g_free(got); + free(want); + if (image) g_object_unref(image); + return both; +} + +/* What libvips says of one loaded image, against the document's; then its pixels. */ +static void check_image(VipsImage *image, const minipsd_doc *doc, const expected *e, const char *how, + const out_buffer *file) { char name[PATH_MAX_SIZE]; const void *icc; @@ -99,7 +166,7 @@ static void check_image(VipsImage *image, const minipsd_doc *doc, const expected check(vips_image_get_double(image, "psd-pixel-aspect-ratio", &ratio) == 0 && fabs(ratio - (doc->pixel_aspect_ratio > 0.0 ? doc->pixel_aspect_ratio : 1.0)) < EPSILON, "pixel aspect ratio", name); - g_object_unref(image); + check(check_pixels(image, file->data, file->size, name), "decoded", name); } /* Writes doc into dir, then loads it the three ways. */ @@ -125,37 +192,86 @@ static void check_document(const minipsd_doc *doc, const expected *e, const char loader = vips_foreign_find_load(path); check(loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0, "found as psdload", e->name); vips_error_clear(); - check_image(vips_image_new_from_file(path, NULL), doc, e, "file"); - check_image(vips_image_new_from_buffer(b.data, b.size, "", NULL), doc, e, "buffer"); + check_image(vips_image_new_from_file(path, NULL), doc, e, "file", &b); + check_image(vips_image_new_from_file(path, "access", VIPS_ACCESS_SEQUENTIAL, NULL), doc, e, "file, sequential", &b); + check_image(vips_image_new_from_buffer(b.data, b.size, "", NULL), doc, e, "buffer", &b); source = vips_source_new_from_memory(b.data, b.size); - check_image(source ? vips_image_new_from_source(source, "", NULL) : NULL, doc, e, "source"); + check_image(source ? vips_image_new_from_source(source, "", NULL) : NULL, doc, e, "source", &b); if (source) g_object_unref(source); free(b.data); } +/* The whole file in a new buffer; NULL when it cannot be read. */ +static uint8_t *read_all(const char *path, size_t *size) +{ + uint8_t *data = NULL; + long n; + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + if (fseek(f, 0, SEEK_END) == 0 && (n = ftell(f)) >= 0 && fseek(f, 0, SEEK_SET) == 0) { + *size = (size_t)n; + data = (uint8_t *)malloc(*size ? *size : 1); + if (data && fread(data, 1, *size, f) != *size) { + free(data); + data = NULL; + } + } + fclose(f); + return data; +} + +/* Each file the list names, loaded through libvips and compared with minipsd's decode. */ +static void check_list(const char *list) +{ + char path[PATH_MAX_SIZE]; + size_t size, length, files = 0, decoded = 0; + uint8_t *data; + FILE *f = fopen(list, "r"); + if (!f) { + check(0, "list read", list); + return; + } + while (fgets(path, sizeof path, f)) { + length = strcspn(path, "\r\n"); + path[length] = 0; + if (!length) continue; + if (!(data = read_all(path, &size))) { + check(0, "file read", path); + continue; + } + ++files; + decoded += (size_t)check_pixels(vips_image_new_from_file(path, NULL), data, size, path); + free(data); + } + fclose(f); + printf("%zu files: %zu decoded as minipsd decodes them, %zu refused by both\n", files, decoded, files - decoded); +} + int main(int argc, char **argv) { static const uint8_t icc[] = "not a real ICC profile"; static uint8_t rgba[PIXELS * 4], cmyka[PIXELS * MAX_PLANES], bits[PIXELS]; - static uint16_t grey16[PIXELS * 2]; + static uint16_t grey16[PIXELS * 2], rgba16[PIXELS * 4]; static const char junk[] = "8BPX, not a PSD"; minipsd_layer layer; minipsd_doc doc; const char *loader; size_t i; - if (argc != 2) { - fprintf(stderr, "usage: %s out-dir\n", argv[0]); + if (argc != 2 && argc != 3) { + fprintf(stderr, "usage: %s out-dir [list]\n", argv[0]); return 2; } if (VIPS_INIT(argv[0])) vips_error_exit(NULL); + vips_concurrency_set(THREADS); minipsd_vips_init(); minipsd_vips_init(); /* twice is harmless */ for (i = 0; i < sizeof rgba; ++i) rgba[i] = (uint8_t)(i * 7); for (i = 0; i < sizeof cmyka; ++i) cmyka[i] = (uint8_t)(i * 5); for (i = 0; i < PIXELS * 2; ++i) grey16[i] = (uint16_t)(i * 4099); - for (i = 0; i < PIXELS; ++i) bits[i] = (uint8_t)(i % 2); + for (i = 0; i < PIXELS * 4; ++i) rgba16[i] = (uint16_t)(i * 2731); + for (i = 0; i < PIXELS; ++i) bits[i] = (uint8_t)(i % 3 == 0); { /* 8-bit RGB with a layer, a profile and non-square pixels; as PSD and as PSB */ static const expected psd = { "rgb8", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB }; @@ -173,6 +289,15 @@ int main(int argc, char **argv) doc.format = MINIPSD_PSB; check_document(&doc, &psb, argv[1]); } + { /* 16-bit RGB, its composite composed by the writer */ + static const expected e = { "rgb16", 4, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_RGB16 }; + layer.pixels = (const uint8_t *)rgba16; + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = &layer, doc.layer_count = 1; + doc.depth = 16; + check_document(&doc, &e, argv[1]); + } /* Grey and CMYK: a layer and its composite (the writer composes RGB only); with a layer, the negative layer count says the composite's transparency is there. */ { /* 16-bit grey */ @@ -209,6 +334,8 @@ int main(int argc, char **argv) check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); vips_error_clear(); + if (argc == 3) check_list(argv[2]); + vips_shutdown(); printf(failures ? "%d failed\n" : "vips plugin: all passed\n", failures); return failures ? 1 : 0; From b2c678d692336ca8e07fda08e20ca7247d6d2f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sat, 24 Oct 2026 21:55:13 +0200 Subject: [PATCH 03/22] feat(vips): layers, ZIP and indexed in psdload (S7.3, 3) layer=N loads one layer (bottom first; -1, the default, the composite): its rectangle, placed by xoffset and yoffset, with psd-layer-name, -blend-mode, -opacity and -hidden; every image says psd-layer-count. An empty layer is refused. ZIP layers inflate through the zlib libvips links (zlib 1.3.1's headers fetched and pinned on Windows), each thread keeping its channels inflated between bands. Indexed comes through its palette as sRGB, RGBA with a transparent index (1047), as libvips' own loaders give palettes; bits-per-sample is the file's depth. vips.plugin checks each document's layers against the pixels it gave, ZIP and ZIP-predicted ones among them, indexed against its palette, and every layer of the listed files against minipsd's decode. --- cmake/minipsd_libvips.cmake | 19 ++ libvips-plugin/CMakeLists.txt | 4 +- libvips-plugin/minipsd_vips.c | 245 +++++++++++++++++++---- libvips-plugin/test_vips.c | 357 +++++++++++++++++++++++++++------- 4 files changed, 516 insertions(+), 109 deletions(-) diff --git a/cmake/minipsd_libvips.cmake b/cmake/minipsd_libvips.cmake index c4d15db..0094249 100644 --- a/cmake/minipsd_libvips.cmake +++ b/cmake/minipsd_libvips.cmake @@ -6,6 +6,9 @@ # elsewhere pkg-config finds the installed one. Gives: # # minipsd_libvips an imported target: libvips' C API, glib and gobject +# minipsd_libvips_zlib the zlib libvips links: Windows, the zip's libz1 (zlib-ng, built +# zlib-compatible; versions.json) with zlib's own headers, fetched and +# pinned, since the zip has none; elsewhere pkg-config's zlib # MINIPSD_LIBVIPS_ENV the environment its tests run in (Windows: the DLLs on PATH), or "" # --------------------------------------------------------------------------- @@ -29,6 +32,19 @@ if(WIN32) endif() target_include_directories(minipsd_libvips SYSTEM INTERFACE "${_vips}/include" "${_vips}/include/glib-2.0" "${_vips}/lib/glib-2.0/include") + FetchContent_Declare(minipsd_zlib_headers + URL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz + URL_HASH SHA256=9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + SOURCE_SUBDIR no-cmake-project) # zlib.h and zconf.h only + FetchContent_MakeAvailable(minipsd_zlib_headers) + add_library(minipsd_libvips_zlib INTERFACE IMPORTED) + target_include_directories(minipsd_libvips_zlib SYSTEM INTERFACE "${minipsd_zlib_headers_SOURCE_DIR}") + if(MSVC) + target_link_libraries(minipsd_libvips_zlib INTERFACE "${_vips}/lib/libz1.lib") + else() + target_link_libraries(minipsd_libvips_zlib INTERFACE "${_vips}/bin/libz1.dll") + endif() set(_vips_path "${_vips}/bin;$ENV{PATH}") # the DLLs, for the tests string(REPLACE ";" "\\;" _vips_path "${_vips_path}") set(MINIPSD_LIBVIPS_ENV "PATH=${_vips_path}") @@ -37,5 +53,8 @@ else() pkg_check_modules(VIPS REQUIRED IMPORTED_TARGET vips) add_library(minipsd_libvips INTERFACE IMPORTED) target_link_libraries(minipsd_libvips INTERFACE PkgConfig::VIPS) + pkg_check_modules(VIPS_ZLIB REQUIRED IMPORTED_TARGET zlib) + add_library(minipsd_libvips_zlib INTERFACE IMPORTED) + target_link_libraries(minipsd_libvips_zlib INTERFACE PkgConfig::VIPS_ZLIB) set(MINIPSD_LIBVIPS_ENV "") endif() diff --git a/libvips-plugin/CMakeLists.txt b/libvips-plugin/CMakeLists.txt index 33545f1..169c04f 100644 --- a/libvips-plugin/CMakeLists.txt +++ b/libvips-plugin/CMakeLists.txt @@ -11,12 +11,12 @@ add_library(minipsd_vips STATIC minipsd_vips.c minipsd_vips.h) add_library(minipsd::vips ALIAS minipsd_vips) target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd::minipsd) +target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd::minipsd minipsd_libvips_zlib) target_compile_options(minipsd_vips PRIVATE ${MINIPSD_WARNINGS}) set_target_properties(minipsd_vips PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) add_executable(minipsd-vips-test test_vips.c) -target_link_libraries(minipsd-vips-test PRIVATE minipsd::vips minipsd::minipsd) +target_link_libraries(minipsd-vips-test PRIVATE minipsd::vips minipsd::minipsd minipsd_libvips_zlib) target_compile_options(minipsd-vips-test PRIVATE ${MINIPSD_WARNINGS}) set_target_properties(minipsd-vips-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) if(MSVC) diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index 485b505..5070e48 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -7,9 +7,13 @@ * (vips_source_map) and read in place by minipsd's reader; nothing is copied. Pixels are decoded * in bands of rows as libvips asks for them, from its threads at once. * - * The image is the composite as the file stores it, no colour converted: the mode's colour - * channels, then its transparency. What libvips has no interpretation for (indexed, Lab, - * multichannel) is MULTIBAND, the colour mode data as "psd-color-mode-data". + * The image is the composite, or with `layer` one layer (its position in xoffset and yoffset), as + * the file stores it, no colour converted: the mode's colour channels, then transparency (a + * layer's always, opaque when it has none). What libvips has no interpretation for (Lab, + * multichannel) is MULTIBAND; the colour mode data is kept as "psd-color-mode-data". Indexed is + * the exception: libvips has no indexed image, so it comes through its palette as sRGB, as + * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). ZIP layers + * are inflated by the zlib libvips links. * * minipsd is compiled in here, private (MINIPSD_STATIC), so a program that uses minipsd itself * keeps its own. @@ -25,15 +29,21 @@ #include "minipsd_vips.h" #include +#include #include #include -#define PSD_SIGNATURE "8BPS" -#define PSD_SIGNATURE_SIZE 4 -#define PSD_SNIFF_SIZE 6 /* the signature and the version (1 PSD, 2 PSB), big-endian */ -#define PSD_UNIT_CM 2 /* resolution info's hResUnit: pixels per cm, shown in cm */ -#define MM_PER_INCH 25.4 +#define PSD_SIGNATURE "8BPS" +#define PSD_SIGNATURE_SIZE 4 +#define PSD_SNIFF_SIZE 6 /* the signature and the version (1 PSD, 2 PSB), big-endian */ +#define PSD_UNIT_CM 2 /* resolution info's hResUnit: pixels per cm, shown in cm */ +#define PSD_STORED_ZIP 2 /* a channel's stored compression marker: ZIP, and ... */ +#define PSD_STORED_ZIP_PRED 3 /* ... ZIP with prediction */ +#define PSD_PALETTE_ENTRIES 256 /* an indexed file's palette: 256 reds, 256 greens, 256 blues */ +#define PSD_RGB 3 +#define PSD_COMPOSITE (-1) /* the `layer` option's default: the composite */ +#define MM_PER_INCH 25.4 /* libvips' VIPS_ARG_* macros pass vips_argument_get_id()'s int as a guint property id, and G_STRUCT_OFFSET's glong as a guint offset, which -Wsign-conversion and -Wconversion report in @@ -51,12 +61,20 @@ /* --- The base class: a PSD read from a source --------------------------------------------- */ typedef struct _VipsForeignLoadPsd { - VipsForeignLoad parent_object; - VipsSource *source; /* set by the subclass's build */ - minipsd_reader *reader; - minipsd_info info; - unsigned bands; /* the colour channels and transparency, up to MINIPSD_PLANES */ - double xres, yres; /* pixels per mm */ + VipsForeignLoad parent_object; + int layer; /* the option: -1 the composite, else a layer, bottom first */ + VipsSource *source; /* set by the subclass's build */ + minipsd_reader *reader; + minipsd_info info; + minipsd_layer_view view; /* the layer, with layer >= 0 */ + uint32_t width, height; /* the image: the canvas, or the layer's rectangle */ + unsigned planes; /* what minipsd decodes: colour channels and transparency, + up to MINIPSD_PLANES */ + unsigned bands; /* what libvips gets: planes, or indexed's RGB(A) */ + int indexed; /* the palette makes the colour */ + int transparent; /* indexed: the transparent index (resource 1047), or -1 */ + size_t scratch_size; /* a ZIP layer's channels inflated whole; 0 without ZIP */ + double xres, yres; /* pixels per mm */ } VipsForeignLoadPsd; typedef VipsForeignLoadClass VipsForeignLoadPsdClass; @@ -99,6 +117,11 @@ static VipsForeignFlags vips_foreign_load_psd_get_flags(VipsForeignLoad *load) return VIPS_FOREIGN_SEQUENTIAL; /* bands of rows; libvips keeps the image for random access */ } +static void psd_error(VipsForeignLoadPsd *psd, const char *message) +{ + vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", message); +} + /* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ static VipsBandFormat psd_format(const minipsd_info *info) { @@ -112,6 +135,8 @@ static VipsInterpretation psd_interpretation(const minipsd_info *info) case MINIPSD_MODE_RGB: return info->depth == 32 ? VIPS_INTERPRETATION_scRGB : info->depth == 16 ? VIPS_INTERPRETATION_RGB16 : VIPS_INTERPRETATION_sRGB; + case MINIPSD_MODE_INDEXED: + return VIPS_INTERPRETATION_sRGB; case MINIPSD_MODE_BITMAP: case MINIPSD_MODE_GREY: case MINIPSD_MODE_DUOTONE: @@ -126,8 +151,12 @@ static VipsInterpretation psd_interpretation(const minipsd_info *info) /* The image's fields: on out, and on the image load() generates. */ static int psd_fields(VipsForeignLoadPsd *psd, VipsImage *image) { - vips_image_init_fields(image, (int)psd->info.width, (int)psd->info.height, (int)psd->bands, psd_format(&psd->info), + vips_image_init_fields(image, (int)psd->width, (int)psd->height, (int)psd->bands, psd_format(&psd->info), VIPS_CODING_NONE, psd_interpretation(&psd->info), psd->xres, psd->yres); + if (psd->layer >= 0) { + image->Xoffset = psd->view.left; + image->Yoffset = psd->view.top; + } return vips_image_pipelinev(image, VIPS_DEMAND_STYLE_THINSTRIP, NULL); } @@ -139,14 +168,58 @@ static void psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, const char *name) vips_image_set_blob_copy(((VipsForeignLoad *)psd)->out, name, res.data.data, res.data.size); } +/* Bytes to inflate the layer's ZIP channels whole, each in its own part (scratch_kept); 0 when + none of the planes is ZIP. */ +static size_t psd_scratch_size(const VipsForeignLoadPsd *psd) +{ + const size_t row = ((size_t)psd->width * psd->info.depth + 7) / 8; + unsigned i; + int zip = 0; + for (i = 0; i < psd->planes; ++i) + if (psd->view.channel[i].size >= 2 && psd->view.channel[i].data && psd->view.channel[i].data[0] == 0 + && (psd->view.channel[i].data[1] == PSD_STORED_ZIP || psd->view.channel[i].data[1] == PSD_STORED_ZIP_PRED)) + zip = 1; + return zip ? psd->planes * psd->height * row : 0; +} + +/* The layer the option names: its record, its size, its name and look as metadata. */ +static int psd_find_layer(VipsForeignLoadPsd *psd) +{ + VipsImage *out = ((VipsForeignLoad *)psd)->out; + char *name; + size_t needed = 0; + int rc, i; + for (i = 0; i <= psd->layer; ++i) + if ((rc = minipsd_reader_next_layer(psd->reader, &psd->view)) != MINIPSD_MORE) { + psd_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); + return -1; + } + if (psd->view.right <= psd->view.left || psd->view.bottom <= psd->view.top) { + psd_error(psd, "the layer is empty"); + return -1; + } + psd->width = (uint32_t)(psd->view.right - psd->view.left); + psd->height = (uint32_t)(psd->view.bottom - psd->view.top); + psd->planes = VIPS_MIN(psd->info.color_channels + 1u, MINIPSD_PLANES); + + if (minipsd_layer_name(&psd->view, NULL, 0, &needed) == MINIPSD_MORE + && (name = (char *)vips_malloc(VIPS_OBJECT(psd), needed)) + && minipsd_layer_name(&psd->view, name, needed, &needed) == MINIPSD_OK) + vips_image_set_string(out, "psd-layer-name", name); + vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); + vips_image_set_int(out, "psd-layer-opacity", psd->view.opacity); + vips_image_set_int(out, "psd-layer-hidden", psd->view.hidden); + return 0; +} + static int vips_foreign_load_psd_header(VipsForeignLoad *load) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; - VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(load); + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; minipsd_resource res; minipsd_resolution_info resolution; minipsd_version_info version; - double xres = psd->info.dpi, yres; + uint16_t index; + double xres, yres; const char *unit = "in"; const void *data; void *workspace; @@ -159,26 +232,46 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) return -1; rc = minipsd_reader_init(&psd->reader, data, size, workspace, workspace_size, NULL, &psd->info); if (rc != MINIPSD_OK) { - vips_error(class->nickname, "%s", minipsd_error_string(rc)); + psd_error(psd, minipsd_error_string(rc)); return -1; } + psd->width = psd->info.width; + psd->height = psd->info.height; + /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ + psd->planes = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); + if (psd->layer >= 0 && psd_find_layer(psd)) return -1; + psd->scratch_size = psd->layer >= 0 ? psd_scratch_size(psd) : 0; + + /* Indexed: the palette's RGB, and transparency where resource 1047 names an index. */ + psd->bands = psd->planes; + psd->transparent = -1; + psd->indexed = psd->info.color_mode == MINIPSD_MODE_INDEXED; + if (psd->indexed) { + if (psd->info.color_mode_data.size < PSD_RGB * PSD_PALETTE_ENTRIES || !psd->info.color_mode_data.data) { + psd_error(psd, "an indexed file without its palette"); + return -1; + } + if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_TRANSPARENCY_INDEX, &res) == MINIPSD_OK + && minipsd_resource_transparency_index(&res, &index) == MINIPSD_OK && index < PSD_PALETTE_ENTRIES) + psd->transparent = index; + psd->bands = PSD_RGB + (psd->transparent >= 0 ? 1u : 0u); + } + /* The resolution: resource 1005 whole, or none (libvips' default). */ - yres = xres; + xres = yres = psd->info.dpi; if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_RESOLUTION_INFO, &res) == MINIPSD_OK && minipsd_resource_resolution_info(&res, &resolution) == MINIPSD_OK) { xres = resolution.horizontal_resolution; yres = resolution.vertical_resolution; if (resolution.horizontal_resolution_unit == PSD_UNIT_CM) unit = "cm"; } - - /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ - psd->bands = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); - psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; - psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; + psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; + psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; if (psd_fields(psd, load->out)) return -1; if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); + vips_image_set_int(load->out, VIPS_META_BITS_PER_SAMPLE, psd->info.depth); if (psd->info.icc.size) vips_image_set_blob_copy(load->out, VIPS_META_ICC_NAME, psd->info.icc.data, psd->info.icc.size); psd_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); @@ -188,6 +281,7 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) vips_image_set_blob_copy(load->out, "psd-color-mode-data", psd->info.color_mode_data.data, psd->info.color_mode_data.size); vips_image_set_double(load->out, "psd-pixel-aspect-ratio", psd->info.pixel_aspect_ratio); + vips_image_set_int(load->out, "psd-layer-count", (int)psd->info.layer_count); /* Without version info (before Photoshop 6) the composite is real. */ vips_image_set_int(load->out, "psd-has-real-merged-data", minipsd_reader_find_resource(psd->reader, MINIPSD_RES_VERSION_INFO, &res) != MINIPSD_OK @@ -196,39 +290,106 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) return 0; } +/* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ +static int psd_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) +{ + uLongf n = (uLongf)dst_size; + (void)user; + if ((uLong)src_size != src_size || n != dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ + return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; +} + +/* A thread's sequence: the scratch a ZIP layer's channels stay inflated in, between its bands. */ +typedef struct psd_sequence { + uint8_t *scratch; + int kept; +} psd_sequence; + +static void *psd_start(VipsImage *out, void *a, void *b) +{ + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; + psd_sequence *seq = g_new0(psd_sequence, 1); + (void)out, (void)b; + /* ponytail: every thread inflates a ZIP layer whole into its own scratch; one shared scratch + under a lock would bound memory at one copy, if layers that large turn up. */ + if (psd->scratch_size && !(seq->scratch = (uint8_t *)g_try_malloc(psd->scratch_size))) { + g_free(seq); + return NULL; + } + return seq; +} + +static int psd_stop(void *seq, void *a, void *b) +{ + (void)a, (void)b; + g_free(((psd_sequence *)seq)->scratch); + g_free(seq); + return 0; +} + +/* Indexed, in place: each pixel's index (in its first band) becomes the palette's RGB, and alpha + 0 at the transparent index. */ +static void psd_expand_palette(const VipsForeignLoadPsd *psd, VipsPel *row, int width) +{ + const uint8_t *palette = psd->info.color_mode_data.data; + int x; + for (x = 0; x < width; ++x, row += psd->bands) { + const unsigned index = row[0]; + row[0] = palette[index]; + row[1] = palette[PSD_PALETTE_ENTRIES + index]; + row[2] = palette[2 * PSD_PALETTE_ENTRIES + index]; + if (psd->transparent >= 0) row[PSD_RGB] = (int)index == psd->transparent ? 0 : UCHAR_MAX; + } +} + /* One band of rows, decoded straight into the region. vips_sequential asks for whole rows; calls run at once from libvips' threads on the one reader, which minipsd allows on a whole file. */ static int psd_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; - const VipsRect *rect = ®ion->valid; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; + psd_sequence *sequence = (psd_sequence *)seq; + const VipsRect *rect = ®ion->valid; minipsd_decode_opts opts; minipsd_target target; VipsPel *top = VIPS_REGION_ADDR_TOPLEFT(region), *row; const size_t skip = VIPS_REGION_LSKIP(region); int rc, x, y; - (void)seq, (void)b, (void)stop; + (void)b, (void)stop; if (rect->left != 0 || rect->width != region->im->Xsize) { - vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", "only whole rows are decoded"); + psd_error(psd, "only whole rows are decoded"); return -1; } memset(&opts, 0, sizeof opts); opts.y_first = (uint32_t)rect->top; opts.y_count = (uint32_t)rect->height; opts.depth = psd->info.depth; - minipsd_target_interleaved(&target, top, skip, psd->bands, psd->info.depth); - rc = minipsd_decode_composite(psd->reader, &target, &opts); + minipsd_target_interleaved(&target, top, skip, psd->planes, psd->info.depth); + if (psd->indexed) target.channel[0].pixel_step = (ptrdiff_t)psd->bands; /* the index, in R */ + if (psd->layer < 0) { + rc = minipsd_decode_composite(psd->reader, &target, &opts); + } else { + opts.inflate = psd_inflate; + opts.scratch = sequence->scratch; + opts.scratch_size = psd->scratch_size; + opts.scratch_kept = sequence->kept; + rc = minipsd_decode_layer(psd->reader, &psd->view, &target, &opts); + sequence->kept = rc == MINIPSD_OK && sequence->scratch; + } if (rc != MINIPSD_OK) { - vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", minipsd_error_string(rc)); + psd_error(psd, minipsd_error_string(rc)); return -1; } - /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ - if (psd->info.color_mode == MINIPSD_MODE_BITMAP) - for (y = 0; y < rect->height; ++y) - for (row = top + (size_t)y * skip, x = 0; x < rect->width; ++x) - row[x] = row[x] ? 0 : UCHAR_MAX; + for (y = 0; y < rect->height; ++y) { + row = top + (size_t)y * skip; + if (psd->indexed) { + psd_expand_palette(psd, row, rect->width); + } else if (psd->info.color_mode == MINIPSD_MODE_BITMAP) { + /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ + for (x = 0; x < rect->width; ++x) row[x] = row[x] ? 0 : UCHAR_MAX; + } + } return 0; } @@ -237,7 +398,7 @@ static int vips_foreign_load_psd_load(VipsForeignLoad *load) VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(load), 2); t[0] = vips_image_new(); - if (psd_fields(psd, t[0]) || vips_image_generate(t[0], NULL, psd_generate, NULL, psd, NULL) + if (psd_fields(psd, t[0]) || vips_image_generate(t[0], psd_start, psd_generate, psd_stop, psd, NULL) || vips_sequential(t[0], &t[1], NULL) || vips_image_write(t[1], load->real)) return -1; return 0; @@ -258,11 +419,17 @@ static void vips_foreign_load_psd_class_init(VipsForeignLoadPsdClass *class) load_class->get_flags = vips_foreign_load_psd_get_flags; load_class->header = vips_foreign_load_psd_header; load_class->load = vips_foreign_load_psd_load; + + PSD_ARGS_BEGIN + VIPS_ARG_INT(class, "layer", 20, "Layer", "Load this layer (bottom first), -1 the composite", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignLoadPsd, layer), PSD_COMPOSITE, INT_MAX, + PSD_COMPOSITE); + PSD_ARGS_END } static void vips_foreign_load_psd_init(VipsForeignLoadPsd *psd) { - (void)psd; + psd->layer = PSD_COMPOSITE; } static const char *psd_suffs[] = { ".psd", ".psb", NULL }; diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index 3a0af56..e9d633b 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -4,10 +4,11 @@ * * Usage: minipsd-vips-test out-dir [list]. Each document is written into out-dir, then loaded as a * file (randomly and sequentially), from a buffer and from a source; the image libvips gives must - * say what the document does, and its pixels be what minipsd decodes. list names other files, one - * path a line, each loaded as a file and compared with minipsd's decode the same way (or refused by - * both). libvips runs on several threads, so bands are decoded at once. Exits with 1 when - * anything fails. + * say what the document does, and its pixels be what minipsd decodes. Its layers, loaded with + * `layer`, must be the pixels the document gave, at their place. list names other files, one path + * a line, whose composite and every layer are loaded and compared with minipsd's decode the same + * way (or refused by both). libvips runs on several threads, so bands are decoded at once. Exits + * with 1 when anything fails. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha @@ -19,6 +20,7 @@ #include "minipsd_vips.h" #include +#include #include #include @@ -26,17 +28,29 @@ #include #include -#define WIDTH 301 /* odd sizes, and rows enough for several bands */ -#define HEIGHT 203 -#define PIXELS (WIDTH * HEIGHT) -#define MAX_PLANES 5 /* CMYK and transparency */ -#define DPI 300.0 -#define ASPECT 0.9 -#define MM_PER_INCH 25.4 -#define EPSILON 1e-9 -#define PATH_MAX_SIZE 1024 +#define WIDTH 301 /* odd sizes, and rows enough for several bands */ +#define HEIGHT 203 +#define PIXELS (WIDTH * HEIGHT) +#define RGBA 4 +#define MAX_PLANES 5 /* CMYK and transparency */ +#define DPI 300.0 +#define ASPECT 0.9 +#define MM_PER_INCH 25.4 +#define EPSILON 1e-9 +#define PATH_MAX_SIZE 1024 +#define NAME_SIZE (PATH_MAX_SIZE + 64) /* a path and what it names in it */ #define WORKSPACE_SIZE 256 -#define THREADS 4 +#define THREADS 4 +#define PALETTE 256 /* entries; the palette is 256 reds, 256 greens, 256 blues */ +#define TRANSPARENT 5 /* the indexed document's transparent index */ +#define HALF_OPAQUE 128 +#define ZIP_LEVEL 6 +#define COMPOSITE (-1) /* decode_image's layer: the composite */ + +/* The ZIP document's two layers: one reaching past the canvas's left edge, one inside. */ +#define ZIP_W 200 +#define ZIP_H 100 +#define ZIP_PIXELS (ZIP_W * ZIP_H) static int failures; @@ -69,58 +83,116 @@ static int to_buffer(void *user, const void *data, size_t size) return 1; } -typedef struct expected { - const char *name; - int bands; - VipsBandFormat format; - VipsInterpretation interpretation; -} expected; +static int deflate_zlib(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) +{ + uLongf n = (uLongf)*dst_size; + int rc; + (void)user; + rc = compress2(dst, &n, src, (uLong)src_size, ZIP_LEVEL); + *dst_size = n; + return rc != Z_OK; +} -/* The composite as minipsd decodes it, laid out as the loader gives it: interleaved, the colour - channels and transparency up to MINIPSD_PLANES, bitmap's black 0 and white 255. NULL with the - error in *rc when minipsd refuses the file. */ -static uint8_t *decode_composite(const void *file, size_t size, size_t *bytes, int *rc) +static int inflate_zlib(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) { - uint8_t workspace[WORKSPACE_SIZE], *pixels; + uLongf n = (uLongf)dst_size; + (void)user; + return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; +} + +/* The composite (layer -1) or a layer as minipsd decodes it, laid out as the loader gives it: + interleaved, the colour channels and transparency (a layer's always) up to MINIPSD_PLANES, + bitmap's black 0 and white 255, indexed through its palette. NULL with the error in *rc when + minipsd refuses it, or MINIPSD_ERR_SIZE for an empty layer, which the loader refuses too. */ +static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *bytes, int *rc) +{ + uint8_t workspace[WORKSPACE_SIZE], *pixels, *scratch = NULL, *p; minipsd_reader *r; minipsd_info info; + minipsd_layer_view view; + minipsd_resource res; minipsd_target target; minipsd_decode_opts opts; size_t need, i, row; - unsigned bands; + uint32_t width, height; + unsigned planes, bands; + uint16_t index; + int transparent = -1, n; if ((*rc = minipsd_reader_workspace_size(&need)) != MINIPSD_OK) return NULL; if (need > sizeof workspace) { *rc = MINIPSD_ERR_ARG; return NULL; } + memset(&view, 0, sizeof view); if ((*rc = minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info)) != MINIPSD_OK) return NULL; - bands = info.color_channels + (info.composite_has_alpha ? 1u : 0u); - if (bands > MINIPSD_PLANES) bands = MINIPSD_PLANES; - row = (size_t)info.width * bands * (info.depth == 32 ? sizeof(float) : info.depth == 16 ? sizeof(uint16_t) : 1); - *bytes = row * info.height; + width = info.width; + height = info.height; + planes = info.color_channels + (info.composite_has_alpha ? 1u : 0u); + if (layer >= 0) { + for (n = 0; n <= layer; ++n) + if ((*rc = minipsd_reader_next_layer(r, &view)) != MINIPSD_MORE) { + if (*rc == MINIPSD_OK) *rc = MINIPSD_ERR_NOT_FOUND; + return NULL; + } + if (view.right <= view.left || view.bottom <= view.top) { + *rc = MINIPSD_ERR_SIZE; + return NULL; + } + width = (uint32_t)(view.right - view.left); + height = (uint32_t)(view.bottom - view.top); + planes = info.color_channels + 1u; + } + if (planes > MINIPSD_PLANES) planes = MINIPSD_PLANES; + bands = planes; + if (info.color_mode == MINIPSD_MODE_INDEXED) { + if (minipsd_reader_find_resource(r, MINIPSD_RES_TRANSPARENCY_INDEX, &res) == MINIPSD_OK + && minipsd_resource_transparency_index(&res, &index) == MINIPSD_OK && index < PALETTE) + transparent = index; + bands = 3 + (transparent >= 0 ? 1u : 0u); + } + row = (size_t)width * bands * (info.depth == 32 ? sizeof(float) : info.depth == 16 ? sizeof(uint16_t) : 1); + *bytes = row * height; if (!(pixels = (uint8_t *)malloc(*bytes ? *bytes : 1))) { *rc = MINIPSD_ERR_ALLOC; return NULL; } - minipsd_target_interleaved(&target, pixels, row, bands, info.depth); + minipsd_target_interleaved(&target, pixels, row, planes, info.depth); + if (info.color_mode == MINIPSD_MODE_INDEXED) target.channel[0].pixel_step = (ptrdiff_t)bands; memset(&opts, 0, sizeof opts); opts.depth = info.depth; - if ((*rc = minipsd_decode_composite(r, &target, &opts)) != MINIPSD_OK) { + if (layer < 0) { + *rc = minipsd_decode_composite(r, &target, &opts); + } else { + opts.inflate = inflate_zlib; + opts.scratch_size = (size_t)height * (((size_t)width * info.depth + 7) / 8); + opts.scratch = scratch = (uint8_t *)malloc(opts.scratch_size ? opts.scratch_size : 1); + *rc = scratch ? minipsd_decode_layer(r, &view, &target, &opts) : MINIPSD_ERR_ALLOC; + free(scratch); + } + if (*rc != MINIPSD_OK) { free(pixels); return NULL; } if (info.color_mode == MINIPSD_MODE_BITMAP) for (i = 0; i < *bytes; ++i) pixels[i] = pixels[i] ? 0 : UCHAR_MAX; + if (info.color_mode == MINIPSD_MODE_INDEXED) + for (p = pixels, i = 0; i < (size_t)width * height; ++i, p += bands) { + const unsigned k = p[0]; + p[0] = info.color_mode_data.data[k]; + p[1] = info.color_mode_data.data[PALETTE + k]; + p[2] = info.color_mode_data.data[2 * PALETTE + k]; + if (transparent >= 0) p[3] = (int)k == transparent ? 0 : UCHAR_MAX; + } return pixels; } /* The loaded image's pixels against minipsd's own decode of the file: the same bytes, or both refuse it. Takes the image. Returns 1 when both decoded, 0 when both refused. */ -static int check_pixels(VipsImage *image, const void *file, size_t size, const char *name) +static int check_pixels(VipsImage *image, const void *file, size_t size, int layer, const char *name) { size_t bytes = 0, got_bytes = 0; int rc, both = 0; - uint8_t *want = decode_composite(file, size, &bytes, &rc); + uint8_t *want = decode_image(file, size, layer, &bytes, &rc); void *got = image ? vips_image_write_to_memory(image, &got_bytes) : NULL; if (want) { both = 1; @@ -136,14 +208,23 @@ static int check_pixels(VipsImage *image, const void *file, size_t size, const c return both; } +typedef struct expected { + const char *name; + int bands; + VipsBandFormat format; + VipsInterpretation interpretation; + int bits; +} expected; + /* What libvips says of one loaded image, against the document's; then its pixels. */ static void check_image(VipsImage *image, const minipsd_doc *doc, const expected *e, const char *how, const out_buffer *file) { - char name[PATH_MAX_SIZE]; - const void *icc; - size_t icc_size; - double ratio = 0.0; + char name[PATH_MAX_SIZE]; + const void *icc; + size_t icc_size; + double ratio = 0.0; + int bits = 0, layers = -1; snprintf(name, sizeof name, "%s (%s)", e->name, how); if (!image) { printf("FAIL %s: not loaded: %s\n", name, vips_error_buffer()); @@ -156,6 +237,9 @@ static void check_image(VipsImage *image, const minipsd_doc *doc, const expected check(vips_image_get_bands(image) == e->bands, "bands", name); check(vips_image_get_format(image) == e->format, "format", name); check(vips_image_get_interpretation(image) == e->interpretation, "interpretation", name); + check(vips_image_get_int(image, VIPS_META_BITS_PER_SAMPLE, &bits) == 0 && bits == e->bits, "bits per sample", name); + check(vips_image_get_int(image, "psd-layer-count", &layers) == 0 && layers == (int)doc->layer_count, + "layer count", name); check(fabs(vips_image_get_xres(image) - doc->dpi / MM_PER_INCH) < EPSILON && fabs(vips_image_get_yres(image) - doc->dpi / MM_PER_INCH) < EPSILON, "resolution", name); @@ -166,25 +250,68 @@ static void check_image(VipsImage *image, const minipsd_doc *doc, const expected check(vips_image_get_double(image, "psd-pixel-aspect-ratio", &ratio) == 0 && fabs(ratio - (doc->pixel_aspect_ratio > 0.0 ? doc->pixel_aspect_ratio : 1.0)) < EPSILON, "pixel aspect ratio", name); - check(check_pixels(image, file->data, file->size, name), "decoded", name); + check(check_pixels(image, file->data, file->size, COMPOSITE, name), "decoded", name); } -/* Writes doc into dir, then loads it the three ways. */ -static void check_document(const minipsd_doc *doc, const expected *e, const char *dir) +/* Each layer of the document, loaded from path with `layer`: its place, its look, and its pixels + byte for byte as the document gave them (layers are not matted). sample is a sample's bytes. */ +static void check_layers(const char *path, const minipsd_doc *doc, size_t sample, const char *doc_name) +{ + char name[PATH_MAX_SIZE]; + const char *got_name; + VipsImage *image; + void *got; + size_t i, bytes, want_bytes; + int opacity = -1, hidden = -1; + for (i = 0; i < doc->layer_count; ++i) { + const minipsd_layer *l = &doc->layers[i]; + const int full = !l->top && !l->left && !l->bottom && !l->right; + const int w = full ? (int)doc->width : l->right - l->left, h = full ? (int)doc->height : l->bottom - l->top; + snprintf(name, sizeof name, "%s layer %zu", doc_name, i); + if (!(image = vips_image_new_from_file(path, "layer", (int)i, NULL))) { + printf("FAIL %s: not loaded: %s\n", name, vips_error_buffer()); + vips_error_clear(); + ++failures; + continue; + } + check(vips_image_get_width(image) == w && vips_image_get_height(image) == h, "size", name); + check(vips_image_get_xoffset(image) == l->left && vips_image_get_yoffset(image) == l->top, "offset", name); + check(vips_image_get_string(image, "psd-layer-name", &got_name) == 0 && strcmp(got_name, l->name) == 0, "name", name); + check(vips_image_get_int(image, "psd-layer-opacity", &opacity) == 0 && opacity == l->opacity, "opacity", name); + check(vips_image_get_int(image, "psd-layer-hidden", &hidden) == 0 && !hidden == !l->hidden, "hidden", name); + want_bytes = (size_t)w * (size_t)h * sample + * (doc->color_mode == MINIPSD_MODE_CMYK ? MAX_PLANES : doc->color_mode == MINIPSD_MODE_GREY ? 2u : RGBA); + got = vips_image_write_to_memory(image, &bytes); + check(got && bytes == want_bytes && memcmp(got, l->pixels, bytes) == 0, "pixels as given", name); + vips_error_clear(); + g_free(got); + g_object_unref(image); + } + /* One past the last: no such layer. */ + image = vips_image_new_from_file(path, "layer", (int)doc->layer_count, NULL); + got = image ? vips_image_write_to_memory(image, &bytes) : NULL; + check(!got, "no layer past the last", doc_name); + vips_error_clear(); + g_free(got); + if (image) g_object_unref(image); +} + +/* Writes doc into dir, then loads it the four ways; path receives the file's, for more checks. */ +static void check_document(const minipsd_doc *doc, const minipsd_write_opts *opts, const expected *e, const char *dir, + char *path, size_t path_size) { out_buffer b = { NULL, 0, 0 }; - char path[PATH_MAX_SIZE]; FILE *f; const char *loader; VipsSource *source; - int rc = minipsd_write(doc, NULL, to_buffer, &b); + int rc = minipsd_write(doc, opts, to_buffer, &b); if (rc != MINIPSD_OK) { printf("FAIL %s: not written: %s\n", e->name, minipsd_error_string(rc)); ++failures; free(b.data); return; } - snprintf(path, sizeof path, "%s/%s.%s", dir, e->name, doc->format == MINIPSD_PSB ? "psb" : "psd"); + snprintf(path, path_size, "%s/%s.%s", dir, e->name, doc->format == MINIPSD_PSB ? "psb" : "psd"); f = fopen(path, "wb"); check(f && fwrite(b.data, 1, b.size, f) == b.size, "file written", e->name); if (f) fclose(f); @@ -201,6 +328,26 @@ static void check_document(const minipsd_doc *doc, const expected *e, const char free(b.data); } +/* An indexed document's image against its palette, worked out here: each pixel the palette's RGB, + alpha 0 at the transparent index when there is one. */ +static void check_indexed(const char *path, const uint8_t *indices, const uint8_t *palette, int transparent) +{ + const unsigned bands = transparent >= 0 ? RGBA : 3; + VipsImage *image = vips_image_new_from_file(path, NULL); + size_t bytes = 0, i; + uint8_t *got = image ? (uint8_t *)vips_image_write_to_memory(image, &bytes) : NULL; + int same = got && bytes == (size_t)PIXELS * bands; + for (i = 0; same && i < PIXELS; ++i) { + const uint8_t *p = got + i * bands; + same = p[0] == palette[indices[i]] && p[1] == palette[PALETTE + indices[i]] && p[2] == palette[2 * PALETTE + indices[i]] + && (transparent < 0 || p[3] == (indices[i] == transparent ? 0 : UCHAR_MAX)); + } + check(same, "the palette's colours", path); + vips_error_clear(); + g_free(got); + if (image) g_object_unref(image); +} + /* The whole file in a new buffer; NULL when it cannot be read. */ static uint8_t *read_all(const char *path, size_t *size) { @@ -220,11 +367,21 @@ static uint8_t *read_all(const char *path, size_t *size) return data; } -/* Each file the list names, loaded through libvips and compared with minipsd's decode. */ +/* The file's layer count, 0 when minipsd does not read it. */ +static size_t layer_count(const void *file, size_t size) +{ + uint8_t workspace[WORKSPACE_SIZE]; + minipsd_reader *r; + minipsd_info info; + return minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK ? info.layer_count : 0; +} + +/* Each file the list names, its composite and every layer, loaded through libvips and compared + with minipsd's decode. */ static void check_list(const char *list) { - char path[PATH_MAX_SIZE]; - size_t size, length, files = 0, decoded = 0; + char path[PATH_MAX_SIZE], name[NAME_SIZE]; + size_t size, length, files = 0, decoded = 0, layers = 0, layers_decoded = 0, i, count; uint8_t *data; FILE *f = fopen(list, "r"); if (!f) { @@ -232,7 +389,7 @@ static void check_list(const char *list) return; } while (fgets(path, sizeof path, f)) { - length = strcspn(path, "\r\n"); + length = strcspn(path, "\r\n"); path[length] = 0; if (!length) continue; if (!(data = read_all(path, &size))) { @@ -240,21 +397,32 @@ static void check_list(const char *list) continue; } ++files; - decoded += (size_t)check_pixels(vips_image_new_from_file(path, NULL), data, size, path); + decoded += (size_t)check_pixels(vips_image_new_from_file(path, NULL), data, size, COMPOSITE, path); + for (count = layer_count(data, size), i = 0; i < count; ++i, ++layers) { + snprintf(name, sizeof name, "%s layer %zu", path, i); + layers_decoded += (size_t)check_pixels(vips_image_new_from_file(path, "layer", (int)i, NULL), data, size, + (int)i, name); + } free(data); } fclose(f); - printf("%zu files: %zu decoded as minipsd decodes them, %zu refused by both\n", files, decoded, files - decoded); + printf("%zu files: %zu composites and %zu of %zu layers decoded as minipsd decodes them, the rest refused by both\n", + files, decoded, layers_decoded, layers); } int main(int argc, char **argv) { static const uint8_t icc[] = "not a real ICC profile"; - static uint8_t rgba[PIXELS * 4], cmyka[PIXELS * MAX_PLANES], bits[PIXELS]; - static uint16_t grey16[PIXELS * 2], rgba16[PIXELS * 4]; + static uint8_t rgba[PIXELS * RGBA], cmyka[PIXELS * MAX_PLANES], bits[PIXELS], indices[PIXELS]; + static uint8_t palette[3 * PALETTE], zip_a[ZIP_PIXELS * RGBA], zip_b[ZIP_PIXELS * RGBA]; + static uint16_t grey16[PIXELS * 2], rgba16[PIXELS * RGBA]; static const char junk[] = "8BPX, not a PSD"; - minipsd_layer layer; + char path[PATH_MAX_SIZE]; + uint8_t transparency[MINIPSD_TRANSPARENCY_INDEX_SIZE]; + minipsd_layer layer, zip_layers[2]; minipsd_doc doc; + minipsd_write_opts opts; + minipsd_resource res; const char *loader; size_t i; @@ -270,63 +438,116 @@ int main(int argc, char **argv) for (i = 0; i < sizeof rgba; ++i) rgba[i] = (uint8_t)(i * 7); for (i = 0; i < sizeof cmyka; ++i) cmyka[i] = (uint8_t)(i * 5); for (i = 0; i < PIXELS * 2; ++i) grey16[i] = (uint16_t)(i * 4099); - for (i = 0; i < PIXELS * 4; ++i) rgba16[i] = (uint16_t)(i * 2731); + for (i = 0; i < PIXELS * RGBA; ++i) rgba16[i] = (uint16_t)(i * 2731); for (i = 0; i < PIXELS; ++i) bits[i] = (uint8_t)(i % 3 == 0); + for (i = 0; i < PIXELS; ++i) indices[i] = (uint8_t)(i * 13); + for (i = 0; i < sizeof palette; ++i) palette[i] = (uint8_t)(i * 11 + i / PALETTE * 85 + 3); /* R, G, B differ */ + for (i = 0; i < sizeof zip_a; ++i) zip_a[i] = (uint8_t)(i * 3); + for (i = 0; i < sizeof zip_b; ++i) zip_b[i] = (uint8_t)(i / 97); { /* 8-bit RGB with a layer, a profile and non-square pixels; as PSD and as PSB */ - static const expected psd = { "rgb8", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB }; - static const expected psb = { "rgb8-psb", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB }; + static const expected psd = { "rgb8", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; + static const expected psb = { "rgb8-psb", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; memset(&layer, 0, sizeof layer); layer.name = "Layer"; layer.pixels = rgba; - layer.opacity = 255; + layer.opacity = UCHAR_MAX; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; doc.layers = &layer, doc.layer_count = 1; doc.icc = icc, doc.icc_size = sizeof icc; doc.pixel_aspect_ratio = ASPECT; - check_document(&doc, &psd, argv[1]); + check_document(&doc, NULL, &psd, argv[1], path, sizeof path); + check_layers(path, &doc, 1, psd.name); doc.format = MINIPSD_PSB; - check_document(&doc, &psb, argv[1]); + check_document(&doc, NULL, &psb, argv[1], path, sizeof path); + check_layers(path, &doc, 1, psb.name); } { /* 16-bit RGB, its composite composed by the writer */ - static const expected e = { "rgb16", 4, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_RGB16 }; + static const expected e = { "rgb16", 4, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_RGB16, 16 }; layer.pixels = (const uint8_t *)rgba16; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; doc.layers = &layer, doc.layer_count = 1; doc.depth = 16; - check_document(&doc, &e, argv[1]); + check_document(&doc, NULL, &e, argv[1], path, sizeof path); + check_layers(path, &doc, sizeof(uint16_t), e.name); } /* Grey and CMYK: a layer and its composite (the writer composes RGB only); with a layer, the negative layer count says the composite's transparency is there. */ { /* 16-bit grey */ - static const expected e = { "grey16", 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16 }; + static const expected e = { "grey16", 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16, 16 }; layer.pixels = (const uint8_t *)grey16; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; doc.layers = &layer, doc.layer_count = 1; doc.color_mode = MINIPSD_MODE_GREY, doc.depth = 16; doc.composite = (const uint8_t *)grey16; - check_document(&doc, &e, argv[1]); + check_document(&doc, NULL, &e, argv[1], path, sizeof path); + check_layers(path, &doc, sizeof(uint16_t), e.name); } { /* 8-bit CMYK */ - static const expected e = { "cmyk8", 5, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_CMYK }; + static const expected e = { "cmyk8", 5, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_CMYK, 8 }; layer.pixels = cmyka; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; doc.layers = &layer, doc.layer_count = 1; doc.color_mode = MINIPSD_MODE_CMYK; doc.composite = cmyka; - check_document(&doc, &e, argv[1]); + check_document(&doc, NULL, &e, argv[1], path, sizeof path); + check_layers(path, &doc, 1, e.name); } - { /* bitmap: one band, no transparency */ - static const expected e = { "bitmap", 1, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_B_W }; + { /* bitmap: one band, no transparency, 1 bit a sample */ + static const expected e = { "bitmap", 1, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_B_W, 1 }; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; doc.color_mode = MINIPSD_MODE_BITMAP, doc.depth = 1; doc.composite = bits; - check_document(&doc, &e, argv[1]); + check_document(&doc, NULL, &e, argv[1], path, sizeof path); + check_layers(path, &doc, 1, e.name); + } + { /* indexed: through the palette, RGB, then RGBA with a transparent index (resource 1047) */ + static const expected rgb = { "indexed", 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; + static const expected alpha = { "indexed-1047", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.color_mode = MINIPSD_MODE_INDEXED; + doc.color_mode_data.data = palette, doc.color_mode_data.size = sizeof palette; + doc.composite = indices; + check_document(&doc, NULL, &rgb, argv[1], path, sizeof path); + check_indexed(path, indices, palette, -1); + check(minipsd_resource_make_transparency_index(TRANSPARENT, transparency, sizeof transparency, NULL) == MINIPSD_OK, + "transparency index made", alpha.name); + memset(&res, 0, sizeof res); + res.id = MINIPSD_RES_TRANSPARENCY_INDEX, res.data.data = transparency, res.data.size = sizeof transparency; + doc.resources = &res, doc.resource_count = 1; + check_document(&doc, NULL, &alpha, argv[1], path, sizeof path); + check_indexed(path, indices, palette, TRANSPARENT); + } + { /* ZIP layers, with and without prediction: one past the canvas's left edge, half opaque, + one hidden inside it */ + static const expected zip = { "zip", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; + static const expected predicted = { "zip-predicted", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; + memset(zip_layers, 0, sizeof zip_layers); + zip_layers[0].name = "Past the edge", zip_layers[0].pixels = zip_a, zip_layers[0].opacity = HALF_OPAQUE; + zip_layers[0].top = 10, zip_layers[0].left = -50; + zip_layers[1].name = "Hidden", zip_layers[1].pixels = zip_b, zip_layers[1].opacity = UCHAR_MAX, zip_layers[1].hidden = 1; + zip_layers[1].top = 60, zip_layers[1].left = 70; + for (i = 0; i < 2; ++i) { + zip_layers[i].bottom = zip_layers[i].top + ZIP_H; + zip_layers[i].right = zip_layers[i].left + ZIP_W; + } + memset(&doc, 0, sizeof doc); + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = zip_layers, doc.layer_count = 2; + doc.compression = MINIPSD_ZIP; + memset(&opts, 0, sizeof opts); + opts.deflate = deflate_zlib; + check_document(&doc, &opts, &zip, argv[1], path, sizeof path); + check_layers(path, &doc, 1, zip.name); + doc.compression = MINIPSD_ZIP_PREDICTED; + check_document(&doc, &opts, &predicted, argv[1], path, sizeof path); + check_layers(path, &doc, 1, predicted.name); } /* Not a PSD: psdload does not claim it. */ From 9778b38b762c08c65cf2ae8715b8dba0ecf188fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sat, 24 Oct 2026 23:38:12 +0200 Subject: [PATCH 04/22] feat(vips): psdsave, an image as a one-layer PSD (S7.4) psdsave, psdsave_buffer and psdsave_target write an image as one layer that is also the composite: grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats, alpha kept or made opaque; the resolution, ICC profile, EXIF, XMP, IPTC and pixel aspect ratio kept. Options: psb (also a file named .psb, read from the target libvips writes files through), zip and layer_name. The image is held whole while it is written; streaming is S7.5. VipsForeignSaveable (8.17) and VipsSaveable are both handled. vips.plugin saves each kind as a file, into a buffer and into a target, the same bytes each time, and reads it back through psdload: the layer as the image, the composite too where alpha is full (floats within 1e-6); ZIP, PSB, the layer name, the profile, and six bands refused. --- libvips-plugin/minipsd_vips.c | 334 +++++++++++++++++++++++++++++++++- libvips-plugin/minipsd_vips.h | 10 +- libvips-plugin/test_vips.c | 195 ++++++++++++++++++++ 3 files changed, 533 insertions(+), 6 deletions(-) diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index 5070e48..d42caa8 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -1,6 +1,6 @@ /** * \file - * \brief minipsd as a libvips loader: a PSD or PSB as a libvips image. + * \brief minipsd as a libvips loader and saver: a PSD or PSB as a libvips image, and back. * * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer * variants make one), so the file can move into libvips/foreign/ as it is. The file is mapped @@ -15,6 +15,10 @@ * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). ZIP layers * are inflated by the zlib libvips links. * + * The saver writes an image as a one-layer PSD (or PSB) with that layer as its composite: grey, RGB + * or CMYK, 8 or 16 bits, grey and RGB also 32, transparency kept or made opaque; the resolution, the + * ICC profile, EXIF, XMP and IPTC kept. The image is held whole in memory while it is written. + * * minipsd is compiled in here, private (MINIPSD_STATIC), so a program that uses minipsd itself * keeps its own. * @@ -582,13 +586,339 @@ static void vips_foreign_load_psd_buffer_init(VipsForeignLoadPsdBuffer *buffer) (void)buffer; } +/* --- The saver's base class: an image as a one-layer PSD into a target --------------------- */ + +#define PSD_ZIP_LEVEL 6 /* zlib's default */ +#define PSD_DEFAULT_DPI 72.0 /* when the image's resolution is none PSD can hold */ +#define PSD_MAX_DPI 65536.0 /* PSD's resolution is a 16.16 fixed-point number */ +#define PSD_LAYER_NAME "Layer 1" +#define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ + +typedef struct _VipsForeignSavePsd { + VipsForeignSave parent_object; + VipsTarget *target; /* set by the subclass's build */ + gboolean psb; /* the option; also a .psb file, or a side past PSD's */ + gboolean zip; /* the option: ZIP the layer's channels */ + char *layer_name; /* the option */ +} VipsForeignSavePsd; + +typedef VipsForeignSaveClass VipsForeignSavePsdClass; + +GType vips_foreign_save_psd_get_type(void); +G_DEFINE_ABSTRACT_TYPE(VipsForeignSavePsd, vips_foreign_save_psd, VIPS_TYPE_FOREIGN_SAVE) + +/* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ +static VipsBandFormat psd_save_formats[] = { + /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ + VIPS_FORMAT_UCHAR, VIPS_FORMAT_UCHAR, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, + VIPS_FORMAT_USHORT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT +}; + +static void vips_foreign_save_psd_dispose(GObject *gobject) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)gobject; + VIPS_UNREF(psd->target); + G_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->dispose(gobject); +} + +static int psd_to_target(void *user, const void *data, size_t size) +{ + return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ +} + +static int psd_deflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) +{ + uLongf n = (uLongf)*dst_size; + int rc; + (void)user; + if ((uLong)src_size != src_size || n != *dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ + rc = compress2(dst, &n, src, (uLong)src_size, PSD_ZIP_LEVEL); + *dst_size = n; + return rc != Z_OK; +} + +/* A blob of the image as a resource to keep, when it has one. */ +static void psd_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, size_t *count) +{ + const void *data; + size_t size; + if (!vips_image_get_typeof(image, name) || vips_image_get_blob(image, name, &data, &size) || !size) return; + memset(&res[*count], 0, sizeof res[*count]); + res[*count].id = id; + res[*count].data.data = (const uint8_t *)data; + res[*count].data.size = size; + ++*count; +} + +static int vips_foreign_save_psd_build(VipsObject *object) +{ + VipsForeignSave *save = (VipsForeignSave *)object; + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsImage **t = (VipsImage **)vips_object_local_array(object, 1); + VipsImage *in; + minipsd_resource resources[PSD_MAX_RESOURCES]; + minipsd_layer layer; + minipsd_doc doc; + minipsd_write_opts opts; + const void *icc; + const char *filename; + size_t icc_size, bytes, colors, count = 0; + double ratio, full; + void *pixels; + int rc; + + if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->build(object)) return -1; + in = save->ready; + + /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ + memset(&doc, 0, sizeof doc); + if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == 4 || in->Bands == 5)) { + doc.color_mode = MINIPSD_MODE_CMYK; + colors = 4; + } else if (in->Bands == 3 || in->Bands == 4) { + doc.color_mode = MINIPSD_MODE_RGB; + colors = PSD_RGB; + } else if (in->Bands == 1 || in->Bands == 2) { + doc.color_mode = MINIPSD_MODE_GREY; + colors = 1; + } else { + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); + return -1; + } + doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; + + /* A layer always has transparency: opaque, when the image has none. */ + if ((size_t)in->Bands == colors) { + full = doc.depth == 32 ? 1.0 : doc.depth == 16 ? USHRT_MAX : UCHAR_MAX; + if (vips_bandjoin_const1(in, &t[0], full, NULL)) return -1; + in = t[0]; + } + if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; + + memset(&layer, 0, sizeof layer); + layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; + layer.pixels = (const uint8_t *)pixels; + layer.opacity = UCHAR_MAX; + + doc.width = (uint32_t)in->Xsize; + doc.height = (uint32_t)in->Ysize; + doc.dpi = in->Xres * MM_PER_INCH; + if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; + doc.layers = &layer; + doc.layer_count = 1; + doc.composite = (const uint8_t *)pixels; /* one layer, at full opacity: the composite itself */ + doc.compression = psd->zip ? MINIPSD_ZIP : MINIPSD_RLE; + /* PSB by the option, a file named .psb (libvips writes files through a target too), or a side + past PSD's. */ + filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); + doc.format = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE + || in->Ysize > MINIPSD_MAX_SIDE + ? MINIPSD_PSB + : MINIPSD_PSD; + if (vips_image_get_typeof(in, VIPS_META_ICC_NAME) && !vips_image_get_blob(in, VIPS_META_ICC_NAME, &icc, &icc_size)) { + doc.icc = (const uint8_t *)icc; + doc.icc_size = icc_size; + } + if (vips_image_get_typeof(in, "psd-pixel-aspect-ratio") && !vips_image_get_double(in, "psd-pixel-aspect-ratio", &ratio) + && ratio > 0.0) + doc.pixel_aspect_ratio = ratio; + psd_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); + psd_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); + psd_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); + doc.resources = resources; + doc.resource_count = count; + + memset(&opts, 0, sizeof opts); + opts.deflate = psd_deflate; + rc = minipsd_write(&doc, &opts, psd_to_target, psd->target); + g_free(pixels); + if (rc != MINIPSD_OK) { + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); + return -1; + } + return vips_target_end(psd->target); +} + +static void vips_foreign_save_psd_class_init(VipsForeignSavePsdClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignSaveClass *save_class = (VipsForeignSaveClass *)class; + + gobject_class->dispose = vips_foreign_save_psd_dispose; + psd_class_properties(class); + + object_class->nickname = "psdsave_base"; + object_class->description = "save PSD and PSB with minipsd"; + object_class->build = vips_foreign_save_psd_build; + + /* Any bands: the build takes grey, RGB and CMYK, with or without alpha, and refuses the rest. */ +#if VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 17) + save_class->saveable = VIPS_FOREIGN_SAVEABLE_ANY; +#else + save_class->saveable = VIPS_SAVEABLE_ANY; +#endif + save_class->format_table = psd_save_formats; + + PSD_ARGS_BEGIN + VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, psb), FALSE); + VIPS_ARG_BOOL(class, "zip", 21, "ZIP", "Compress the layer with ZIP instead of RLE", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, zip), FALSE); + VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, layer_name), PSD_LAYER_NAME); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_init(VipsForeignSavePsd *psd) +{ + (void)psd; +} + +/* --- psdsave_target ------------------------------------------------------------------------ */ + +typedef struct _VipsForeignSavePsdTarget { + VipsForeignSavePsd parent_object; + VipsTarget *target; +} VipsForeignSavePsdTarget; + +typedef VipsForeignSavePsdClass VipsForeignSavePsdTargetClass; + +GType vips_foreign_save_psd_target_get_type(void); +G_DEFINE_TYPE(VipsForeignSavePsdTarget, vips_foreign_save_psd_target, vips_foreign_save_psd_get_type()) + +static int vips_foreign_save_psd_target_build(VipsObject *object) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsForeignSavePsdTarget *target = (VipsForeignSavePsdTarget *)object; + if (target->target) { + psd->target = target->target; + g_object_ref(psd->target); + } + return VIPS_OBJECT_CLASS(vips_foreign_save_psd_target_parent_class)->build(object); +} + +static void vips_foreign_save_psd_target_class_init(VipsForeignSavePsdTargetClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + psd_class_properties(class); + object_class->nickname = "psdsave_target"; + object_class->description = "save PSD and PSB to a target with minipsd"; + object_class->build = vips_foreign_save_psd_target_build; + foreign_class->suffs = psd_suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdTarget, target), VIPS_TYPE_TARGET); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_target_init(VipsForeignSavePsdTarget *target) +{ + (void)target; +} + +/* --- psdsave (a file) ---------------------------------------------------------------------- */ + +typedef struct _VipsForeignSavePsdFile { + VipsForeignSavePsd parent_object; + char *filename; +} VipsForeignSavePsdFile; + +typedef VipsForeignSavePsdClass VipsForeignSavePsdFileClass; + +GType vips_foreign_save_psd_file_get_type(void); +G_DEFINE_TYPE(VipsForeignSavePsdFile, vips_foreign_save_psd_file, vips_foreign_save_psd_get_type()) + +static int vips_foreign_save_psd_file_build(VipsObject *object) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsForeignSavePsdFile *file = (VipsForeignSavePsdFile *)object; + if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; + return VIPS_OBJECT_CLASS(vips_foreign_save_psd_file_parent_class)->build(object); +} + +static void vips_foreign_save_psd_file_class_init(VipsForeignSavePsdFileClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + psd_class_properties(class); + object_class->nickname = "psdsave"; + object_class->description = "save PSD and PSB to a file with minipsd"; + object_class->build = vips_foreign_save_psd_file_build; + foreign_class->suffs = psd_suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdFile, filename), NULL); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_file_init(VipsForeignSavePsdFile *file) +{ + (void)file; +} + +/* --- psdsave_buffer ------------------------------------------------------------------------ */ + +typedef struct _VipsForeignSavePsdBuffer { + VipsForeignSavePsd parent_object; + VipsArea *buf; +} VipsForeignSavePsdBuffer; + +typedef VipsForeignSavePsdClass VipsForeignSavePsdBufferClass; + +GType vips_foreign_save_psd_buffer_get_type(void); +G_DEFINE_TYPE(VipsForeignSavePsdBuffer, vips_foreign_save_psd_buffer, vips_foreign_save_psd_get_type()) + +static int vips_foreign_save_psd_buffer_build(VipsObject *object) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsBlob *blob; + if (!(psd->target = vips_target_new_to_memory())) return -1; + if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_buffer_parent_class)->build(object)) return -1; + g_object_get(psd->target, "blob", &blob, NULL); + g_object_set(object, "buffer", blob, NULL); + vips_area_unref(VIPS_AREA(blob)); + return 0; +} + +static void vips_foreign_save_psd_buffer_class_init(VipsForeignSavePsdBufferClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + psd_class_properties(class); + object_class->nickname = "psdsave_buffer"; + object_class->description = "save PSD and PSB to a buffer with minipsd"; + object_class->build = vips_foreign_save_psd_buffer_build; + foreign_class->suffs = psd_suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdBuffer, buf), VIPS_TYPE_BLOB); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) +{ + (void)buffer; +} + /* --- Registration -------------------------------------------------------------------------- */ int minipsd_vips_init(void) { - /* Registering a type is enough: libvips finds loaders among VipsForeignLoad's subclasses. */ + /* Registering a type is enough: libvips finds loaders and savers among VipsForeignLoad's and + VipsForeignSave's subclasses. */ (void)vips_foreign_load_psd_file_get_type(); (void)vips_foreign_load_psd_buffer_get_type(); (void)vips_foreign_load_psd_source_get_type(); + (void)vips_foreign_save_psd_file_get_type(); + (void)vips_foreign_save_psd_buffer_get_type(); + (void)vips_foreign_save_psd_target_get_type(); return 0; } diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index 509ac8c..1eec051 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -1,10 +1,12 @@ /** * \file - * \brief minipsd as a libvips loader of PSD and PSB: psdload, psdload_buffer, psdload_source. + * \brief minipsd as a libvips loader and saver of PSD and PSB: psdload, psdload_buffer, + * psdload_source; psdsave, psdsave_buffer, psdsave_target. * - * libvips' prebuilt Windows builds load no modules, so a program registers the loader itself: call - * minipsd_vips_init() once, after VIPS_INIT(). vips_image_new_from_file("x.psd") and the other - * ways libvips finds a loader then find this one. + * libvips' prebuilt Windows builds load no modules, so a program registers them itself: call + * minipsd_vips_init() once, after VIPS_INIT(). vips_image_new_from_file("x.psd"), + * vips_image_write_to_file(image, "x.psd") and the other ways libvips finds a loader or a saver + * then find these. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index e9d633b..0491104 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -37,6 +37,7 @@ #define ASPECT 0.9 #define MM_PER_INCH 25.4 #define EPSILON 1e-9 +#define FLOAT_EPSILON 1e-6 /* a 32-bit composite's un-matting, 1 - (1 - c) / a, is off by rounding */ #define PATH_MAX_SIZE 1024 #define NAME_SIZE (PATH_MAX_SIZE + 64) /* a path and what it names in it */ #define WORKSPACE_SIZE 256 @@ -46,6 +47,8 @@ #define HALF_OPAQUE 128 #define ZIP_LEVEL 6 #define COMPOSITE (-1) /* decode_image's layer: the composite */ +#define STORED_RLE 1 /* a channel's stored compression markers */ +#define STORED_ZIP 2 /* The ZIP document's two layers: one reaching past the canvas's left edge, one inside. */ #define ZIP_W 200 @@ -348,6 +351,114 @@ static void check_indexed(const char *path, const uint8_t *indices, const uint8_ if (image) g_object_unref(image); } +static uint8_t *read_all(const char *path, size_t *size); + +/* The same samples: bytes, or floats within FLOAT_EPSILON. */ +static int same_samples(const void *got, const void *want, size_t bytes, int floats) +{ + const float *g = (const float *)got, *w = (const float *)want; + size_t i; + if (!floats) return memcmp(got, want, bytes) == 0; + for (i = 0; i < bytes / sizeof(float); ++i) + if ((double)fabsf(g[i] - w[i]) > FLOAT_EPSILON) return 0; + return 1; +} + +/* An image over memory, with its interpretation and the test's resolution. */ +static VipsImage *make_image(const void *data, int bands, VipsBandFormat format, VipsInterpretation interpretation) +{ + const size_t bytes = (size_t)PIXELS * (size_t)bands * vips_format_sizeof(format); + VipsImage *memory = vips_image_new_from_memory(data, bytes, WIDTH, HEIGHT, bands, format), *image = NULL; + if (memory) { + vips_copy(memory, &image, "interpretation", interpretation, "xres", DPI / MM_PER_INCH, "yres", DPI / MM_PER_INCH, + NULL); + g_object_unref(memory); + } + return image; +} + +/* An image saved through psdsave as a file, into a buffer and into a target, the same bytes each + time, then read back through psdload: the layer is the image (made opaque when it had no + alpha), and so is the composite when alpha is full (the matte undone exactly). options are a + psdsave option and its value, or NULL. */ +static void check_saved(VipsImage *image, const char *name, const char *dir, int full_alpha, const char *option, + int value) +{ + char path[PATH_MAX_SIZE]; + VipsImage *want_image = NULL, *back; + VipsTarget *target; + VipsBlob *blob = NULL; + void *buffer = NULL, *want = NULL, *got; + uint8_t *file; + size_t file_size = 0, buffer_size = 0, want_bytes = 0, got_bytes = 0, target_size = 0; + const void *target_data = NULL; + const char *saver; + int colors, saved, layer; + if (!image) { + check(0, "image made", name); + return; + } + snprintf(path, sizeof path, "%s/%s", dir, name); + saved = option ? vips_image_write_to_file(image, path, option, value, NULL) : vips_image_write_to_file(image, path, NULL); + if (saved) { + printf("FAIL %s: not saved: %s\n", name, vips_error_buffer()); + vips_error_clear(); + ++failures; + g_object_unref(image); + return; + } + saver = vips_foreign_find_save(path); + check(saver && strcmp(saver, "VipsForeignSavePsdFile") == 0, "found as psdsave", name); + + /* The same bytes three ways. */ + file = read_all(path, &file_size); + target = vips_target_new_to_memory(); + if (option) { + vips_image_write_to_buffer(image, ".psd", &buffer, &buffer_size, option, value, NULL); + if (target) vips_image_write_to_target(image, ".psd", target, option, value, NULL); + } else { + vips_image_write_to_buffer(image, ".psd", &buffer, &buffer_size, NULL); + if (target) vips_image_write_to_target(image, ".psd", target, NULL); + } + if (target) g_object_get(target, "blob", &blob, NULL); + if (blob) target_data = vips_blob_get(blob, &target_size); + check(file && buffer && buffer_size == file_size && memcmp(buffer, file, file_size) == 0, "buffer as the file", name); + check(file && target_data && target_size == file_size && memcmp(target_data, file, file_size) == 0, "target as the file", + name); + vips_error_clear(); + + /* Read back: the layer, then the composite. */ + colors = image->Type == VIPS_INTERPRETATION_CMYK ? 4 : image->Bands >= 3 ? 3 : 1; + if (image->Bands == colors) { + vips_bandjoin_const1(image, &want_image, image->BandFmt == VIPS_FORMAT_FLOAT ? 1.0 + : image->BandFmt == VIPS_FORMAT_USHORT ? USHRT_MAX : UCHAR_MAX, NULL); + } else { + want_image = image; + g_object_ref(want_image); + } + if (want_image) want = vips_image_write_to_memory(want_image, &want_bytes); + for (layer = 0; layer >= COMPOSITE; --layer) { + if (layer == COMPOSITE && !full_alpha) break; + back = vips_image_new_from_file(path, "layer", layer, NULL); + got = back ? vips_image_write_to_memory(back, &got_bytes) : NULL; + check(want && got && got_bytes == want_bytes + && same_samples(got, want, want_bytes, layer == COMPOSITE && image->BandFmt == VIPS_FORMAT_FLOAT), + layer == COMPOSITE ? "composite read back as the image" : "layer read back as the image", name); + if (back && layer == 0) + check(fabs(vips_image_get_xres(back) - DPI / MM_PER_INCH) < EPSILON, "resolution read back", name); + vips_error_clear(); + g_free(got); + if (back) g_object_unref(back); + } + g_free(want); + if (want_image) g_object_unref(want_image); + if (blob) vips_area_unref(VIPS_AREA(blob)); + if (target) g_object_unref(target); + g_free(buffer); + free(file); + g_object_unref(image); +} + /* The whole file in a new buffer; NULL when it cannot be read. */ static uint8_t *read_all(const char *path, size_t *size) { @@ -376,6 +487,23 @@ static size_t layer_count(const void *file, size_t size) return minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK ? info.layer_count : 0; } +/* The stored compression marker of the file's first layer's first channel; -1 without one. */ +static int first_layer_compression(const char *path) +{ + uint8_t workspace[WORKSPACE_SIZE], *data; + minipsd_reader *r; + minipsd_info info; + minipsd_layer_view view; + size_t size = 0; + int marker = -1; + if (!(data = read_all(path, &size))) return -1; + if (minipsd_reader_init(&r, data, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK + && minipsd_reader_next_layer(r, &view) == MINIPSD_MORE && view.channel[0].data && view.channel[0].size >= 2) + marker = view.channel[0].data[0] << 8 | view.channel[0].data[1]; + free(data); + return marker; +} + /* Each file the list names, its composite and every layer, loaded through libvips and compared with minipsd's decode. */ static void check_list(const char *list) @@ -416,6 +544,8 @@ int main(int argc, char **argv) static uint8_t rgba[PIXELS * RGBA], cmyka[PIXELS * MAX_PLANES], bits[PIXELS], indices[PIXELS]; static uint8_t palette[3 * PALETTE], zip_a[ZIP_PIXELS * RGBA], zip_b[ZIP_PIXELS * RGBA]; static uint16_t grey16[PIXELS * 2], rgba16[PIXELS * RGBA]; + static uint8_t rgb8[PIXELS * 3]; + static float rgbf[PIXELS * 3]; static const char junk[] = "8BPX, not a PSD"; char path[PATH_MAX_SIZE]; uint8_t transparency[MINIPSD_TRANSPARENCY_INDEX_SIZE]; @@ -444,6 +574,8 @@ int main(int argc, char **argv) for (i = 0; i < sizeof palette; ++i) palette[i] = (uint8_t)(i * 11 + i / PALETTE * 85 + 3); /* R, G, B differ */ for (i = 0; i < sizeof zip_a; ++i) zip_a[i] = (uint8_t)(i * 3); for (i = 0; i < sizeof zip_b; ++i) zip_b[i] = (uint8_t)(i / 97); + for (i = 0; i < sizeof rgb8; ++i) rgb8[i] = (uint8_t)(i * 17 + i / 255); + for (i = 0; i < PIXELS * 3; ++i) rgbf[i] = (float)(i % 1001) / 1000.0f; { /* 8-bit RGB with a layer, a profile and non-square pixels; as PSD and as PSB */ static const expected psd = { "rgb8", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; @@ -550,6 +682,69 @@ int main(int argc, char **argv) check_layers(path, &doc, 1, predicted.name); } + { /* The saver: grey, RGB and CMYK at 8 and 16 bits, RGB as floats; ZIP; PSB by option and by + suffix; a layer name and a profile kept; what PSD cannot hold refused. */ + VipsImage *image, *back; + uint8_t *file; + size_t size = 0; + const char *got_name; + const void *got_icc, *want_icc = NULL; + size_t want_icc_size = 0; + VipsBlob *profile = NULL; + check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-rgb8.psd", argv[1], 1, NULL, 0); + check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-rgba8.psd", argv[1], 0, NULL, 0); + check_saved(make_image(rgba16, 3, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_RGB16), "save-rgb16.psd", argv[1], 1, NULL, 0); + check_saved(make_image(indices, 1, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_B_W), "save-grey8.psd", argv[1], 1, NULL, 0); + check_saved(make_image(grey16, 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16), "save-greya16.psd", argv[1], 0, NULL, 0); + check_saved(make_image(cmyka, 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_CMYK), "save-cmyk8.psd", argv[1], 1, NULL, 0); + check_saved(make_image(rgbf, 3, VIPS_FORMAT_FLOAT, VIPS_INTERPRETATION_scRGB), "save-float.psd", argv[1], 1, NULL, 0); + check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 1, "zip", TRUE); + snprintf(path, sizeof path, "%s/save-zip.psd", argv[1]); + check(first_layer_compression(path) == STORED_ZIP, "ZIP by option", path); + snprintf(path, sizeof path, "%s/save-rgb8.psd", argv[1]); + check(first_layer_compression(path) == STORED_RLE, "RLE by default", path); + check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-psb.psd", argv[1], 1, "psb", TRUE); + snprintf(path, sizeof path, "%s/save-psb.psd", argv[1]); + file = read_all(path, &size); + check(file && size > 5 && file[5] == MINIPSD_PSB, "PSB by option", path); + free(file); + /* A file named .psb is a PSB; a buffer or a target in memory has no name: psb says it. */ + snprintf(path, sizeof path, "%s/save-suffix.psb", argv[1]); + image = make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB); + check(image && vips_image_write_to_file(image, path, NULL) == 0, "saved", path); + if (image) g_object_unref(image); + back = vips_image_new_from_file(path, "layer", 0, NULL); + check(back != NULL, "read back", path); + if (back) g_object_unref(back); + file = read_all(path, &size); + check(file && size > 5 && file[5] == MINIPSD_PSB, "PSB by suffix", path); + free(file); + + snprintf(path, sizeof path, "%s/save-named.psd", argv[1]); + image = make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB); + /* A real profile: libvips drops one it cannot read when it saves. */ + if (vips_profile_load("srgb", &profile, NULL) == 0) { + want_icc = vips_blob_get(profile, &want_icc_size); + if (image) vips_image_set_blob_copy(image, VIPS_META_ICC_NAME, want_icc, want_icc_size); + } + check(image && vips_call("psdsave", image, path, "layer_name", "Ink", NULL) == 0, "saved by psdsave", path); + back = vips_image_new_from_file(path, "layer", 0, NULL); + check(back && vips_image_get_string(back, "psd-layer-name", &got_name) == 0 && strcmp(got_name, "Ink") == 0, + "layer name kept", path); + check(back && want_icc && vips_image_get_blob(back, VIPS_META_ICC_NAME, &got_icc, &size) == 0 + && size == want_icc_size && memcmp(got_icc, want_icc, size) == 0, + "ICC profile kept", path); + if (profile) vips_area_unref(VIPS_AREA(profile)); + if (back) g_object_unref(back); + if (image) g_object_unref(image); + + snprintf(path, sizeof path, "%s/save-refused.psd", argv[1]); + image = make_image(rgba16, 6, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_MULTIBAND); + check(image && vips_image_write_to_file(image, path, NULL) != 0, "six bands refused", path); + vips_error_clear(); + if (image) g_object_unref(image); + } + /* Not a PSD: psdload does not claim it. */ loader = vips_foreign_find_load_buffer(junk, sizeof junk); check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); From be4313d709b3734aac3dda1b3fef88b26e8a5b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 00:01:34 +0200 Subject: [PATCH 05/22] fix(writer): name the composite's transparency where the count cannot GIMP takes the composite's first extra channel for transparency only by a negative layer count or the alpha name "Transparency", and reads no 'Mt*' block, so a 32-bit document's transparency (its layers in the writer's 'Lr32', the count positive) showed as an extra channel. Where the count cannot say it, the writer now names the channel as Photoshop's transparentbg.psd does: 1006 "Transparency", 1045 the same, 1053 id 0, unless the document keeps names of its own. A document with no layers also gets an empty 'Mtrn' ('Mt16', 'Mt32'), which the reader already honours: its transparency was lost before. Files with layers at 8 and 16 bits are written byte for byte as before. --- docs/SPECIFICATION.md | 13 ++++++- include/minipsd/writer.h | 67 +++++++++++++++++++++++++++++++++--- tests/psd_tools/roundtrip.py | 5 +++ tests/test_reader.cpp | 49 ++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index b55773f..fc068bf 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -355,7 +355,10 @@ several threads from one reader into one image. Photoshop's 16-bit files have them; what the block held goes, the document's layers win. - **No layers:** a document with a composite (supplied or stored) and `layer_count` 0 is written flat: an empty layer info and the tail, as `minipsd_read` keeps a flat file with - `opts->stored`. + `opts->stored`; when the composite has transparency (not in a flat mode), an empty `Mtrn`, + `Mt16` or `Mt32` by its depth after the tail, unless the tail has one (Adobe's "merged + transparency"; no file in psd-tools' set is flat with transparency, since Photoshop keeps it on + a layer). - **The format is what the document is.** A PSB is written with its wide fields (eight-byte section and channel lengths, four-byte RLE row counts), and everything kept from a file is written as it is, so it must be in that format's layout: `minipsd_read` sets `format` to the @@ -418,6 +421,12 @@ several threads from one reader into one image. clipping, groups and effects are not drawn; - transparency (of a supplied or composed composite, always, but in a flat mode) is the first extra channel, signalled by writing the layer count as a **negative** number, as the format specifies; + where the count cannot say it (the writer's `Lr32`, whose count is positive, and a document + with no layers), by the `Mt32` (`Mtrn`, `Mt16`) block and by naming the channel as + Photoshop's `transparentbg.psd` in psd-tools' set does: 1006 "Transparency", 1045 the same + with its NUL, 1053 id 0. GIMP takes the channel for transparency only by a negative count or + that name, and reads none of the blocks; a document that keeps any of 1006, 1045 or 1053 + gets none of the writer's; - **colour is matted against white:** `c*a + max*(1-a)`, `max` being 255 or 65535 (at 32 bits `c*a + (1-a)`). Photoshop stores it this way and readers subtract the white back out. The writer mattes, a supplied composite too, unless `composite_matted` says it already is: then it @@ -435,6 +444,8 @@ several threads from one reader into one image. 1064 goes. The resolution stays square: the ratio is never folded into 1005. Photoshop's files in psd-tools' set all hold 1.0 (290 version 2, 15 version 1), so another ratio is written as the format lays it out but not verified against a reader; + - 1006, 1045 and 1053 naming the composite's transparency, where the layer count cannot say it + (Composite image, above); - a resource of odd length is padded to even. - **Typed setters:** the mirror of the typed views (in [What is read](#what-is-read)): a struct in, the bytes the view reads out, into the caller's buffer, counted first (`needed`, and diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 3685afb..2b0a52f 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -203,8 +203,9 @@ static unsigned mpsd__composite_channels(const minipsd_doc *d) return mpsd__copies_composite(d) ? d->composite_channels : mpsd__doc_pixel(d); } -/* Is the composite's fourth channel transparency, the layer count negative? Always for a - supplied or composed one. */ +/* Is the composite's fourth channel transparency? Always for a supplied or composed one. The layer + count says it, negative, or where it cannot (mpsd__alpha_uncounted) a 'Mtrn' ('Mt16', 'Mt32') + block and the channel's names. */ static int mpsd__composite_alpha(const minipsd_doc *d) { return !mpsd__copies_composite(d) || d->composite_alpha; } /* No source: the pieces outside a layer channel and the composite. */ @@ -455,6 +456,16 @@ static uint64_t mpsd__max_layers_section(unsigned psb) { return psb ? MPSD__FAR static const minipsd_resource *mpsd__find_resource(const minipsd_doc *d, unsigned id); static int mpsd__aspect_kept(const minipsd_doc *d); +static int mpsd__names_transparency(const minipsd_doc *d); +static size_t mpsd__tail_block(const minipsd_doc *d, const char *a, const char *b, size_t *end); + +/* Photoshop's names for the composite's transparency, as its files hold them: the alpha channel + names (1006, a Pascal string; 1045, a count of UTF-16 units, then the name with its NUL) and + their identifiers (1053). */ +static const uint8_t mpsd__transparency_name[] = { 12, 'T', 'r', 'a', 'n', 's', 'p', 'a', 'r', 'e', 'n', 'c', 'y' }; +static const uint8_t mpsd__transparency_unicode[] = { 0, 0, 0, 13, 0, 'T', 0, 'r', 0, 'a', 0, 'n', 0, 's', 0, 'p', + 0, 'a', 0, 'r', 0, 'e', 0, 'n', 0, 'c', 0, 'y', 0, 0 }; +static const uint8_t mpsd__transparency_id[] = { 0, 0, 0, 0 }; /* A kept resource is written unless the document's own field says it: dpi, icc, and a pixel aspect ratio the document sets that no kept 1064 says. */ @@ -542,7 +553,8 @@ static void mpsd__pixel_aspect(mpsd__sink *s, const minipsd_doc *d) } /* The image resources: the writer's own first (the resolution, the ICC profile if any, the pixel - aspect ratio if set), then the kept ones. A kept one of these goes: the document's field says + aspect ratio if set, the transparency's names where the layer count cannot say it), then the + kept ones. A kept one of these goes: the document's field says it. */ static void mpsd__resources(mpsd__sink *s, const minipsd_doc *d) { @@ -553,6 +565,12 @@ static void mpsd__resources(mpsd__sink *s, const minipsd_doc *d) mpsd__resolution(s, d, res ? res->name : none); if (d->icc) mpsd__put_resource(s, MINIPSD_RES_ICC_PROFILE, icc ? icc->name : none, d->icc, d->icc_size); if (d->pixel_aspect_ratio > 0.0 && !mpsd__aspect_kept(d)) mpsd__pixel_aspect(s, d); + if (mpsd__names_transparency(d)) { + mpsd__put_resource(s, MINIPSD_RES_ALPHA_NAMES, none, mpsd__transparency_name, sizeof mpsd__transparency_name); + mpsd__put_resource(s, MINIPSD_RES_UNICODE_ALPHA_NAMES, none, mpsd__transparency_unicode, + sizeof mpsd__transparency_unicode); + mpsd__put_resource(s, MINIPSD_RES_ALPHA_IDENTIFIERS, none, mpsd__transparency_id, sizeof mpsd__transparency_id); + } for (i = 0; i < d->resource_count; ++i) if (mpsd__kept_resource(d, &d->resources[i])) mpsd__put_resource(s, d->resources[i].id, d->resources[i].name, d->resources[i].data.data, d->resources[i].data.size); @@ -594,6 +612,13 @@ static int mpsd__stored_ok(minipsd_bytes c, uint32_t width, uint32_t height, uns layers: the writer writes them there and leaves the layer info empty. What it held goes: the document's layers win. */ static size_t mpsd__layers_block(const minipsd_doc *d, size_t *end) +{ + return mpsd__tail_block(d, "Lr16", "Lr32", end); +} + +/* Where the first block of the kept tail keyed a or b starts, and where it ends (padded); both the + tail's size when there is none. */ +static size_t mpsd__tail_block(const minipsd_doc *d, const char *a, const char *b, size_t *end) { const minipsd_bytes t = d->layer_tail; const unsigned psb = mpsd__doc_psb(d); @@ -611,7 +636,7 @@ static size_t mpsd__layers_block(const minipsd_doc *d, size_t *end) size = mpsd__getlen(p + MPSD__BLOCK_LEN, head > MPSD__BLOCK_HEAD); if (size > left - head) return t.size; span = mpsd__pad((size_t)size, 4) < left - head ? mpsd__pad((size_t)size, 4) : left - head; - if (mpsd__tag(p + MPSD__BLOCK_KEY, "Lr16") || mpsd__tag(p + MPSD__BLOCK_KEY, "Lr32")) { *end = at + head + span; return at; } + if (mpsd__tag(p + MPSD__BLOCK_KEY, a) || mpsd__tag(p + MPSD__BLOCK_KEY, b)) { *end = at + head + span; return at; } at += head + span; } return t.size; @@ -663,6 +688,38 @@ static int mpsd__own_layers_block(const minipsd_doc *d) return mpsd__doc_depth(d) == MPSD__CH_32BIT && d->layer_count && mpsd__layers_block(d, &end) == d->layer_tail.size; } +/* Is the composite's transparency where the layer count cannot say it? In the writer's 'Lr32', + whose count is positive, and in a document with no layers, whose count is 0; a flat mode has + none. */ +static int mpsd__alpha_uncounted(const minipsd_doc *d) +{ + return mpsd__composite_alpha(d) && !mpsd__doc_flat(d) && (mpsd__own_layers_block(d) || !d->layer_count); +} + +/* Then the writer names it as Photoshop does, which GIMP reads it by, unless the document keeps + alpha channel names or identifiers of its own. */ +static int mpsd__names_transparency(const minipsd_doc *d) +{ + return mpsd__alpha_uncounted(d) && !mpsd__find_resource(d, MINIPSD_RES_ALPHA_NAMES) + && !mpsd__find_resource(d, MINIPSD_RES_UNICODE_ALPHA_NAMES) && !mpsd__find_resource(d, MINIPSD_RES_ALPHA_IDENTIFIERS); +} + +/* A document with no layers says it with an empty block after the tail, as the 'Lr32' one does + with 'Mt32': 'Mtrn', 'Mt16' or 'Mt32' by its depth (Adobe's "merged transparency"), unless the + kept tail has one. */ +static const char *mpsd__merged_key(const minipsd_doc *d) +{ + const unsigned depth = mpsd__doc_depth(d); + return depth == MPSD__CH_32BIT ? "Mt32" : depth == MPSD__CH_16BIT ? "Mt16" : "Mtrn"; +} + +static int mpsd__own_merged_block(const minipsd_doc *d) +{ + size_t end; + return !d->layer_count && mpsd__alpha_uncounted(d) && mpsd__tail_block(d, "Mtrn", "Mt16", &end) == d->layer_tail.size + && mpsd__tail_block(d, "Mt32", "Mt32", &end) == d->layer_tail.size; +} + /* A block of the writer's own, of no data or of size bytes that follow it: '8BIM', or in a PSB '8B64' as Photoshop marks the long keys these are, the key, the length. */ static void mpsd__own_block(mpsd__sink *s, const char *key, uint64_t size, unsigned out) @@ -691,6 +748,7 @@ static uint64_t mpsd__section_size(const minipsd_doc *d, uint64_t info, unsigned if (mpsd__layers_block(d, &end) < d->layer_tail.size || mpsd__own_layers_block(d)) c.count += MPSD__BLOCK_LEN + mpsd__len_size(out) + (mpsd__pad((size_t)info, 4) - info); /* '8BIM', 'Lr16', length; padding */ if (mpsd__own_layers_block(d) && mpsd__composite_alpha(d)) c.count += MPSD__BLOCK_LEN + mpsd__len_size(out); /* 'Mt32' */ + if (mpsd__own_merged_block(d)) c.count += MPSD__BLOCK_LEN + mpsd__len_size(out); /* 'Mtrn' */ return mpsd__len_size(out) + info + c.count; } @@ -922,6 +980,7 @@ static void mpsd__tail(mpsd__sink *s, const minipsd_doc *d, uint64_t content, ui mpsd__zeros(s, mpsd__pad((size_t)info, 4) - (size_t)info); } else { (void)mpsd__tail_range(s, d, 0, d->layer_tail.size, out); /* minipsd_doc_check has accepted it */ + if (mpsd__own_merged_block(d)) mpsd__own_block(s, mpsd__merged_key(d), 0, out); } if (!mpsd__copies_composite(d)) mpsd__u16(s, mpsd__marker(d)); /* a stored composite brings its own */ } diff --git a/tests/psd_tools/roundtrip.py b/tests/psd_tools/roundtrip.py index 5003108..c45331d 100644 --- a/tests/psd_tools/roundtrip.py +++ b/tests/psd_tools/roundtrip.py @@ -636,6 +636,11 @@ def plane(values, k): if depth == 32: # in 'Lr32', as Photoshop keeps them, the layer info empty; 'Mt32': the composite's alpha expect("layer info", li.layer_count if li else 0, 0) expect("blocks", [k for k in (b"Mt32", b"Lr32") if lmi.tagged_blocks.get_data(k) is not None], [b"Mt32", b"Lr32"]) + # the layer count cannot say it, so the channel is named as Photoshop's transparentbg.psd + # names it, which GIMP reads it by + names = psd.image_resources + expect("alpha names", tuple(list(names.get_data(k) or []) for k in (ResourceId.ALPHA_NAMES_PASCAL, + ResourceId.ALPHA_NAMES_UNICODE, ResourceId.ALPHA_IDENTIFIERS)), (["Transparency"], ["Transparency\x00"], [0])) li = lmi.tagged_blocks.get_data(b"Lr32") expect("layers", len(li.layer_records), 2) for record, channels, values, (rw, rh) in zip(li.layer_records, li.channel_image_data, (base, top), ((w, h), (lw, lh))): diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp index 9a3a5c1..97f2bc6 100644 --- a/tests/test_reader.cpp +++ b/tests/test_reader.cpp @@ -2707,6 +2707,55 @@ TEST(MinipsdReadTest, EveryModeComesBackAsWritten) ASSERT_TRUE(g); EXPECT_EQ(write(*g), again) << "a second pass changes no byte"; + // Where the layer count cannot say the composite has transparency ('Lr32' holds a positive one; + // a document with no layers has 0), the writer names the channel as Photoshop does, which GIMP + // reads it by: 1006, 1045 and 1053, once, kept as they are on the next pass; a document with no + // layers also gets an empty 'Mtrn' ('Mt16', 'Mt32') after its tail, which the reader honours. + const Bytes pascal = {12, 'T', 'r', 'a', 'n', 's', 'p', 'a', 'r', 'e', 'n', 'c', 'y'}; + const auto names = [&](const Bytes &file) { + Open o(file); + EXPECT_EQ(o.rc, MINIPSD_OK); + std::vector ids; + minipsd_resource res{}; + for (std::size_t at = 0; minipsd_reader_next_resource(o.r, &at, &res) == MINIPSD_MORE;) + if (res.id == MINIPSD_RES_ALPHA_NAMES || res.id == MINIPSD_RES_UNICODE_ALPHA_NAMES || res.id == MINIPSD_RES_ALPHA_IDENTIFIERS) { + ids.push_back(res.id); + if (res.id == MINIPSD_RES_ALPHA_NAMES) { EXPECT_EQ(Bytes(res.data.data, res.data.data + res.data.size), pascal); } + } + EXPECT_NE(o.info.composite_has_alpha, 0); + return ids; + }; + const std::vector photoshops = {MINIPSD_RES_ALPHA_NAMES, MINIPSD_RES_UNICODE_ALPHA_NAMES, MINIPSD_RES_ALPHA_IDENTIFIERS}; + EXPECT_EQ(names(floats), photoshops) << "'Lr32'"; + EXPECT_EQ(names(again), photoshops) << "kept, not written twice"; + const std::vector greyAlpha = {10, 255, 20, 128}; // 2 x 1, grey and transparency + minipsd_doc flat = docOf(2, 1, nullptr, 0); + flat.color_mode = MINIPSD_MODE_GREY; + flat.composite = greyAlpha.data(); + const Bytes noLayers = write(flat); + EXPECT_EQ(names(noLayers), photoshops) << "no layers"; + const Bytes mtrn = {'8', 'B', 'I', 'M', 'M', 't', 'r', 'n', 0, 0, 0, 0}; + const auto block = std::search(noLayers.begin(), noLayers.end(), mtrn.begin(), mtrn.end()); + ASSERT_NE(block, noLayers.end()) << "an empty 'Mtrn'"; + EXPECT_EQ(std::search(block + 1, noLayers.end(), mtrn.begin(), mtrn.end()), noLayers.end()) << "one"; + const Doc h = readDoc(noLayers, nullptr); + ASSERT_TRUE(h); + const Bytes once = write(*h); + const Doc k = readDoc(once, nullptr); + ASSERT_TRUE(k); + EXPECT_EQ(write(*k), once) << "a second pass changes no byte"; + EXPECT_EQ(names(once), photoshops) << "read and written: kept, not written twice"; + const Bytes other = {7, 'O', 'u', 'r', ' ', 'o', 'w', 'n'}; + const minipsd_resource own{MINIPSD_RES_ALPHA_NAMES, {other.data(), other.size()}, {}}; + flat.resources = &own; flat.resource_count = 1; + { + Open o(write(flat)); + minipsd_resource res{}; + ASSERT_EQ(minipsd_reader_find_resource(o.r, MINIPSD_RES_ALPHA_NAMES, &res), MINIPSD_OK); + EXPECT_EQ(Bytes(res.data.data, res.data.data + res.data.size), other) << "the document's own names win"; + EXPECT_EQ(minipsd_reader_find_resource(o.r, MINIPSD_RES_ALPHA_IDENTIFIERS, &res), MINIPSD_ERR_NOT_FOUND) << "none of the writer's"; + } + const Bytes bits = {0, 1, 0, 0, 7, 0, 0, 0, 255, 1}; // 10 x 1: 0 white, any other black minipsd_doc b = docOf(10, 1, nullptr, 0); b.depth = 1; From b542d0836260729889945876b159304814a3d05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 00:50:20 +0200 Subject: [PATCH 06/22] docs(vips): the plugin documented; CMYK as libvips holds it (S7.2-S7.4, 5) CMYK reached libvips as PSD stores it, the ink inverted; libvips holds 0 as no ink (as tiffload gives it, as jpegload and ImageMagick make it), so psdload inverts the four colour channels and psdsave inverts them back. MINIPSD_VIPS_PLUGIN moves to the top CMakeLists, so the plugin builds without the tests; a program may bring its own libvips as the targets minipsd_libvips and minipsd_libvips_zlib. vips.plugin also reads a 1024 x 30000 PSD sequentially, libvips holding a few rows' worth, and checks the saver's CMYK in the file without the plugin. examples/vips_composite.c builds on Linux (three casts). MANUAL, SPECIFICATION, CODE-ARCH, CHEATSHEET, README and the changelog describe the plugin; the ROADMAP ticks S7.2 to S7.4. --- CMakeLists.txt | 9 ++- README.md | 3 + cmake/minipsd_libvips.cmake | 8 +++ docs/CHANGELOG.md | 18 ++++++ docs/CHEATSHEET.md | 2 + docs/CODE-ARCH.md | 7 ++- docs/MANUAL.md | 35 ++++++++++++ docs/ROADMAP.md | 29 ++++++++-- docs/SPECIFICATION.md | 61 ++++++++++++++++++++ examples/vips_composite.c | 6 +- libvips-plugin/CMakeLists.txt | 7 ++- libvips-plugin/minipsd_vips.c | 30 +++++++++- libvips-plugin/test_vips.c | 104 +++++++++++++++++++++++++++++++++- tests/CMakeLists.txt | 1 - 14 files changed, 304 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 23f0eff..447684a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,8 +28,10 @@ project(minipsd # Tests and install rules are for working on minipsd itself, not for consumers. option(MINIPSD_BUILD_TESTS "Build the minipsd tests" ${PROJECT_IS_TOP_LEVEL}) option(MINIPSD_INSTALL "Generate the minipsd install rules" ${PROJECT_IS_TOP_LEVEL}) +# The libvips plugin (libvips-plugin/): psdload and psdsave for a program that links libvips. +option(MINIPSD_VIPS_PLUGIN "Build the libvips plugin, minipsd::vips (libvips fetched on Windows)" OFF) -if(PROJECT_IS_TOP_LEVEL OR MINIPSD_BUILD_TESTS) +if(PROJECT_IS_TOP_LEVEL OR MINIPSD_BUILD_TESTS OR MINIPSD_VIPS_PLUGIN) enable_language(C) # also gives GNUInstallDirs a known architecture endif() include(GNUInstallDirs) @@ -170,5 +172,8 @@ endif() if(MINIPSD_BUILD_TESTS) enable_language(CXX) enable_testing() - add_subdirectory(tests) + add_subdirectory(tests) # the plugin too, with the tests' warnings and its own test +elseif(MINIPSD_VIPS_PLUGIN) + include(cmake/minipsd_libvips.cmake) + add_subdirectory(libvips-plugin) endif() diff --git a/README.md b/README.md index 1b1fd71..c221e65 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ no deflate), and are copied as they are when such a file is read and written bac files are bounded by caller limits. How it does on 315 files from other programs: [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md). +In a program built on libvips, [libvips-plugin/](libvips-plugin/) makes minipsd its PSD and PSB +loader and saver; see the manual's [With libvips](docs/MANUAL.md#with-libvips). + ## Adding it to a project Copy the `include/` directory, or use CMake (FetchContent, git submodule, `find_package`), diff --git a/cmake/minipsd_libvips.cmake b/cmake/minipsd_libvips.cmake index 0094249..35598e3 100644 --- a/cmake/minipsd_libvips.cmake +++ b/cmake/minipsd_libvips.cmake @@ -10,8 +10,16 @@ # zlib-compatible; versions.json) with zlib's own headers, fetched and # pinned, since the zip has none; elsewhere pkg-config's zlib # MINIPSD_LIBVIPS_ENV the environment its tests run in (Windows: the DLLs on PATH), or "" +# +# A program with libvips of its own defines both targets before adding minipsd, visible to it (an +# INTERFACE library, or IMPORTED GLOBAL); nothing is then fetched or found here. # --------------------------------------------------------------------------- +if(TARGET minipsd_libvips AND TARGET minipsd_libvips_zlib) + set(MINIPSD_LIBVIPS_ENV "") + return() +endif() + if(WIN32) set(MINIPSD_VIPS_VERSION 8.18.7) include(FetchContent) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5e2cd87..e61419a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,24 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 rules in [TODO.md](TODO.md): breaking changes bump the MINOR, additions and fixes the PATCH. +## [Unreleased] + +### Added +- The libvips plugin (`libvips-plugin/`, `MINIPSD_VIPS_PLUGIN`, the static library + `minipsd::vips`): minipsd as libvips' loader and saver of PSD and PSB, registered with + `minipsd_vips_init()` after `VIPS_INIT`. `psdload` gives the composite or, with `layer`, one + layer placed by its offset, decoded in bands of rows from libvips' threads; grey, RGB, CMYK, + bitmap, indexed (through its palette), Lab and multichannel at every depth, with the + resolution, ICC profile, EXIF, XMP, IPTC and `psd-*` fields. `psdsave` writes an image as one + layer with its composite, with `psb`, `zip` and `layer_name`. + +### Fixed +- A composite's transparency is now named where the layer count cannot say it (a 32-bit + document, whose layers go in `Lr32` with a positive count, and a document with no layers): 1006, + 1045 and 1053 "Transparency", as Photoshop writes them, unless the document keeps names of its + own. GIMP showed it as an extra channel. A document with no layers also gets an empty `Mtrn` + (`Mt16`, `Mt32`): its transparency was lost before. + ## [0.3.1] — 2026-10-24 ### Added diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 9cdfefe..46d7a43 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -136,6 +136,7 @@ sudo tar -xzf dist/minipsd-0.1.0.tar.gz --strip-components=1 -C /usr/local | `minipsd-cxx17` (build target) | the header compiles warning-free as C++17 | | `minipsd-include-check` (build target) | `minipsd.h` compiles alone, then with its implementation, leaving no internal function unused | | `psd-tools-roundtrip` | documents written through minipsd, read back by `psd-tools` | +| `vips.plugin` | with `MINIPSD_VIPS_PLUGIN`: documents and every file at hand loaded through libvips (composite and layers) against minipsd's decode, saved and read back, a tall image read in bands | | `install` | `cmake --install` into the build tree (`tests/prefix/`) | | `consumer-copy` | a project with `include/` copied in | | `consumer-subdirectory` | `add_subdirectory` (git submodule) | @@ -154,6 +155,7 @@ paths also check the SBOM fragment's version and that it lists the headers. | `MINIPSD_INSTALL` | ON only as the top-level project | install rules | | `MINIPSD_REQUIRE_PSD_TOOLS` | OFF (CI: ON) | fail the configure instead of skipping the round trip | | `MINIPSD_VIPS_EXAMPLE` | OFF (CI: ON in `msvc`) | build and run `examples/vips_composite.c`: on Windows fetch libvips' prebuilt "web" build (8.18.7, 11 MB, pinned by hash), elsewhere find libvips with pkg-config; with `tests/fixtures/Test-g-example.psd` present, also on that file | +| `MINIPSD_VIPS_PLUGIN` | OFF (CI: ON in `msvc`) | build `minipsd::vips`, the libvips plugin (`libvips-plugin/`), and with the tests `vips.plugin`; libvips as for `MINIPSD_VIPS_EXAMPLE`, plus its zlib (on Windows zlib 1.3.1's headers, pinned by hash) | | `MINIPSD_PSD_TOOLS_FILES` | OFF (CI: ON in `linux-gcc`) | fetch psd-tools' 315 test files, 278 PSD and 37 PSB (v1.25.0, about 22 MB, pinned by hash) and read every one | | `MINIPSD_TEST_FIXTURES` | ON (`linux-coverage`: OFF) | read the local PSDs in `tests/fixtures/` too; off, a local coverage run measures what CI's does | | `MINIPSD_CLANG_TIDY` | empty (`linux-tidy`: `clang-tidy`) | run clang-tidy on the test targets | diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index 5b86972..fc23a82 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -115,6 +115,11 @@ examples/ the I/O implementations; built and run in CI, nev write_nonblocking.c O_NONBLOCK pipe + poll (POSIX) read_file.c whole file into memory, described, every layer and the composite decoded resave.c minipsd_read with stored, one layer changed, written back: the round trip +libvips-plugin/ minipsd as libvips' loader and saver (psdload, psdsave), registered by the program + minipsd_vips.h, .c minipsd_vips_init and the six operations; minipsd compiled in, private + test_vips.c documents and files loaded and saved through libvips, against minipsd's decode +cmake/ + minipsd_libvips.cmake libvips and its zlib for the example and the plugin: fetched on Windows, else pkg-config tests/ test_.cpp one file per module; detail modules are tested directly fuzz/ the reader's libFuzzer target, run locally (linux-fuzz preset); @@ -730,7 +735,7 @@ commit message. [ROADMAP.md](ROADMAP.md) turns these into numbered steps. | **4. Reader** | `reader.h` for the own subset, then real-application files; fuzzing | 0.2.1 | | **5. Round trip** | layer rectangles in the writer, a supplied composite, full import, kept unknown content, pass-through | 0.2.x | | **6. Features** | one row of the table above per stage, writer and reader together | 0.2.x | -| **7. Interoperability** | libvips: an example and a MANUAL chapter | 0.2.x | +| **7. Interoperability** | libvips: an example, a MANUAL chapter, and the plugin (`psdload`, `psdsave`) | 0.3.x | Every stage follows these rules: diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 21253a1..8ee4854 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -790,6 +790,41 @@ materialised before writing. Reading, bands of rows (`minipsd_decode_opts`) fit Build the example with `-DMINIPSD_VIPS_EXAMPLE=ON`: on Windows CMake fetches libvips' prebuilt build, elsewhere pkg-config finds yours. minipsd itself never depends on libvips. +### PSD in libvips: the plugin + +[libvips-plugin/](../libvips-plugin/) makes minipsd libvips' loader and saver of PSD and PSB, so +`vips_image_new_from_file` and `vips_image_write_to_file` take `.psd` and `.psb` in your program. +libvips' prebuilt Windows builds load no modules, so your program registers them, once, after +`VIPS_INIT`: + +```c +#include "minipsd_vips.h" + +if (VIPS_INIT(argv[0])) vips_error_exit(NULL); +minipsd_vips_init(); +VipsImage *page = vips_image_new_from_file("page.psd", NULL); /* the composite */ +VipsImage *third = vips_image_new_from_file("page.psd", "layer", 2, NULL); /* the third layer up */ +vips_image_write_to_file(page, "copy.psd", "layer_name", "Page", NULL); +``` + +- **Loading** (`psdload`, `psdload_buffer`, `psdload_source`): the composite, or with `layer` one + layer, bottom first, placed by `xoffset` and `yoffset`. Grey, RGB and CMYK come as libvips + names them (CMYK with 0 for no ink, the other way round from PSD), bitmap as `B_W` (black 0), + indexed through its palette as sRGB, Lab and multichannel as `MULTIBAND`, each at the file's + depth. With it: the resolution, the ICC profile, EXIF, XMP, IPTC, `bits-per-sample`, and + `psd-*` fields such as `psd-layer-count` and a layer's name, blend mode and opacity (the list: + the spec's [Working with libvips](SPECIFICATION.md#working-with-libvips)). Bands of rows are + decoded from libvips' threads at once; with `access=sequential` memory stays at a few bands. +- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): one layer that is also the + composite; grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats; alpha kept, or the layer + made opaque. Options: `psb` (also a file named `.psb`), `zip`, `layer_name`. The image is + held whole while it is written. +- **Building:** `-DMINIPSD_VIPS_PLUGIN=ON` gives the static library `minipsd::vips`; link it. + minipsd is compiled into it privately (`MINIPSD_STATIC`), so your own use of minipsd keeps its + own. libvips comes as for the example, or from your project: define `minipsd_libvips` and + `minipsd_libvips_zlib` (libvips, and the zlib it links, which inflates ZIP layers) before + adding minipsd. + ## Memory and ownership minipsd allocates only inside `minipsd_write`, which frees it before returning, and diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ca3fa49..50f8032 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -521,9 +521,10 @@ feature; a supplied composite needs nothing. See libvips is where minipsd is meant to be used: as a loader and saver of PSD and PSB, and in programs that render a composite with libvips and write the layers with minipsd. Steps 7.2 on are the loader and saver, built first as a plugin in this repository (`libvips-plugin/`): libvips -foreign classes registered by the program that links them, then, if libvips' prebuilt builds -load modules, a loadable module. Platemaker uses it, which is the proof; a pull request to libvips -comes at the end of stage 7, or of 8, with the same files. +foreign classes registered by the program that links them. libvips' prebuilt Windows builds load +no modules (8.18.7: `enable modules: false`), so a loadable module waits for a Linux user who wants +one. Platemaker uses it, which is the proof; whether a pull request to libvips follows is decided +once Platemaker does. - [x] **S7.1** `examples/vips_composite.c`: a document's composite rendered with libvips (`vips_composite`, blend keys mapped as in the spec's @@ -537,7 +538,7 @@ comes at the end of stage 7, or of 8, with the same files. is read back and its composite must be the render, which is the test. CI's `msvc` job runs it; locally it also runs on `tests/fixtures/Test-g-example.psd`. Tracking libvips' master is left for later. -- [ ] **S7.2** *design.* minipsd as a libvips loader and saver: +- [x] **S7.2** *design.* minipsd as a libvips loader and saver: - **Loading.** What the image is: the composite by default, one layer by an option, its offset in the metadata. Metadata: ICC profile, resolution, interpretation, sample format. @@ -546,13 +547,29 @@ comes at the end of stage 7, or of 8, with the same files. - **What minipsd still needs.** The row provider (S7.5); the metadata ImageMagick gives (S6.17, P1). - **Building.** The build dependency: pkg-config, a meson wrap. -- [ ] **S7.3** Loader: a PSD or PSB as a libvips image, the composite or a layer, decoded in + As done: registered by the program (`minipsd_vips_init`), no module; the composite by + default, `layer=N` with its offset; the samples as libvips holds them (CMYK's ink the other + way round, bitmap `B_W`, indexed through its palette: libvips has no indexed image), the + rest as stored, what libvips cannot hold `MULTIBAND`, nothing converted; minipsd compiled + in privately; zlib libvips' own; libvips from `cmake/minipsd_libvips.cmake` or the program. + Pages as layers, and a meson wrap, wait for a pull request to libvips. +- [x] **S7.3** Loader: a PSD or PSB as a libvips image, the composite or a layer, decoded in strips; ZIP through zlib. *Done when:* psd-tools' files that minipsd decodes load with the samples psd-tools gives; a large PSB loads with memory bounded by strips, not by the image. -- [ ] **S7.4** Saver: an image as a one-layer PSD or PSB with its composite, the image whole in + As done: `vips.plugin` compares every composite and layer of psd-tools' set (and + `tests/fixtures`) with minipsd's decode, which `psd-tools-roundtrip` holds to psd-tools' + (318 of 319 composites, 1330 of 2280 layers; the rest refused by both: a truncated file and + 950 empty layers); a 1024 x 30000 PSD read with `access=sequential` leaves libvips holding + under 2 MB of its 60 MB (all of it when the loader is not sequential). A PSD, not a PSB: the same bands. +- [x] **S7.4** Saver: an image as a one-layer PSD or PSB with its composite, the image whole in memory; RLE, or ZIP through zlib. *Done when:* psd-tools and GIMP open what it writes, and the loader reads it back the same. + As done: `psdsave`, `_buffer`, `_target`; options `psb`, `zip`, `layer_name`. psd-tools + opens every file the test writes, its composite the layer (but where alpha is partial, by + the matte's rounding), full cyan as (255, 0, 0, 0); GIMP opens them, by hand. GIMP showed a + 32-bit file's transparency as an extra channel: the writer now names it as Photoshop does + where the layer count cannot (see the changelog). - [ ] **S7.5** Row provider (`W6`): a layer's or the composite's rows asked from a callback instead of an `pixels` pointer, twice with RLE or ZIP (sizing, then writing), so it must be deterministic, as libvips pipelines are. The saver then streams. diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index fc068bf..ec81af0 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -695,6 +695,67 @@ does, and its memory images are minipsd's buffers, with no conversion: of libvips, the composite rendered by `vips_composite`, the file written with it and read back; CI's `msvc` job runs it on libvips' prebuilt Windows build, pinned to the newest release. +### The plugin + +`libvips-plugin/` (`MINIPSD_VIPS_PLUGIN`, the static library `minipsd::vips`) is minipsd as +libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSource` or a +`VipsTarget`, file and buffer variants that make one), so the same file can move into libvips. + +- **Registration.** `minipsd_vips_init()`, after `VIPS_INIT`, registers the six operations' + types; libvips then finds them among its loaders and savers by `is_a` and suffix. libvips' + prebuilt Windows builds (8.18.7: `enable modules: false`) load no modules, so a loadable module + would not work there; the program registers instead. The loader's priority is libvips' + default, 0, above `magickload`'s -100. +- **Loading** (`psdload`, `psdload_buffer`, `psdload_source`). The file is mapped + (`vips_source_map`) and read in place. The image is the composite, or with `layer` (bottom + first; -1, the default, the composite) that layer: its rectangle, placed by `xoffset` and + `yoffset`, always with transparency (opaque when the layer has none). An empty layer (a group's + marker, an adjustment) is refused. Bands are the mode's colour channels and transparency, up to + `MINIPSD_PLANES` (a multichannel file's sixth channel on is not decoded): + + | Mode | libvips | Samples | + |---|---|---| + | grey, duotone | `B_W`, `GREY16` at 16 bits | as stored | + | RGB | `sRGB`, `RGB16`, `scRGB` at 32 bits | as stored | + | CMYK | `CMYK` | ink inverted from PSD's (full: no ink) to libvips' (0: no ink), as its `jpegload` makes Adobe's inverted CMYK and ImageMagick makes PSD's; transparency as stored | + | bitmap | `B_W`, 8-bit | black 0, white 255 | + | indexed | `sRGB`, 8-bit | through the palette, a fourth band when 1047 names a transparent index | + | Lab, multichannel | `MULTIBAND` | as stored | + + Decoding is `SEQUENTIAL`: `vips_image_generate`, then `vips_sequential`, as `spngload` does. + Each band of rows is one `minipsd_decode_composite` (or `_layer`) straight into the region, + from libvips' threads at once on the one reader (see Threads); with `access=sequential` the + image streams, otherwise libvips keeps it for random access. A ZIP layer inflates through the + zlib libvips links, each thread keeping the layer's channels inflated between its bands + (`scratch_kept`), so every thread holds the layer's channels once. +- **Metadata**, set on the image: + + | Field | From | + |---|---| + | `xres`, `yres`, `resolution-unit` | 1005 (horizontal and vertical, in or cm); else `dpi` | + | `icc-profile-data`, `exif-data`, `xmp-data`, `iptc-data` | 1039, 1058, 1060, 1028 | + | `bits-per-sample` | the file's depth (1 for bitmap) | + | `psd-color-mode-data` | the palette or the duotone data | + | `psd-pixel-aspect-ratio` | 1064, 1.0 without it | + | `psd-has-real-merged-data` | 1057; 1 without it | + | `psd-layer-count` | the file's layers | + | `psd-layer-name`, `-blend-mode`, `-opacity`, `-hidden` | the layer, with `layer` | + +- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`). One layer that is also the + composite: grey, RGB or CMYK (by `interpretation` and bands, each with or without alpha), 8 or + 16 bits, grey and RGB as floats; other band counts are refused. Without alpha the layer is + made opaque. CMYK goes back to PSD's inverted ink. Kept: the resolution, the ICC profile, EXIF, + XMP, IPTC and `psd-pixel-aspect-ratio`. Options: `psb` (PSB also for a file named `.psb`, read + from the target, since libvips writes files through `psdsave_target`, and for a side past + 30000), `zip` (the layer's channels; the composite stays RLE), `layer_name` ("Layer 1"). The + image is held whole while it is written; streaming waits for the row provider (S7.5). +- **Builds.** The plugin compiles minipsd in privately (`MINIPSD_STATIC`). On Windows it uses the + zip's `libz1` (zlib-ng, built zlib-compatible) with zlib 1.3.1's headers, fetched and pinned, + since the zip has none; elsewhere pkg-config's `vips` and `zlib`. libvips 8.17 replaced + `VipsSaveable` with `VipsForeignSaveable`; both are handled. CI's `msvc` job runs its test, + which compares every pixel with minipsd's own decode, on psd-tools' set too, and checks that a + 30000-row image read sequentially leaves libvips holding a few rows' worth. + ## Consumer requirements Platemaker is the first consumer. Breaking any of these breaks it: diff --git a/examples/vips_composite.c b/examples/vips_composite.c index de4b073..fbd6028 100644 --- a/examples/vips_composite.c +++ b/examples/vips_composite.c @@ -127,7 +127,7 @@ static uint8_t *render(const minipsd_doc *d, size_t *unmapped) || l->right <= l->left || l->bottom <= l->top) continue; if (!(in[n] = layer_image(l))) goto done; - modes[n - 1] = blend_mode(l->blend_mode, &mapped); + modes[n - 1] = (int)blend_mode(l->blend_mode, &mapped); *unmapped += !mapped; x[n - 1] = (int)l->left; /* where the layer goes; it may reach past the canvas */ y[n - 1] = (int)l->top; @@ -199,12 +199,12 @@ static void make_layers(minipsd_layer layers[4]) int x, y; for (y = 0; y < H; ++y) for (x = 0; x < W; ++x) { /* a gradient: red across, green down, opaque */ - uint8_t *p = background + ((size_t)y * W + x) * RGBA; + uint8_t *p = background + ((size_t)y * W + (size_t)x) * RGBA; p[0] = (uint8_t)(x * FULL / (W - 1)); p[1] = (uint8_t)(y * FULL / (H - 1)); p[2] = 160; p[3] = FULL; } for (y = 0; y < 128; ++y) for (x = 0; x < 128; ++x) { /* an orange disc, transparent around it */ - uint8_t *p = disc + ((size_t)y * 128 + x) * RGBA; + uint8_t *p = disc + ((size_t)y * 128 + (size_t)x) * RGBA; const int dx = x - 64, dy = y - 64; p[0] = 255; p[1] = 150; p[2] = 40; p[3] = dx * dx + dy * dy < 60 * 60 ? FULL : 0; } diff --git a/libvips-plugin/CMakeLists.txt b/libvips-plugin/CMakeLists.txt index 169c04f..7ede971 100644 --- a/libvips-plugin/CMakeLists.txt +++ b/libvips-plugin/CMakeLists.txt @@ -1,7 +1,8 @@ # --------------------------------------------------------------------------- # The libvips plugin: minipsd as libvips' psdload (MINIPSD_VIPS_PLUGIN). A static library the # program links, then registers with minipsd_vips_init() after VIPS_INIT(); minipsd is compiled -# into it, private (MINIPSD_STATIC). libvips comes from cmake/minipsd_libvips.cmake. +# into it, private (MINIPSD_STATIC). libvips comes from cmake/minipsd_libvips.cmake, or from the +# program, which defines minipsd_libvips and minipsd_libvips_zlib before adding minipsd. # # minipsd::vips the plugin # vips.plugin documents minipsd writes and the PSDs at hand, loaded through libvips and @@ -15,6 +16,10 @@ target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd::minip target_compile_options(minipsd_vips PRIVATE ${MINIPSD_WARNINGS}) set_target_properties(minipsd_vips PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) +if(NOT MINIPSD_BUILD_TESTS) + return() +endif() + add_executable(minipsd-vips-test test_vips.c) target_link_libraries(minipsd-vips-test PRIVATE minipsd::vips minipsd::minipsd minipsd_libvips_zlib) target_compile_options(minipsd-vips-test PRIVATE ${MINIPSD_WARNINGS}) diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index d42caa8..9cf537d 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -12,7 +12,9 @@ * layer's always, opaque when it has none). What libvips has no interpretation for (Lab, * multichannel) is MULTIBAND; the colour mode data is kept as "psd-color-mode-data". Indexed is * the exception: libvips has no indexed image, so it comes through its palette as sRGB, as - * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). ZIP layers + * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). CMYK + * comes the other way round from PSD's inverted ink, and bitmap as B_W (black 0), as libvips + * holds them. ZIP layers * are inflated by the zlib libvips links. * * The saver writes an image as a one-layer PSD (or PSB) with that layer as its composite: grey, RGB @@ -48,6 +50,7 @@ #define PSD_RGB 3 #define PSD_COMPOSITE (-1) /* the `layer` option's default: the composite */ #define MM_PER_INCH 25.4 +#define PSD_CMYK 4 /* CMYK's colour channels */ /* libvips' VIPS_ARG_* macros pass vips_argument_get_id()'s int as a guint property id, and G_STRUCT_OFFSET's glong as a guint offset, which -Wsign-conversion and -Wconversion report in @@ -333,6 +336,28 @@ static int psd_stop(void *seq, void *a, void *b) /* Indexed, in place: each pixel's index (in its first band) becomes the palette's RGB, and alpha 0 at the transparent index. */ +/* CMYK the other way round, in place: PSD stores ink inverted (full: no ink), libvips holds it + as TIFF does (0: no ink), as its own jpegload makes Adobe's inverted CMYK and ImageMagick + makes PSD's. The colour channels only, not transparency; both ways, loading and saving. */ +static void psd_invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) +{ + size_t i; + unsigned c; + for (i = 0; i < count; ++i) + for (c = 0; c < PSD_CMYK; ++c) { + if (depth == 32) { + float *v = (float *)pixels + i * bands + c; + *v = 1.0f - *v; + } else if (depth == 16) { + uint16_t *v = (uint16_t *)pixels + i * bands + c; + *v = (uint16_t)(USHRT_MAX - *v); + } else { + uint8_t *v = (uint8_t *)pixels + i * bands + c; + *v = (uint8_t)(UCHAR_MAX - *v); + } + } +} + static void psd_expand_palette(const VipsForeignLoadPsd *psd, VipsPel *row, int width) { const uint8_t *palette = psd->info.color_mode_data.data; @@ -389,6 +414,8 @@ static int psd_generate(VipsRegion *region, void *seq, void *a, void *b, gboolea row = top + (size_t)y * skip; if (psd->indexed) { psd_expand_palette(psd, row, rect->width); + } else if (psd->info.color_mode == MINIPSD_MODE_CMYK) { + psd_invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); } else if (psd->info.color_mode == MINIPSD_MODE_BITMAP) { /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ for (x = 0; x < rect->width; ++x) row[x] = row[x] ? 0 : UCHAR_MAX; @@ -694,6 +721,7 @@ static int vips_foreign_save_psd_build(VipsObject *object) in = t[0]; } if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; + if (doc.color_mode == MINIPSD_MODE_CMYK) psd_invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); memset(&layer, 0, sizeof layer); layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index 0491104..f4baf55 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -33,6 +33,7 @@ #define PIXELS (WIDTH * HEIGHT) #define RGBA 4 #define MAX_PLANES 5 /* CMYK and transparency */ +#define CMYK_COLORS 4 /* CMYK's colour channels */ #define DPI 300.0 #define ASPECT 0.9 #define MM_PER_INCH 25.4 @@ -47,6 +48,9 @@ #define HALF_OPAQUE 128 #define ZIP_LEVEL 6 #define COMPOSITE (-1) /* decode_image's layer: the composite */ +#define TALL_W 1024 /* the tall image: PSD's highest, 60 MB of grey and transparency; */ +#define TALL_H MINIPSD_MAX_SIDE /* libvips caches rows, so its memory follows the width */ +#define BOUND 8 /* read in bands, libvips holds less than an eighth of it */ #define STORED_RLE 1 /* a channel's stored compression markers */ #define STORED_ZIP 2 @@ -103,9 +107,30 @@ static int inflate_zlib(void *user, const uint8_t *src, size_t src_size, uint8_t return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; } +/* CMYK's ink the other way round, in place: PSD's inverted (full: no ink) to libvips' (0: no ink), + and back. 8- or 16-bit samples, the four colour channels of each pixel. */ +static void invert_cmyk(uint8_t *pixels, size_t count, unsigned bands, size_t sample) +{ + size_t i; + unsigned c; + for (i = 0; i < count; ++i) + for (c = 0; c < CMYK_COLORS; ++c) { + uint8_t *v = pixels + (i * bands + c) * sample; + if (sample == sizeof(uint16_t)) { + uint16_t w; + memcpy(&w, v, sizeof w); + w = (uint16_t)(USHRT_MAX - w); + memcpy(v, &w, sizeof w); + } else { + *v = (uint8_t)(UCHAR_MAX - *v); + } + } +} + /* The composite (layer -1) or a layer as minipsd decodes it, laid out as the loader gives it: interleaved, the colour channels and transparency (a layer's always) up to MINIPSD_PLANES, - bitmap's black 0 and white 255, indexed through its palette. NULL with the error in *rc when + bitmap's black 0 and white 255, indexed through its palette, CMYK's ink 0 for none. NULL with + the error in *rc when minipsd refuses it, or MINIPSD_ERR_SIZE for an empty layer, which the loader refuses too. */ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *bytes, int *rc) { @@ -178,6 +203,8 @@ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *b } if (info.color_mode == MINIPSD_MODE_BITMAP) for (i = 0; i < *bytes; ++i) pixels[i] = pixels[i] ? 0 : UCHAR_MAX; + if (info.color_mode == MINIPSD_MODE_CMYK) + invert_cmyk(pixels, (size_t)width * height, bands, info.depth == 16 ? sizeof(uint16_t) : 1); if (info.color_mode == MINIPSD_MODE_INDEXED) for (p = pixels, i = 0; i < (size_t)width * height; ++i, p += bands) { const unsigned k = p[0]; @@ -285,6 +312,8 @@ static void check_layers(const char *path, const minipsd_doc *doc, size_t sample want_bytes = (size_t)w * (size_t)h * sample * (doc->color_mode == MINIPSD_MODE_CMYK ? MAX_PLANES : doc->color_mode == MINIPSD_MODE_GREY ? 2u : RGBA); got = vips_image_write_to_memory(image, &bytes); + if (got && doc->color_mode == MINIPSD_MODE_CMYK && bytes == want_bytes) /* back to PSD's ink */ + invert_cmyk((uint8_t *)got, (size_t)w * (size_t)h, CMYK_COLORS + 1, sample); check(got && bytes == want_bytes && memcmp(got, l->pixels, bytes) == 0, "pixels as given", name); vips_error_clear(); g_free(got); @@ -487,6 +516,70 @@ static size_t layer_count(const void *file, size_t size) return minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK ? info.layer_count : 0; } +/* A tall image read sequentially is decoded in bands: libvips' tracked memory, its regions and + caches, stays far below the image's size (the file is mapped, which libvips does not count). + Run first, before anything else raises the high-water mark. */ +static void check_bounded(const char *dir) +{ + char path[PATH_MAX_SIZE]; + const size_t bytes = (size_t)TALL_W * TALL_H * 2; /* grey and transparency */ + uint8_t *composite = (uint8_t *)malloc(bytes); + out_buffer b = { NULL, 0, 0 }; + minipsd_doc doc; + VipsImage *image; + FILE *f; + double mean = 0.0; + size_t i; + if (!composite) { + check(0, "memory for the tall image", "bounded"); + return; + } + for (i = 0; i < bytes; ++i) composite[i] = (uint8_t)(i % 2 ? UCHAR_MAX : i / 2 % TALL_W); + memset(&doc, 0, sizeof doc); + doc.width = TALL_W, doc.height = TALL_H, doc.dpi = DPI; + doc.color_mode = MINIPSD_MODE_GREY; + doc.composite = composite; + check(minipsd_write(&doc, NULL, to_buffer, &b) == MINIPSD_OK, "tall image written", "bounded"); + free(composite); + snprintf(path, sizeof path, "%s/tall.psd", dir); + f = fopen(path, "wb"); + check(f && b.data && fwrite(b.data, 1, b.size, f) == b.size, "file written", path); + if (f) fclose(f); + free(b.data); + + image = vips_image_new_from_file(path, "access", VIPS_ACCESS_SEQUENTIAL, NULL); + check(image && vips_avg(image, &mean, NULL) == 0, "read through", path); + printf("tall image: %zu bytes decoded, libvips held at most %zu\n", bytes, vips_tracked_get_mem_highwater()); + check(vips_tracked_get_mem_highwater() < bytes / BOUND, "memory bounded by bands, not by the image", path); + vips_error_clear(); + if (image) g_object_unref(image); +} + +/* The composite's first pixel's sample in channel k, as the file stores it (minipsd alone, 8-bit); + -1 when it does not decode. */ +static int stored_sample(const char *path, unsigned k) +{ + uint8_t workspace[WORKSPACE_SIZE], *data, *pixels = NULL; + minipsd_reader *r; + minipsd_info info; + minipsd_target target; + minipsd_decode_opts opts; + size_t size = 0; + int sample = -1; + if (!(data = read_all(path, &size))) return -1; + if (minipsd_reader_init(&r, data, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK && info.depth == 8 + && (pixels = (uint8_t *)malloc((size_t)info.width * info.height * MINIPSD_PLANES))) { + minipsd_target_interleaved(&target, pixels, (size_t)info.width * MINIPSD_PLANES, MINIPSD_PLANES, 8); + memset(&opts, 0, sizeof opts); + opts.depth = 8; + opts.y_count = 1; + if (minipsd_decode_composite(r, &target, &opts) == MINIPSD_OK) sample = pixels[k]; + } + free(pixels); + free(data); + return sample; +} + /* The stored compression marker of the file's first layer's first channel; -1 without one. */ static int first_layer_compression(const char *path) { @@ -546,6 +639,7 @@ int main(int argc, char **argv) static uint16_t grey16[PIXELS * 2], rgba16[PIXELS * RGBA]; static uint8_t rgb8[PIXELS * 3]; static float rgbf[PIXELS * 3]; + static uint8_t cyan[PIXELS * CMYK_COLORS]; /* libvips' ink: full cyan, nothing else */ static const char junk[] = "8BPX, not a PSD"; char path[PATH_MAX_SIZE]; uint8_t transparency[MINIPSD_TRANSPARENCY_INDEX_SIZE]; @@ -564,6 +658,7 @@ int main(int argc, char **argv) vips_concurrency_set(THREADS); minipsd_vips_init(); minipsd_vips_init(); /* twice is harmless */ + check_bounded(argv[1]); for (i = 0; i < sizeof rgba; ++i) rgba[i] = (uint8_t)(i * 7); for (i = 0; i < sizeof cmyka; ++i) cmyka[i] = (uint8_t)(i * 5); @@ -576,6 +671,7 @@ int main(int argc, char **argv) for (i = 0; i < sizeof zip_b; ++i) zip_b[i] = (uint8_t)(i / 97); for (i = 0; i < sizeof rgb8; ++i) rgb8[i] = (uint8_t)(i * 17 + i / 255); for (i = 0; i < PIXELS * 3; ++i) rgbf[i] = (float)(i % 1001) / 1000.0f; + for (i = 0; i < PIXELS; ++i) cyan[i * CMYK_COLORS] = UCHAR_MAX; { /* 8-bit RGB with a layer, a profile and non-square pixels; as PSD and as PSB */ static const expected psd = { "rgb8", 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB, 8 }; @@ -697,6 +793,12 @@ int main(int argc, char **argv) check_saved(make_image(indices, 1, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_B_W), "save-grey8.psd", argv[1], 1, NULL, 0); check_saved(make_image(grey16, 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16), "save-greya16.psd", argv[1], 0, NULL, 0); check_saved(make_image(cmyka, 4, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_CMYK), "save-cmyk8.psd", argv[1], 1, NULL, 0); + check_saved(make_image(cyan, CMYK_COLORS, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_CMYK), "save-cyan.psd", argv[1], 1, NULL, 0); + snprintf(path, sizeof path, "%s/save-cyan.psd", argv[1]); + check(stored_sample(path, 0) == 0 && stored_sample(path, 1) == UCHAR_MAX, "full cyan stored as 0, no magenta as 255", path); + snprintf(path, sizeof path, "%s/save-cmyk8.psd", argv[1]); + check(stored_sample(path, 0) == UCHAR_MAX - cmyka[0] && stored_sample(path, 3) == UCHAR_MAX - cmyka[3], + "CMYK stored as PSD's inverted ink", path); check_saved(make_image(rgbf, 3, VIPS_FORMAT_FLOAT, VIPS_INTERPRETATION_scRGB), "save-float.psd", argv[1], 1, NULL, 0); check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 1, "zip", TRUE); snprintf(path, sizeof path, "%s/save-zip.psd", argv[1]); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8e9045a..ca63b82 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -220,7 +220,6 @@ set_tests_properties(fuzz.replay.example PROPERTIES FIXTURES_REQUIRED minipsd-ex # libvips, for what uses it (cmake/minipsd_libvips.cmake): both off by default, as libvips is # fetched (Windows) or must be installed (pkg-config). option(MINIPSD_VIPS_EXAMPLE "Build and run examples/vips_composite.c against libvips (fetched on Windows)" OFF) -option(MINIPSD_VIPS_PLUGIN "Build and test the libvips plugin in libvips-plugin/ (libvips fetched on Windows)" OFF) if(MINIPSD_VIPS_EXAMPLE OR MINIPSD_VIPS_PLUGIN) include("${PROJECT_SOURCE_DIR}/cmake/minipsd_libvips.cmake") endif() From fcd12d6bfa82f32cab5d186e96e2480c8d1d821d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 01:31:56 +0200 Subject: [PATCH 07/22] refactor(vips): one file pair a side, everything named minipsd_vips_* minipsd_vips.c had grown to about a thousand lines. The loader's classes go to minipsd_vips_psdload.c, the saver's to minipsd_vips_psdsave.c, each with a header declaring its types; what both share (constants, PSD_ARGS_*, the suffixes, class_properties, invert_cmyk) to minipsd_vips_util.c/.h, the helpers extern as minipsd_vips__*. minipsd_vips.c keeps only minipsd_vips_init. Each side compiles in only its half of minipsd: the loader with MINIPSD_NO_WRITER, the saver with MINIPSD_NO_READER. Functions and types take the plugin's prefix: minipsd_vips_psdload_* and minipsd_vips_psdsave_*, statics included, and the GTypes MinipsdVipsPsdload* and MinipsdVipsPsdsave*, so neither the type names nor the get_type symbols can meet a loader libvips may add itself. The operations keep their nicknames, psdload and psdsave. --- docs/CODE-ARCH.md | 6 +- libvips-plugin/CMakeLists.txt | 6 +- libvips-plugin/minipsd_vips.c | 943 +------------------------- libvips-plugin/minipsd_vips_psdload.c | 561 +++++++++++++++ libvips-plugin/minipsd_vips_psdload.h | 20 + libvips-plugin/minipsd_vips_psdsave.c | 350 ++++++++++ libvips-plugin/minipsd_vips_psdsave.h | 20 + libvips-plugin/minipsd_vips_util.c | 40 ++ libvips-plugin/minipsd_vips_util.h | 49 ++ libvips-plugin/test_vips.c | 6 +- 10 files changed, 1062 insertions(+), 939 deletions(-) create mode 100644 libvips-plugin/minipsd_vips_psdload.c create mode 100644 libvips-plugin/minipsd_vips_psdload.h create mode 100644 libvips-plugin/minipsd_vips_psdsave.c create mode 100644 libvips-plugin/minipsd_vips_psdsave.h create mode 100644 libvips-plugin/minipsd_vips_util.c create mode 100644 libvips-plugin/minipsd_vips_util.h diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index fc23a82..7b53ff0 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -116,7 +116,11 @@ examples/ the I/O implementations; built and run in CI, nev read_file.c whole file into memory, described, every layer and the composite decoded resave.c minipsd_read with stored, one layer changed, written back: the round trip libvips-plugin/ minipsd as libvips' loader and saver (psdload, psdsave), registered by the program - minipsd_vips.h, .c minipsd_vips_init and the six operations; minipsd compiled in, private + minipsd_vips.h the one public call, minipsd_vips_init + minipsd_vips.c minipsd_vips_init: registers both sides with libvips + minipsd_vips_psdload.c/.h psdload, _buffer, _source; minipsd's reader compiled in, private + minipsd_vips_psdsave.c/.h psdsave, _buffer, _target; minipsd's writer compiled in, private + minipsd_vips_util.c/.h internal: constants and helpers both sides share test_vips.c documents and files loaded and saved through libvips, against minipsd's decode cmake/ minipsd_libvips.cmake libvips and its zlib for the example and the plugin: fetched on Windows, else pkg-config diff --git a/libvips-plugin/CMakeLists.txt b/libvips-plugin/CMakeLists.txt index 7ede971..0d1e3fa 100644 --- a/libvips-plugin/CMakeLists.txt +++ b/libvips-plugin/CMakeLists.txt @@ -9,7 +9,11 @@ # compared with minipsd's own decode (test_vips.c) # --------------------------------------------------------------------------- -add_library(minipsd_vips STATIC minipsd_vips.c minipsd_vips.h) +add_library(minipsd_vips STATIC + minipsd_vips.c minipsd_vips.h + minipsd_vips_psdload.c minipsd_vips_psdload.h + minipsd_vips_psdsave.c minipsd_vips_psdsave.h + minipsd_vips_util.c minipsd_vips_util.h) add_library(minipsd::vips ALIAS minipsd_vips) target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd::minipsd minipsd_libvips_zlib) diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index 9cf537d..eab7561 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -1,940 +1,15 @@ /** * \file - * \brief minipsd as a libvips loader and saver: a PSD or PSB as a libvips image, and back. - * - * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer - * variants make one), so the file can move into libvips/foreign/ as it is. The file is mapped - * (vips_source_map) and read in place by minipsd's reader; nothing is copied. Pixels are decoded - * in bands of rows as libvips asks for them, from its threads at once. - * - * The image is the composite, or with `layer` one layer (its position in xoffset and yoffset), as - * the file stores it, no colour converted: the mode's colour channels, then transparency (a - * layer's always, opaque when it has none). What libvips has no interpretation for (Lab, - * multichannel) is MULTIBAND; the colour mode data is kept as "psd-color-mode-data". Indexed is - * the exception: libvips has no indexed image, so it comes through its palette as sRGB, as - * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). CMYK - * comes the other way round from PSD's inverted ink, and bitmap as B_W (black 0), as libvips - * holds them. ZIP layers - * are inflated by the zlib libvips links. - * - * The saver writes an image as a one-layer PSD (or PSB) with that layer as its composite: grey, RGB - * or CMYK, 8 or 16 bits, grey and RGB also 32, transparency kept or made opaque; the resolution, the - * ICC profile, EXIF, XMP and IPTC kept. The image is held whole in memory while it is written. - * - * minipsd is compiled in here, private (MINIPSD_STATIC), so a program that uses minipsd itself - * keeps its own. + * \brief minipsd_vips_init: registers psdload and psdsave with libvips. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha */ -#define MINIPSD_STATIC -#define MINIPSD_IMPLEMENTATION -#include "minipsd.h" - #include "minipsd_vips.h" -#include -#include - -#include -#include - -#define PSD_SIGNATURE "8BPS" -#define PSD_SIGNATURE_SIZE 4 -#define PSD_SNIFF_SIZE 6 /* the signature and the version (1 PSD, 2 PSB), big-endian */ -#define PSD_UNIT_CM 2 /* resolution info's hResUnit: pixels per cm, shown in cm */ -#define PSD_STORED_ZIP 2 /* a channel's stored compression marker: ZIP, and ... */ -#define PSD_STORED_ZIP_PRED 3 /* ... ZIP with prediction */ -#define PSD_PALETTE_ENTRIES 256 /* an indexed file's palette: 256 reds, 256 greens, 256 blues */ -#define PSD_RGB 3 -#define PSD_COMPOSITE (-1) /* the `layer` option's default: the composite */ -#define MM_PER_INCH 25.4 -#define PSD_CMYK 4 /* CMYK's colour channels */ - -/* libvips' VIPS_ARG_* macros pass vips_argument_get_id()'s int as a guint property id, and - G_STRUCT_OFFSET's glong as a guint offset, which -Wsign-conversion and -Wconversion report in - our code; only around them are they off. */ -#if defined(__GNUC__) -# define PSD_ARGS_BEGIN \ - _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wsign-conversion\"") \ - _Pragma("GCC diagnostic ignored \"-Wconversion\"") -# define PSD_ARGS_END _Pragma("GCC diagnostic pop") -#else -# define PSD_ARGS_BEGIN -# define PSD_ARGS_END -#endif - -/* --- The base class: a PSD read from a source --------------------------------------------- */ - -typedef struct _VipsForeignLoadPsd { - VipsForeignLoad parent_object; - int layer; /* the option: -1 the composite, else a layer, bottom first */ - VipsSource *source; /* set by the subclass's build */ - minipsd_reader *reader; - minipsd_info info; - minipsd_layer_view view; /* the layer, with layer >= 0 */ - uint32_t width, height; /* the image: the canvas, or the layer's rectangle */ - unsigned planes; /* what minipsd decodes: colour channels and transparency, - up to MINIPSD_PLANES */ - unsigned bands; /* what libvips gets: planes, or indexed's RGB(A) */ - int indexed; /* the palette makes the colour */ - int transparent; /* indexed: the transparent index (resource 1047), or -1 */ - size_t scratch_size; /* a ZIP layer's channels inflated whole; 0 without ZIP */ - double xres, yres; /* pixels per mm */ -} VipsForeignLoadPsd; - -typedef VipsForeignLoadClass VipsForeignLoadPsdClass; - -GType vips_foreign_load_psd_get_type(void); -G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadPsd, vips_foreign_load_psd, VIPS_TYPE_FOREIGN_LOAD) - -/* Do these bytes start a PSD or a PSB? */ -static int psd_is_a_bytes(const unsigned char *data, size_t size) -{ - return size >= PSD_SNIFF_SIZE && memcmp(data, PSD_SIGNATURE, PSD_SIGNATURE_SIZE) == 0 && data[4] == 0 - && (data[5] == MINIPSD_PSD || data[5] == MINIPSD_PSB); -} - -static gboolean psd_is_a_source(VipsSource *source) -{ - const unsigned char *data = vips_source_sniff(source, PSD_SNIFF_SIZE); - return data && psd_is_a_bytes(data, PSD_SNIFF_SIZE); -} - -/* GObject resets set_property and get_property in every class (g_object_base_class_init), so - each class with arguments of its own sets libvips' again. */ -static void psd_class_properties(void *class) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS(class); - gobject_class->set_property = vips_object_set_property; - gobject_class->get_property = vips_object_get_property; -} - -static void vips_foreign_load_psd_dispose(GObject *gobject) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)gobject; - VIPS_UNREF(psd->source); - G_OBJECT_CLASS(vips_foreign_load_psd_parent_class)->dispose(gobject); -} - -static VipsForeignFlags vips_foreign_load_psd_get_flags(VipsForeignLoad *load) -{ - (void)load; - return VIPS_FOREIGN_SEQUENTIAL; /* bands of rows; libvips keeps the image for random access */ -} - -static void psd_error(VipsForeignLoadPsd *psd, const char *message) -{ - vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", message); -} - -/* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ -static VipsBandFormat psd_format(const minipsd_info *info) -{ - return info->depth == 32 ? VIPS_FORMAT_FLOAT : info->depth == 16 ? VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR; -} - -/* What the samples are, as libvips names it; MULTIBAND where it has no name for the mode. */ -static VipsInterpretation psd_interpretation(const minipsd_info *info) -{ - switch (info->color_mode) { - case MINIPSD_MODE_RGB: - return info->depth == 32 ? VIPS_INTERPRETATION_scRGB - : info->depth == 16 ? VIPS_INTERPRETATION_RGB16 : VIPS_INTERPRETATION_sRGB; - case MINIPSD_MODE_INDEXED: - return VIPS_INTERPRETATION_sRGB; - case MINIPSD_MODE_BITMAP: - case MINIPSD_MODE_GREY: - case MINIPSD_MODE_DUOTONE: - return info->depth == 16 ? VIPS_INTERPRETATION_GREY16 : VIPS_INTERPRETATION_B_W; - case MINIPSD_MODE_CMYK: - return VIPS_INTERPRETATION_CMYK; - default: - return VIPS_INTERPRETATION_MULTIBAND; - } -} - -/* The image's fields: on out, and on the image load() generates. */ -static int psd_fields(VipsForeignLoadPsd *psd, VipsImage *image) -{ - vips_image_init_fields(image, (int)psd->width, (int)psd->height, (int)psd->bands, psd_format(&psd->info), - VIPS_CODING_NONE, psd_interpretation(&psd->info), psd->xres, psd->yres); - if (psd->layer >= 0) { - image->Xoffset = psd->view.left; - image->Yoffset = psd->view.top; - } - return vips_image_pipelinev(image, VIPS_DEMAND_STYLE_THINSTRIP, NULL); -} - -/* A resource's bytes as a blob of the image, when the file has it. */ -static void psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, const char *name) -{ - minipsd_resource res; - if (minipsd_reader_find_resource(psd->reader, id, &res) == MINIPSD_OK && res.data.size) - vips_image_set_blob_copy(((VipsForeignLoad *)psd)->out, name, res.data.data, res.data.size); -} - -/* Bytes to inflate the layer's ZIP channels whole, each in its own part (scratch_kept); 0 when - none of the planes is ZIP. */ -static size_t psd_scratch_size(const VipsForeignLoadPsd *psd) -{ - const size_t row = ((size_t)psd->width * psd->info.depth + 7) / 8; - unsigned i; - int zip = 0; - for (i = 0; i < psd->planes; ++i) - if (psd->view.channel[i].size >= 2 && psd->view.channel[i].data && psd->view.channel[i].data[0] == 0 - && (psd->view.channel[i].data[1] == PSD_STORED_ZIP || psd->view.channel[i].data[1] == PSD_STORED_ZIP_PRED)) - zip = 1; - return zip ? psd->planes * psd->height * row : 0; -} - -/* The layer the option names: its record, its size, its name and look as metadata. */ -static int psd_find_layer(VipsForeignLoadPsd *psd) -{ - VipsImage *out = ((VipsForeignLoad *)psd)->out; - char *name; - size_t needed = 0; - int rc, i; - for (i = 0; i <= psd->layer; ++i) - if ((rc = minipsd_reader_next_layer(psd->reader, &psd->view)) != MINIPSD_MORE) { - psd_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); - return -1; - } - if (psd->view.right <= psd->view.left || psd->view.bottom <= psd->view.top) { - psd_error(psd, "the layer is empty"); - return -1; - } - psd->width = (uint32_t)(psd->view.right - psd->view.left); - psd->height = (uint32_t)(psd->view.bottom - psd->view.top); - psd->planes = VIPS_MIN(psd->info.color_channels + 1u, MINIPSD_PLANES); - - if (minipsd_layer_name(&psd->view, NULL, 0, &needed) == MINIPSD_MORE - && (name = (char *)vips_malloc(VIPS_OBJECT(psd), needed)) - && minipsd_layer_name(&psd->view, name, needed, &needed) == MINIPSD_OK) - vips_image_set_string(out, "psd-layer-name", name); - vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); - vips_image_set_int(out, "psd-layer-opacity", psd->view.opacity); - vips_image_set_int(out, "psd-layer-hidden", psd->view.hidden); - return 0; -} - -static int vips_foreign_load_psd_header(VipsForeignLoad *load) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; - minipsd_resource res; - minipsd_resolution_info resolution; - minipsd_version_info version; - uint16_t index; - double xres, yres; - const char *unit = "in"; - const void *data; - void *workspace; - size_t size, workspace_size; - int rc; - - if (!(data = vips_source_map(psd->source, &size))) return -1; - if (minipsd_reader_workspace_size(&workspace_size) != MINIPSD_OK - || !(workspace = vips_malloc(VIPS_OBJECT(load), workspace_size))) - return -1; - rc = minipsd_reader_init(&psd->reader, data, size, workspace, workspace_size, NULL, &psd->info); - if (rc != MINIPSD_OK) { - psd_error(psd, minipsd_error_string(rc)); - return -1; - } - - psd->width = psd->info.width; - psd->height = psd->info.height; - /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ - psd->planes = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); - if (psd->layer >= 0 && psd_find_layer(psd)) return -1; - psd->scratch_size = psd->layer >= 0 ? psd_scratch_size(psd) : 0; - - /* Indexed: the palette's RGB, and transparency where resource 1047 names an index. */ - psd->bands = psd->planes; - psd->transparent = -1; - psd->indexed = psd->info.color_mode == MINIPSD_MODE_INDEXED; - if (psd->indexed) { - if (psd->info.color_mode_data.size < PSD_RGB * PSD_PALETTE_ENTRIES || !psd->info.color_mode_data.data) { - psd_error(psd, "an indexed file without its palette"); - return -1; - } - if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_TRANSPARENCY_INDEX, &res) == MINIPSD_OK - && minipsd_resource_transparency_index(&res, &index) == MINIPSD_OK && index < PSD_PALETTE_ENTRIES) - psd->transparent = index; - psd->bands = PSD_RGB + (psd->transparent >= 0 ? 1u : 0u); - } - - /* The resolution: resource 1005 whole, or none (libvips' default). */ - xres = yres = psd->info.dpi; - if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_RESOLUTION_INFO, &res) == MINIPSD_OK - && minipsd_resource_resolution_info(&res, &resolution) == MINIPSD_OK) { - xres = resolution.horizontal_resolution; - yres = resolution.vertical_resolution; - if (resolution.horizontal_resolution_unit == PSD_UNIT_CM) unit = "cm"; - } - psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; - psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; - if (psd_fields(psd, load->out)) return -1; - if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); - - vips_image_set_int(load->out, VIPS_META_BITS_PER_SAMPLE, psd->info.depth); - if (psd->info.icc.size) - vips_image_set_blob_copy(load->out, VIPS_META_ICC_NAME, psd->info.icc.data, psd->info.icc.size); - psd_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); - psd_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); - psd_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); - if (psd->info.color_mode_data.size) - vips_image_set_blob_copy(load->out, "psd-color-mode-data", psd->info.color_mode_data.data, - psd->info.color_mode_data.size); - vips_image_set_double(load->out, "psd-pixel-aspect-ratio", psd->info.pixel_aspect_ratio); - vips_image_set_int(load->out, "psd-layer-count", (int)psd->info.layer_count); - /* Without version info (before Photoshop 6) the composite is real. */ - vips_image_set_int(load->out, "psd-has-real-merged-data", - minipsd_reader_find_resource(psd->reader, MINIPSD_RES_VERSION_INFO, &res) != MINIPSD_OK - || minipsd_resource_version_info(&res, &version) != MINIPSD_OK - || version.has_real_merged_data); - return 0; -} - -/* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ -static int psd_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) -{ - uLongf n = (uLongf)dst_size; - (void)user; - if ((uLong)src_size != src_size || n != dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ - return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; -} - -/* A thread's sequence: the scratch a ZIP layer's channels stay inflated in, between its bands. */ -typedef struct psd_sequence { - uint8_t *scratch; - int kept; -} psd_sequence; - -static void *psd_start(VipsImage *out, void *a, void *b) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; - psd_sequence *seq = g_new0(psd_sequence, 1); - (void)out, (void)b; - /* ponytail: every thread inflates a ZIP layer whole into its own scratch; one shared scratch - under a lock would bound memory at one copy, if layers that large turn up. */ - if (psd->scratch_size && !(seq->scratch = (uint8_t *)g_try_malloc(psd->scratch_size))) { - g_free(seq); - return NULL; - } - return seq; -} - -static int psd_stop(void *seq, void *a, void *b) -{ - (void)a, (void)b; - g_free(((psd_sequence *)seq)->scratch); - g_free(seq); - return 0; -} - -/* Indexed, in place: each pixel's index (in its first band) becomes the palette's RGB, and alpha - 0 at the transparent index. */ -/* CMYK the other way round, in place: PSD stores ink inverted (full: no ink), libvips holds it - as TIFF does (0: no ink), as its own jpegload makes Adobe's inverted CMYK and ImageMagick - makes PSD's. The colour channels only, not transparency; both ways, loading and saving. */ -static void psd_invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) -{ - size_t i; - unsigned c; - for (i = 0; i < count; ++i) - for (c = 0; c < PSD_CMYK; ++c) { - if (depth == 32) { - float *v = (float *)pixels + i * bands + c; - *v = 1.0f - *v; - } else if (depth == 16) { - uint16_t *v = (uint16_t *)pixels + i * bands + c; - *v = (uint16_t)(USHRT_MAX - *v); - } else { - uint8_t *v = (uint8_t *)pixels + i * bands + c; - *v = (uint8_t)(UCHAR_MAX - *v); - } - } -} - -static void psd_expand_palette(const VipsForeignLoadPsd *psd, VipsPel *row, int width) -{ - const uint8_t *palette = psd->info.color_mode_data.data; - int x; - for (x = 0; x < width; ++x, row += psd->bands) { - const unsigned index = row[0]; - row[0] = palette[index]; - row[1] = palette[PSD_PALETTE_ENTRIES + index]; - row[2] = palette[2 * PSD_PALETTE_ENTRIES + index]; - if (psd->transparent >= 0) row[PSD_RGB] = (int)index == psd->transparent ? 0 : UCHAR_MAX; - } -} - -/* One band of rows, decoded straight into the region. vips_sequential asks for whole rows; calls - run at once from libvips' threads on the one reader, which minipsd allows on a whole file. */ -static int psd_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; - psd_sequence *sequence = (psd_sequence *)seq; - const VipsRect *rect = ®ion->valid; - minipsd_decode_opts opts; - minipsd_target target; - VipsPel *top = VIPS_REGION_ADDR_TOPLEFT(region), *row; - const size_t skip = VIPS_REGION_LSKIP(region); - int rc, x, y; - (void)b, (void)stop; - - if (rect->left != 0 || rect->width != region->im->Xsize) { - psd_error(psd, "only whole rows are decoded"); - return -1; - } - memset(&opts, 0, sizeof opts); - opts.y_first = (uint32_t)rect->top; - opts.y_count = (uint32_t)rect->height; - opts.depth = psd->info.depth; - minipsd_target_interleaved(&target, top, skip, psd->planes, psd->info.depth); - if (psd->indexed) target.channel[0].pixel_step = (ptrdiff_t)psd->bands; /* the index, in R */ - if (psd->layer < 0) { - rc = minipsd_decode_composite(psd->reader, &target, &opts); - } else { - opts.inflate = psd_inflate; - opts.scratch = sequence->scratch; - opts.scratch_size = psd->scratch_size; - opts.scratch_kept = sequence->kept; - rc = minipsd_decode_layer(psd->reader, &psd->view, &target, &opts); - sequence->kept = rc == MINIPSD_OK && sequence->scratch; - } - if (rc != MINIPSD_OK) { - psd_error(psd, minipsd_error_string(rc)); - return -1; - } - - for (y = 0; y < rect->height; ++y) { - row = top + (size_t)y * skip; - if (psd->indexed) { - psd_expand_palette(psd, row, rect->width); - } else if (psd->info.color_mode == MINIPSD_MODE_CMYK) { - psd_invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); - } else if (psd->info.color_mode == MINIPSD_MODE_BITMAP) { - /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ - for (x = 0; x < rect->width; ++x) row[x] = row[x] ? 0 : UCHAR_MAX; - } - } - return 0; -} - -static int vips_foreign_load_psd_load(VipsForeignLoad *load) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; - VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(load), 2); - t[0] = vips_image_new(); - if (psd_fields(psd, t[0]) || vips_image_generate(t[0], psd_start, psd_generate, psd_stop, psd, NULL) - || vips_sequential(t[0], &t[1], NULL) || vips_image_write(t[1], load->real)) - return -1; - return 0; -} - -static void vips_foreign_load_psd_class_init(VipsForeignLoadPsdClass *class) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS(class); - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - - gobject_class->dispose = vips_foreign_load_psd_dispose; - psd_class_properties(class); - - object_class->nickname = "psdload_base"; - object_class->description = "load PSD and PSB with minipsd"; - - load_class->get_flags = vips_foreign_load_psd_get_flags; - load_class->header = vips_foreign_load_psd_header; - load_class->load = vips_foreign_load_psd_load; - - PSD_ARGS_BEGIN - VIPS_ARG_INT(class, "layer", 20, "Layer", "Load this layer (bottom first), -1 the composite", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignLoadPsd, layer), PSD_COMPOSITE, INT_MAX, - PSD_COMPOSITE); - PSD_ARGS_END -} - -static void vips_foreign_load_psd_init(VipsForeignLoadPsd *psd) -{ - psd->layer = PSD_COMPOSITE; -} - -static const char *psd_suffs[] = { ".psd", ".psb", NULL }; - -/* --- psdload_source ----------------------------------------------------------------------- */ - -typedef struct _VipsForeignLoadPsdSource { - VipsForeignLoadPsd parent_object; - VipsSource *source; -} VipsForeignLoadPsdSource; - -typedef VipsForeignLoadPsdClass VipsForeignLoadPsdSourceClass; - -GType vips_foreign_load_psd_source_get_type(void); -G_DEFINE_TYPE(VipsForeignLoadPsdSource, vips_foreign_load_psd_source, vips_foreign_load_psd_get_type()) - -static int vips_foreign_load_psd_source_build(VipsObject *object) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; - VipsForeignLoadPsdSource *source = (VipsForeignLoadPsdSource *)object; - if (source->source) { - psd->source = source->source; - g_object_ref(psd->source); - } - return VIPS_OBJECT_CLASS(vips_foreign_load_psd_source_parent_class)->build(object); -} - -static void vips_foreign_load_psd_source_class_init(VipsForeignLoadPsdSourceClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class); - VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - - psd_class_properties(class); - object_class->nickname = "psdload_source"; - object_class->description = "load PSD and PSB from a source with minipsd"; - object_class->build = vips_foreign_load_psd_source_build; - operation_class->flags |= VIPS_OPERATION_NOCACHE; - load_class->is_a_source = psd_is_a_source; - - PSD_ARGS_BEGIN - VIPS_ARG_OBJECT(class, "source", 1, "Source", "Source to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignLoadPsdSource, source), VIPS_TYPE_SOURCE); - PSD_ARGS_END -} - -static void vips_foreign_load_psd_source_init(VipsForeignLoadPsdSource *source) -{ - (void)source; -} - -/* --- psdload (a file) ---------------------------------------------------------------------- */ - -typedef struct _VipsForeignLoadPsdFile { - VipsForeignLoadPsd parent_object; - char *filename; -} VipsForeignLoadPsdFile; - -typedef VipsForeignLoadPsdClass VipsForeignLoadPsdFileClass; - -GType vips_foreign_load_psd_file_get_type(void); -G_DEFINE_TYPE(VipsForeignLoadPsdFile, vips_foreign_load_psd_file, vips_foreign_load_psd_get_type()) - -static gboolean psd_is_a_file(const char *filename) -{ - VipsSource *source = vips_source_new_from_file(filename); - gboolean is_a; - if (!source) return FALSE; - is_a = psd_is_a_source(source); - g_object_unref(source); - return is_a; -} - -static int vips_foreign_load_psd_file_build(VipsObject *object) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; - VipsForeignLoadPsdFile *file = (VipsForeignLoadPsdFile *)object; - if (file->filename && !(psd->source = vips_source_new_from_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_load_psd_file_parent_class)->build(object); -} - -static void vips_foreign_load_psd_file_class_init(VipsForeignLoadPsdFileClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - - psd_class_properties(class); - object_class->nickname = "psdload"; - object_class->description = "load PSD and PSB with minipsd"; - object_class->build = vips_foreign_load_psd_file_build; - foreign_class->suffs = psd_suffs; - load_class->is_a = psd_is_a_file; - - PSD_ARGS_BEGIN - VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignLoadPsdFile, filename), NULL); - PSD_ARGS_END -} - -static void vips_foreign_load_psd_file_init(VipsForeignLoadPsdFile *file) -{ - (void)file; -} - -/* --- psdload_buffer ------------------------------------------------------------------------ */ - -typedef struct _VipsForeignLoadPsdBuffer { - VipsForeignLoadPsd parent_object; - VipsBlob *blob; -} VipsForeignLoadPsdBuffer; - -typedef VipsForeignLoadPsdClass VipsForeignLoadPsdBufferClass; - -GType vips_foreign_load_psd_buffer_get_type(void); -G_DEFINE_TYPE(VipsForeignLoadPsdBuffer, vips_foreign_load_psd_buffer, vips_foreign_load_psd_get_type()) - -static gboolean psd_is_a_buffer(const void *data, size_t size) -{ - return psd_is_a_bytes((const unsigned char *)data, size); -} - -static int vips_foreign_load_psd_buffer_build(VipsObject *object) -{ - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; - VipsForeignLoadPsdBuffer *buffer = (VipsForeignLoadPsdBuffer *)object; - if (buffer->blob && !(psd->source = vips_source_new_from_blob(buffer->blob))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_load_psd_buffer_parent_class)->build(object); -} - -static void vips_foreign_load_psd_buffer_class_init(VipsForeignLoadPsdBufferClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - - psd_class_properties(class); - object_class->nickname = "psdload_buffer"; - object_class->description = "load PSD and PSB from a buffer with minipsd"; - object_class->build = vips_foreign_load_psd_buffer_build; - load_class->is_a_buffer = psd_is_a_buffer; - - PSD_ARGS_BEGIN - VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignLoadPsdBuffer, blob), VIPS_TYPE_BLOB); - PSD_ARGS_END -} - -static void vips_foreign_load_psd_buffer_init(VipsForeignLoadPsdBuffer *buffer) -{ - (void)buffer; -} - -/* --- The saver's base class: an image as a one-layer PSD into a target --------------------- */ - -#define PSD_ZIP_LEVEL 6 /* zlib's default */ -#define PSD_DEFAULT_DPI 72.0 /* when the image's resolution is none PSD can hold */ -#define PSD_MAX_DPI 65536.0 /* PSD's resolution is a 16.16 fixed-point number */ -#define PSD_LAYER_NAME "Layer 1" -#define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ - -typedef struct _VipsForeignSavePsd { - VipsForeignSave parent_object; - VipsTarget *target; /* set by the subclass's build */ - gboolean psb; /* the option; also a .psb file, or a side past PSD's */ - gboolean zip; /* the option: ZIP the layer's channels */ - char *layer_name; /* the option */ -} VipsForeignSavePsd; - -typedef VipsForeignSaveClass VipsForeignSavePsdClass; - -GType vips_foreign_save_psd_get_type(void); -G_DEFINE_ABSTRACT_TYPE(VipsForeignSavePsd, vips_foreign_save_psd, VIPS_TYPE_FOREIGN_SAVE) - -/* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ -static VipsBandFormat psd_save_formats[] = { - /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ - VIPS_FORMAT_UCHAR, VIPS_FORMAT_UCHAR, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, - VIPS_FORMAT_USHORT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT -}; - -static void vips_foreign_save_psd_dispose(GObject *gobject) -{ - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)gobject; - VIPS_UNREF(psd->target); - G_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->dispose(gobject); -} - -static int psd_to_target(void *user, const void *data, size_t size) -{ - return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ -} - -static int psd_deflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) -{ - uLongf n = (uLongf)*dst_size; - int rc; - (void)user; - if ((uLong)src_size != src_size || n != *dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ - rc = compress2(dst, &n, src, (uLong)src_size, PSD_ZIP_LEVEL); - *dst_size = n; - return rc != Z_OK; -} - -/* A blob of the image as a resource to keep, when it has one. */ -static void psd_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, size_t *count) -{ - const void *data; - size_t size; - if (!vips_image_get_typeof(image, name) || vips_image_get_blob(image, name, &data, &size) || !size) return; - memset(&res[*count], 0, sizeof res[*count]); - res[*count].id = id; - res[*count].data.data = (const uint8_t *)data; - res[*count].data.size = size; - ++*count; -} - -static int vips_foreign_save_psd_build(VipsObject *object) -{ - VipsForeignSave *save = (VipsForeignSave *)object; - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; - VipsImage **t = (VipsImage **)vips_object_local_array(object, 1); - VipsImage *in; - minipsd_resource resources[PSD_MAX_RESOURCES]; - minipsd_layer layer; - minipsd_doc doc; - minipsd_write_opts opts; - const void *icc; - const char *filename; - size_t icc_size, bytes, colors, count = 0; - double ratio, full; - void *pixels; - int rc; - - if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->build(object)) return -1; - in = save->ready; - - /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ - memset(&doc, 0, sizeof doc); - if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == 4 || in->Bands == 5)) { - doc.color_mode = MINIPSD_MODE_CMYK; - colors = 4; - } else if (in->Bands == 3 || in->Bands == 4) { - doc.color_mode = MINIPSD_MODE_RGB; - colors = PSD_RGB; - } else if (in->Bands == 1 || in->Bands == 2) { - doc.color_mode = MINIPSD_MODE_GREY; - colors = 1; - } else { - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); - return -1; - } - doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; - - /* A layer always has transparency: opaque, when the image has none. */ - if ((size_t)in->Bands == colors) { - full = doc.depth == 32 ? 1.0 : doc.depth == 16 ? USHRT_MAX : UCHAR_MAX; - if (vips_bandjoin_const1(in, &t[0], full, NULL)) return -1; - in = t[0]; - } - if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; - if (doc.color_mode == MINIPSD_MODE_CMYK) psd_invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); - - memset(&layer, 0, sizeof layer); - layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; - layer.pixels = (const uint8_t *)pixels; - layer.opacity = UCHAR_MAX; - - doc.width = (uint32_t)in->Xsize; - doc.height = (uint32_t)in->Ysize; - doc.dpi = in->Xres * MM_PER_INCH; - if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; - doc.layers = &layer; - doc.layer_count = 1; - doc.composite = (const uint8_t *)pixels; /* one layer, at full opacity: the composite itself */ - doc.compression = psd->zip ? MINIPSD_ZIP : MINIPSD_RLE; - /* PSB by the option, a file named .psb (libvips writes files through a target too), or a side - past PSD's. */ - filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); - doc.format = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE - || in->Ysize > MINIPSD_MAX_SIDE - ? MINIPSD_PSB - : MINIPSD_PSD; - if (vips_image_get_typeof(in, VIPS_META_ICC_NAME) && !vips_image_get_blob(in, VIPS_META_ICC_NAME, &icc, &icc_size)) { - doc.icc = (const uint8_t *)icc; - doc.icc_size = icc_size; - } - if (vips_image_get_typeof(in, "psd-pixel-aspect-ratio") && !vips_image_get_double(in, "psd-pixel-aspect-ratio", &ratio) - && ratio > 0.0) - doc.pixel_aspect_ratio = ratio; - psd_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); - psd_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); - psd_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); - doc.resources = resources; - doc.resource_count = count; - - memset(&opts, 0, sizeof opts); - opts.deflate = psd_deflate; - rc = minipsd_write(&doc, &opts, psd_to_target, psd->target); - g_free(pixels); - if (rc != MINIPSD_OK) { - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); - return -1; - } - return vips_target_end(psd->target); -} - -static void vips_foreign_save_psd_class_init(VipsForeignSavePsdClass *class) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS(class); - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignSaveClass *save_class = (VipsForeignSaveClass *)class; - - gobject_class->dispose = vips_foreign_save_psd_dispose; - psd_class_properties(class); - - object_class->nickname = "psdsave_base"; - object_class->description = "save PSD and PSB with minipsd"; - object_class->build = vips_foreign_save_psd_build; - - /* Any bands: the build takes grey, RGB and CMYK, with or without alpha, and refuses the rest. */ -#if VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 17) - save_class->saveable = VIPS_FOREIGN_SAVEABLE_ANY; -#else - save_class->saveable = VIPS_SAVEABLE_ANY; -#endif - save_class->format_table = psd_save_formats; - - PSD_ARGS_BEGIN - VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, psb), FALSE); - VIPS_ARG_BOOL(class, "zip", 21, "ZIP", "Compress the layer with ZIP instead of RLE", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, zip), FALSE); - VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, layer_name), PSD_LAYER_NAME); - PSD_ARGS_END -} - -static void vips_foreign_save_psd_init(VipsForeignSavePsd *psd) -{ - (void)psd; -} - -/* --- psdsave_target ------------------------------------------------------------------------ */ - -typedef struct _VipsForeignSavePsdTarget { - VipsForeignSavePsd parent_object; - VipsTarget *target; -} VipsForeignSavePsdTarget; - -typedef VipsForeignSavePsdClass VipsForeignSavePsdTargetClass; - -GType vips_foreign_save_psd_target_get_type(void); -G_DEFINE_TYPE(VipsForeignSavePsdTarget, vips_foreign_save_psd_target, vips_foreign_save_psd_get_type()) - -static int vips_foreign_save_psd_target_build(VipsObject *object) -{ - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; - VipsForeignSavePsdTarget *target = (VipsForeignSavePsdTarget *)object; - if (target->target) { - psd->target = target->target; - g_object_ref(psd->target); - } - return VIPS_OBJECT_CLASS(vips_foreign_save_psd_target_parent_class)->build(object); -} - -static void vips_foreign_save_psd_target_class_init(VipsForeignSavePsdTargetClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - - psd_class_properties(class); - object_class->nickname = "psdsave_target"; - object_class->description = "save PSD and PSB to a target with minipsd"; - object_class->build = vips_foreign_save_psd_target_build; - foreign_class->suffs = psd_suffs; - - PSD_ARGS_BEGIN - VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdTarget, target), VIPS_TYPE_TARGET); - PSD_ARGS_END -} - -static void vips_foreign_save_psd_target_init(VipsForeignSavePsdTarget *target) -{ - (void)target; -} - -/* --- psdsave (a file) ---------------------------------------------------------------------- */ - -typedef struct _VipsForeignSavePsdFile { - VipsForeignSavePsd parent_object; - char *filename; -} VipsForeignSavePsdFile; - -typedef VipsForeignSavePsdClass VipsForeignSavePsdFileClass; - -GType vips_foreign_save_psd_file_get_type(void); -G_DEFINE_TYPE(VipsForeignSavePsdFile, vips_foreign_save_psd_file, vips_foreign_save_psd_get_type()) - -static int vips_foreign_save_psd_file_build(VipsObject *object) -{ - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; - VipsForeignSavePsdFile *file = (VipsForeignSavePsdFile *)object; - if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_save_psd_file_parent_class)->build(object); -} - -static void vips_foreign_save_psd_file_class_init(VipsForeignSavePsdFileClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - - psd_class_properties(class); - object_class->nickname = "psdsave"; - object_class->description = "save PSD and PSB to a file with minipsd"; - object_class->build = vips_foreign_save_psd_file_build; - foreign_class->suffs = psd_suffs; - - PSD_ARGS_BEGIN - VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdFile, filename), NULL); - PSD_ARGS_END -} - -static void vips_foreign_save_psd_file_init(VipsForeignSavePsdFile *file) -{ - (void)file; -} - -/* --- psdsave_buffer ------------------------------------------------------------------------ */ - -typedef struct _VipsForeignSavePsdBuffer { - VipsForeignSavePsd parent_object; - VipsArea *buf; -} VipsForeignSavePsdBuffer; - -typedef VipsForeignSavePsdClass VipsForeignSavePsdBufferClass; - -GType vips_foreign_save_psd_buffer_get_type(void); -G_DEFINE_TYPE(VipsForeignSavePsdBuffer, vips_foreign_save_psd_buffer, vips_foreign_save_psd_get_type()) - -static int vips_foreign_save_psd_buffer_build(VipsObject *object) -{ - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; - VipsBlob *blob; - if (!(psd->target = vips_target_new_to_memory())) return -1; - if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_buffer_parent_class)->build(object)) return -1; - g_object_get(psd->target, "blob", &blob, NULL); - g_object_set(object, "buffer", blob, NULL); - vips_area_unref(VIPS_AREA(blob)); - return 0; -} - -static void vips_foreign_save_psd_buffer_class_init(VipsForeignSavePsdBufferClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - - psd_class_properties(class); - object_class->nickname = "psdsave_buffer"; - object_class->description = "save PSD and PSB to a buffer with minipsd"; - object_class->build = vips_foreign_save_psd_buffer_build; - foreign_class->suffs = psd_suffs; - - PSD_ARGS_BEGIN - VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdBuffer, buf), VIPS_TYPE_BLOB); - PSD_ARGS_END -} - -static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) -{ - (void)buffer; -} +#include "minipsd_vips_psdload.h" +#include "minipsd_vips_psdsave.h" /* --- Registration -------------------------------------------------------------------------- */ @@ -942,11 +17,11 @@ int minipsd_vips_init(void) { /* Registering a type is enough: libvips finds loaders and savers among VipsForeignLoad's and VipsForeignSave's subclasses. */ - (void)vips_foreign_load_psd_file_get_type(); - (void)vips_foreign_load_psd_buffer_get_type(); - (void)vips_foreign_load_psd_source_get_type(); - (void)vips_foreign_save_psd_file_get_type(); - (void)vips_foreign_save_psd_buffer_get_type(); - (void)vips_foreign_save_psd_target_get_type(); + (void)minipsd_vips_psdload_file_get_type(); + (void)minipsd_vips_psdload_buffer_get_type(); + (void)minipsd_vips_psdload_source_get_type(); + (void)minipsd_vips_psdsave_file_get_type(); + (void)minipsd_vips_psdsave_buffer_get_type(); + (void)minipsd_vips_psdsave_target_get_type(); return 0; } diff --git a/libvips-plugin/minipsd_vips_psdload.c b/libvips-plugin/minipsd_vips_psdload.c new file mode 100644 index 0000000..8188094 --- /dev/null +++ b/libvips-plugin/minipsd_vips_psdload.c @@ -0,0 +1,561 @@ +/** + * \file + * \brief psdload, psdload_buffer, psdload_source: a PSD or PSB as a libvips image. + * + * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer + * variants make one), so the code can move into libvips/foreign/ (as psdload.c, the minipsd_vips_ + * names back to vips_foreign_). The file is mapped (vips_source_map) and read in place by + * minipsd's reader; nothing is copied. + * Pixels are decoded in bands of rows as libvips asks for them, from its threads at once. + * + * The image is the composite, or with `layer` one layer (its position in xoffset and yoffset), as + * the file stores it, no colour converted: the mode's colour channels, then transparency (a + * layer's always, opaque when it has none). What libvips has no interpretation for (Lab, + * multichannel) is MULTIBAND; the colour mode data is kept as "psd-color-mode-data". Indexed is + * the exception: libvips has no indexed image, so it comes through its palette as sRGB, as + * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). CMYK + * comes the other way round from PSD's inverted ink, and bitmap as B_W (black 0), as libvips + * holds them. ZIP layers are inflated by the zlib libvips links. + * + * minipsd's reader is compiled in here, private (MINIPSD_STATIC; the writer left out), so a + * program that uses minipsd itself keeps its own. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#define MINIPSD_STATIC +#define MINIPSD_NO_WRITER +#define MINIPSD_IMPLEMENTATION +#include "minipsd.h" + +#include "minipsd_vips_psdload.h" +#include "minipsd_vips_util.h" + +#include + +#include +#include + +#define PSD_SIGNATURE "8BPS" +#define PSD_SIGNATURE_SIZE 4 +#define PSD_SNIFF_SIZE 6 /* the signature and the version (1 PSD, 2 PSB), big-endian */ +#define PSD_UNIT_CM 2 /* resolution info's hResUnit: pixels per cm, shown in cm */ +#define PSD_STORED_ZIP 2 /* a channel's stored compression marker: ZIP, and ... */ +#define PSD_STORED_ZIP_PRED 3 /* ... ZIP with prediction */ +#define PSD_PALETTE_ENTRIES 256 /* an indexed file's palette: 256 reds, 256 greens, 256 blues */ +#define PSD_COMPOSITE (-1) /* the `layer` option's default: the composite */ + +/* --- The base class: a PSD read from a source --------------------------------------------- */ + +typedef struct _MinipsdVipsPsdload { + VipsForeignLoad parent_object; + int layer; /* the option: -1 the composite, else a layer, bottom first */ + VipsSource *source; /* set by the subclass's build */ + minipsd_reader *reader; + minipsd_info info; + minipsd_layer_view view; /* the layer, with layer >= 0 */ + uint32_t width, height; /* the image: the canvas, or the layer's rectangle */ + unsigned planes; /* what minipsd decodes: colour channels and transparency, + up to MINIPSD_PLANES */ + unsigned bands; /* what libvips gets: planes, or indexed's RGB(A) */ + int indexed; /* the palette makes the colour */ + int transparent; /* indexed: the transparent index (resource 1047), or -1 */ + size_t scratch_size; /* a ZIP layer's channels inflated whole; 0 without ZIP */ + double xres, yres; /* pixels per mm */ +} MinipsdVipsPsdload; + +typedef VipsForeignLoadClass MinipsdVipsPsdloadClass; + +G_DEFINE_ABSTRACT_TYPE(MinipsdVipsPsdload, minipsd_vips_psdload, VIPS_TYPE_FOREIGN_LOAD) + +/* Do these bytes start a PSD or a PSB? */ +static int minipsd_vips_psdload_is_a_bytes(const unsigned char *data, size_t size) +{ + return size >= PSD_SNIFF_SIZE && memcmp(data, PSD_SIGNATURE, PSD_SIGNATURE_SIZE) == 0 && data[4] == 0 + && (data[5] == MINIPSD_PSD || data[5] == MINIPSD_PSB); +} + +static gboolean minipsd_vips_psdload_is_a_source(VipsSource *source) +{ + const unsigned char *data = vips_source_sniff(source, PSD_SNIFF_SIZE); + return data && minipsd_vips_psdload_is_a_bytes(data, PSD_SNIFF_SIZE); +} + +static void minipsd_vips_psdload_dispose(GObject *gobject) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)gobject; + VIPS_UNREF(psd->source); + G_OBJECT_CLASS(minipsd_vips_psdload_parent_class)->dispose(gobject); +} + +static VipsForeignFlags minipsd_vips_psdload_get_flags(VipsForeignLoad *load) +{ + (void)load; + return VIPS_FOREIGN_SEQUENTIAL; /* bands of rows; libvips keeps the image for random access */ +} + +static void minipsd_vips_psdload_error(MinipsdVipsPsdload *psd, const char *message) +{ + vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", message); +} + +/* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ +static VipsBandFormat minipsd_vips_psdload_format(const minipsd_info *info) +{ + return info->depth == 32 ? VIPS_FORMAT_FLOAT : info->depth == 16 ? VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR; +} + +/* What the samples are, as libvips names it; MULTIBAND where it has no name for the mode. */ +static VipsInterpretation minipsd_vips_psdload_interpretation(const minipsd_info *info) +{ + switch (info->color_mode) { + case MINIPSD_MODE_RGB: + return info->depth == 32 ? VIPS_INTERPRETATION_scRGB + : info->depth == 16 ? VIPS_INTERPRETATION_RGB16 : VIPS_INTERPRETATION_sRGB; + case MINIPSD_MODE_INDEXED: + return VIPS_INTERPRETATION_sRGB; + case MINIPSD_MODE_BITMAP: + case MINIPSD_MODE_GREY: + case MINIPSD_MODE_DUOTONE: + return info->depth == 16 ? VIPS_INTERPRETATION_GREY16 : VIPS_INTERPRETATION_B_W; + case MINIPSD_MODE_CMYK: + return VIPS_INTERPRETATION_CMYK; + default: + return VIPS_INTERPRETATION_MULTIBAND; + } +} + +/* The image's fields: on out, and on the image load() generates. */ +static int minipsd_vips_psdload_fields(MinipsdVipsPsdload *psd, VipsImage *image) +{ + vips_image_init_fields(image, (int)psd->width, (int)psd->height, (int)psd->bands, + minipsd_vips_psdload_format(&psd->info), VIPS_CODING_NONE, + minipsd_vips_psdload_interpretation(&psd->info), psd->xres, psd->yres); + if (psd->layer >= 0) { + image->Xoffset = psd->view.left; + image->Yoffset = psd->view.top; + } + return vips_image_pipelinev(image, VIPS_DEMAND_STYLE_THINSTRIP, NULL); +} + +/* A resource's bytes as a blob of the image, when the file has it. */ +static void minipsd_vips_psdload_set_blob(MinipsdVipsPsdload *psd, uint16_t id, const char *name) +{ + minipsd_resource res; + if (minipsd_reader_find_resource(psd->reader, id, &res) == MINIPSD_OK && res.data.size) + vips_image_set_blob_copy(((VipsForeignLoad *)psd)->out, name, res.data.data, res.data.size); +} + +/* Bytes to inflate the layer's ZIP channels whole, each in its own part (scratch_kept); 0 when + none of the planes is ZIP. */ +static size_t minipsd_vips_psdload_scratch_size(const MinipsdVipsPsdload *psd) +{ + const size_t row = ((size_t)psd->width * psd->info.depth + 7) / 8; + unsigned i; + int zip = 0; + for (i = 0; i < psd->planes; ++i) + if (psd->view.channel[i].size >= 2 && psd->view.channel[i].data && psd->view.channel[i].data[0] == 0 + && (psd->view.channel[i].data[1] == PSD_STORED_ZIP || psd->view.channel[i].data[1] == PSD_STORED_ZIP_PRED)) + zip = 1; + return zip ? psd->planes * psd->height * row : 0; +} + +/* The layer the option names: its record, its size, its name and look as metadata. */ +static int minipsd_vips_psdload_find_layer(MinipsdVipsPsdload *psd) +{ + VipsImage *out = ((VipsForeignLoad *)psd)->out; + char *name; + size_t needed = 0; + int rc, i; + for (i = 0; i <= psd->layer; ++i) + if ((rc = minipsd_reader_next_layer(psd->reader, &psd->view)) != MINIPSD_MORE) { + minipsd_vips_psdload_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); + return -1; + } + if (psd->view.right <= psd->view.left || psd->view.bottom <= psd->view.top) { + minipsd_vips_psdload_error(psd, "the layer is empty"); + return -1; + } + psd->width = (uint32_t)(psd->view.right - psd->view.left); + psd->height = (uint32_t)(psd->view.bottom - psd->view.top); + psd->planes = VIPS_MIN(psd->info.color_channels + 1u, MINIPSD_PLANES); + + if (minipsd_layer_name(&psd->view, NULL, 0, &needed) == MINIPSD_MORE + && (name = (char *)vips_malloc(VIPS_OBJECT(psd), needed)) + && minipsd_layer_name(&psd->view, name, needed, &needed) == MINIPSD_OK) + vips_image_set_string(out, "psd-layer-name", name); + vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); + vips_image_set_int(out, "psd-layer-opacity", psd->view.opacity); + vips_image_set_int(out, "psd-layer-hidden", psd->view.hidden); + return 0; +} + +static int minipsd_vips_psdload_header(VipsForeignLoad *load) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)load; + minipsd_resource res; + minipsd_resolution_info resolution; + minipsd_version_info version; + uint16_t index; + double xres, yres; + const char *unit = "in"; + const void *data; + void *workspace; + size_t size, workspace_size; + int rc; + + if (!(data = vips_source_map(psd->source, &size))) return -1; + if (minipsd_reader_workspace_size(&workspace_size) != MINIPSD_OK + || !(workspace = vips_malloc(VIPS_OBJECT(load), workspace_size))) + return -1; + rc = minipsd_reader_init(&psd->reader, data, size, workspace, workspace_size, NULL, &psd->info); + if (rc != MINIPSD_OK) { + minipsd_vips_psdload_error(psd, minipsd_error_string(rc)); + return -1; + } + + psd->width = psd->info.width; + psd->height = psd->info.height; + /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ + psd->planes = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); + if (psd->layer >= 0 && minipsd_vips_psdload_find_layer(psd)) return -1; + psd->scratch_size = psd->layer >= 0 ? minipsd_vips_psdload_scratch_size(psd) : 0; + + /* Indexed: the palette's RGB, and transparency where resource 1047 names an index. */ + psd->bands = psd->planes; + psd->transparent = -1; + psd->indexed = psd->info.color_mode == MINIPSD_MODE_INDEXED; + if (psd->indexed) { + if (psd->info.color_mode_data.size < PSD_RGB * PSD_PALETTE_ENTRIES || !psd->info.color_mode_data.data) { + minipsd_vips_psdload_error(psd, "an indexed file without its palette"); + return -1; + } + if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_TRANSPARENCY_INDEX, &res) == MINIPSD_OK + && minipsd_resource_transparency_index(&res, &index) == MINIPSD_OK && index < PSD_PALETTE_ENTRIES) + psd->transparent = index; + psd->bands = PSD_RGB + (psd->transparent >= 0 ? 1u : 0u); + } + + /* The resolution: resource 1005 whole, or none (libvips' default). */ + xres = yres = psd->info.dpi; + if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_RESOLUTION_INFO, &res) == MINIPSD_OK + && minipsd_resource_resolution_info(&res, &resolution) == MINIPSD_OK) { + xres = resolution.horizontal_resolution; + yres = resolution.vertical_resolution; + if (resolution.horizontal_resolution_unit == PSD_UNIT_CM) unit = "cm"; + } + psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; + psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; + if (minipsd_vips_psdload_fields(psd, load->out)) return -1; + if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); + + vips_image_set_int(load->out, VIPS_META_BITS_PER_SAMPLE, psd->info.depth); + if (psd->info.icc.size) + vips_image_set_blob_copy(load->out, VIPS_META_ICC_NAME, psd->info.icc.data, psd->info.icc.size); + minipsd_vips_psdload_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); + minipsd_vips_psdload_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); + minipsd_vips_psdload_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); + if (psd->info.color_mode_data.size) + vips_image_set_blob_copy(load->out, "psd-color-mode-data", psd->info.color_mode_data.data, + psd->info.color_mode_data.size); + vips_image_set_double(load->out, "psd-pixel-aspect-ratio", psd->info.pixel_aspect_ratio); + vips_image_set_int(load->out, "psd-layer-count", (int)psd->info.layer_count); + /* Without version info (before Photoshop 6) the composite is real. */ + vips_image_set_int(load->out, "psd-has-real-merged-data", + minipsd_reader_find_resource(psd->reader, MINIPSD_RES_VERSION_INFO, &res) != MINIPSD_OK + || minipsd_resource_version_info(&res, &version) != MINIPSD_OK + || version.has_real_merged_data); + return 0; +} + +/* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ +static int minipsd_vips_psdload_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) +{ + uLongf n = (uLongf)dst_size; + (void)user; + if ((uLong)src_size != src_size || n != dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ + return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; +} + +/* A thread's sequence: the scratch a ZIP layer's channels stay inflated in, between its bands. */ +typedef struct minipsd_vips_psdload_sequence { + uint8_t *scratch; + int kept; +} minipsd_vips_psdload_sequence; + +static void *minipsd_vips_psdload_start(VipsImage *out, void *a, void *b) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)a; + minipsd_vips_psdload_sequence *seq = g_new0(minipsd_vips_psdload_sequence, 1); + (void)out, (void)b; + /* ponytail: every thread inflates a ZIP layer whole into its own scratch; one shared scratch + under a lock would bound memory at one copy, if layers that large turn up. */ + if (psd->scratch_size && !(seq->scratch = (uint8_t *)g_try_malloc(psd->scratch_size))) { + g_free(seq); + return NULL; + } + return seq; +} + +static int minipsd_vips_psdload_stop(void *seq, void *a, void *b) +{ + (void)a, (void)b; + g_free(((minipsd_vips_psdload_sequence *)seq)->scratch); + g_free(seq); + return 0; +} + +/* Indexed, in place: each pixel's index (in its first band) becomes the palette's RGB, and alpha + 0 at the transparent index. */ +static void minipsd_vips_psdload_expand_palette(const MinipsdVipsPsdload *psd, VipsPel *row, int width) +{ + const uint8_t *palette = psd->info.color_mode_data.data; + int x; + for (x = 0; x < width; ++x, row += psd->bands) { + const unsigned index = row[0]; + row[0] = palette[index]; + row[1] = palette[PSD_PALETTE_ENTRIES + index]; + row[2] = palette[2 * PSD_PALETTE_ENTRIES + index]; + if (psd->transparent >= 0) row[PSD_RGB] = (int)index == psd->transparent ? 0 : UCHAR_MAX; + } +} + +/* One band of rows, decoded straight into the region. vips_sequential asks for whole rows; calls + run at once from libvips' threads on the one reader, which minipsd allows on a whole file. */ +static int minipsd_vips_psdload_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)a; + minipsd_vips_psdload_sequence *sequence = (minipsd_vips_psdload_sequence *)seq; + const VipsRect *rect = ®ion->valid; + minipsd_decode_opts opts; + minipsd_target target; + VipsPel *top = VIPS_REGION_ADDR_TOPLEFT(region), *row; + const size_t skip = VIPS_REGION_LSKIP(region); + int rc, x, y; + (void)b, (void)stop; + + if (rect->left != 0 || rect->width != region->im->Xsize) { + minipsd_vips_psdload_error(psd, "only whole rows are decoded"); + return -1; + } + memset(&opts, 0, sizeof opts); + opts.y_first = (uint32_t)rect->top; + opts.y_count = (uint32_t)rect->height; + opts.depth = psd->info.depth; + minipsd_target_interleaved(&target, top, skip, psd->planes, psd->info.depth); + if (psd->indexed) target.channel[0].pixel_step = (ptrdiff_t)psd->bands; /* the index, in R */ + if (psd->layer < 0) { + rc = minipsd_decode_composite(psd->reader, &target, &opts); + } else { + opts.inflate = minipsd_vips_psdload_inflate; + opts.scratch = sequence->scratch; + opts.scratch_size = psd->scratch_size; + opts.scratch_kept = sequence->kept; + rc = minipsd_decode_layer(psd->reader, &psd->view, &target, &opts); + sequence->kept = rc == MINIPSD_OK && sequence->scratch; + } + if (rc != MINIPSD_OK) { + minipsd_vips_psdload_error(psd, minipsd_error_string(rc)); + return -1; + } + + for (y = 0; y < rect->height; ++y) { + row = top + (size_t)y * skip; + if (psd->indexed) { + minipsd_vips_psdload_expand_palette(psd, row, rect->width); + } else if (psd->info.color_mode == MINIPSD_MODE_CMYK) { + minipsd_vips__invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); + } else if (psd->info.color_mode == MINIPSD_MODE_BITMAP) { + /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ + for (x = 0; x < rect->width; ++x) row[x] = row[x] ? 0 : UCHAR_MAX; + } + } + return 0; +} + +static int minipsd_vips_psdload_load(VipsForeignLoad *load) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)load; + VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(load), 2); + t[0] = vips_image_new(); + if (minipsd_vips_psdload_fields(psd, t[0]) + || vips_image_generate(t[0], minipsd_vips_psdload_start, minipsd_vips_psdload_generate, + minipsd_vips_psdload_stop, psd, NULL) + || vips_sequential(t[0], &t[1], NULL) || vips_image_write(t[1], load->real)) + return -1; + return 0; +} + +static void minipsd_vips_psdload_class_init(MinipsdVipsPsdloadClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + gobject_class->dispose = minipsd_vips_psdload_dispose; + minipsd_vips__class_properties(class); + + object_class->nickname = "psdload_base"; + object_class->description = "load PSD and PSB with minipsd"; + + load_class->get_flags = minipsd_vips_psdload_get_flags; + load_class->header = minipsd_vips_psdload_header; + load_class->load = minipsd_vips_psdload_load; + + PSD_ARGS_BEGIN + VIPS_ARG_INT(class, "layer", 20, "Layer", "Load this layer (bottom first), -1 the composite", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdload, layer), PSD_COMPOSITE, INT_MAX, + PSD_COMPOSITE); + PSD_ARGS_END +} + +static void minipsd_vips_psdload_init(MinipsdVipsPsdload *psd) +{ + psd->layer = PSD_COMPOSITE; +} + +/* --- psdload_source ----------------------------------------------------------------------- */ + +typedef struct _MinipsdVipsPsdloadSource { + MinipsdVipsPsdload parent_object; + VipsSource *source; +} MinipsdVipsPsdloadSource; + +typedef MinipsdVipsPsdloadClass MinipsdVipsPsdloadSourceClass; + +G_DEFINE_TYPE(MinipsdVipsPsdloadSource, minipsd_vips_psdload_source, minipsd_vips_psdload_get_type()) + +static int minipsd_vips_psdload_source_build(VipsObject *object) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)object; + MinipsdVipsPsdloadSource *source = (MinipsdVipsPsdloadSource *)object; + if (source->source) { + psd->source = source->source; + g_object_ref(psd->source); + } + return VIPS_OBJECT_CLASS(minipsd_vips_psdload_source_parent_class)->build(object); +} + +static void minipsd_vips_psdload_source_class_init(MinipsdVipsPsdloadSourceClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class); + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + minipsd_vips__class_properties(class); + object_class->nickname = "psdload_source"; + object_class->description = "load PSD and PSB from a source with minipsd"; + object_class->build = minipsd_vips_psdload_source_build; + operation_class->flags |= VIPS_OPERATION_NOCACHE; + load_class->is_a_source = minipsd_vips_psdload_is_a_source; + + PSD_ARGS_BEGIN + VIPS_ARG_OBJECT(class, "source", 1, "Source", "Source to load from", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(MinipsdVipsPsdloadSource, source), VIPS_TYPE_SOURCE); + PSD_ARGS_END +} + +static void minipsd_vips_psdload_source_init(MinipsdVipsPsdloadSource *source) +{ + (void)source; +} + +/* --- psdload (a file) ---------------------------------------------------------------------- */ + +typedef struct _MinipsdVipsPsdloadFile { + MinipsdVipsPsdload parent_object; + char *filename; +} MinipsdVipsPsdloadFile; + +typedef MinipsdVipsPsdloadClass MinipsdVipsPsdloadFileClass; + +G_DEFINE_TYPE(MinipsdVipsPsdloadFile, minipsd_vips_psdload_file, minipsd_vips_psdload_get_type()) + +static gboolean minipsd_vips_psdload_is_a_file(const char *filename) +{ + VipsSource *source = vips_source_new_from_file(filename); + gboolean is_a; + if (!source) return FALSE; + is_a = minipsd_vips_psdload_is_a_source(source); + g_object_unref(source); + return is_a; +} + +static int minipsd_vips_psdload_file_build(VipsObject *object) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)object; + MinipsdVipsPsdloadFile *file = (MinipsdVipsPsdloadFile *)object; + if (file->filename && !(psd->source = vips_source_new_from_file(file->filename))) return -1; + return VIPS_OBJECT_CLASS(minipsd_vips_psdload_file_parent_class)->build(object); +} + +static void minipsd_vips_psdload_file_class_init(MinipsdVipsPsdloadFileClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + minipsd_vips__class_properties(class); + object_class->nickname = "psdload"; + object_class->description = "load PSD and PSB with minipsd"; + object_class->build = minipsd_vips_psdload_file_build; + foreign_class->suffs = minipsd_vips__suffs; + load_class->is_a = minipsd_vips_psdload_is_a_file; + + PSD_ARGS_BEGIN + VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to load from", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(MinipsdVipsPsdloadFile, filename), NULL); + PSD_ARGS_END +} + +static void minipsd_vips_psdload_file_init(MinipsdVipsPsdloadFile *file) +{ + (void)file; +} + +/* --- psdload_buffer ------------------------------------------------------------------------ */ + +typedef struct _MinipsdVipsPsdloadBuffer { + MinipsdVipsPsdload parent_object; + VipsBlob *blob; +} MinipsdVipsPsdloadBuffer; + +typedef MinipsdVipsPsdloadClass MinipsdVipsPsdloadBufferClass; + +G_DEFINE_TYPE(MinipsdVipsPsdloadBuffer, minipsd_vips_psdload_buffer, minipsd_vips_psdload_get_type()) + +static gboolean minipsd_vips_psdload_is_a_buffer(const void *data, size_t size) +{ + return minipsd_vips_psdload_is_a_bytes((const unsigned char *)data, size); +} + +static int minipsd_vips_psdload_buffer_build(VipsObject *object) +{ + MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)object; + MinipsdVipsPsdloadBuffer *buffer = (MinipsdVipsPsdloadBuffer *)object; + if (buffer->blob && !(psd->source = vips_source_new_from_blob(buffer->blob))) return -1; + return VIPS_OBJECT_CLASS(minipsd_vips_psdload_buffer_parent_class)->build(object); +} + +static void minipsd_vips_psdload_buffer_class_init(MinipsdVipsPsdloadBufferClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; + + minipsd_vips__class_properties(class); + object_class->nickname = "psdload_buffer"; + object_class->description = "load PSD and PSB from a buffer with minipsd"; + object_class->build = minipsd_vips_psdload_buffer_build; + load_class->is_a_buffer = minipsd_vips_psdload_is_a_buffer; + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to load from", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(MinipsdVipsPsdloadBuffer, blob), VIPS_TYPE_BLOB); + PSD_ARGS_END +} + +static void minipsd_vips_psdload_buffer_init(MinipsdVipsPsdloadBuffer *buffer) +{ + (void)buffer; +} diff --git a/libvips-plugin/minipsd_vips_psdload.h b/libvips-plugin/minipsd_vips_psdload.h new file mode 100644 index 0000000..5b2a475 --- /dev/null +++ b/libvips-plugin/minipsd_vips_psdload.h @@ -0,0 +1,20 @@ +/** + * \file + * \brief Internal to the plugin: the psdload operations' types (minipsd_vips_psdload.c). + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#ifndef MINIPSD_VIPS_PSDLOAD_H +#define MINIPSD_VIPS_PSDLOAD_H + +#include + +/* The abstract base, then the three made from it; minipsd_vips_init registers the three. */ +GType minipsd_vips_psdload_get_type(void); +GType minipsd_vips_psdload_source_get_type(void); +GType minipsd_vips_psdload_file_get_type(void); +GType minipsd_vips_psdload_buffer_get_type(void); + +#endif /* MINIPSD_VIPS_PSDLOAD_H */ diff --git a/libvips-plugin/minipsd_vips_psdsave.c b/libvips-plugin/minipsd_vips_psdsave.c new file mode 100644 index 0000000..6919a43 --- /dev/null +++ b/libvips-plugin/minipsd_vips_psdsave.c @@ -0,0 +1,350 @@ +/** + * \file + * \brief psdsave, psdsave_buffer, psdsave_target: an image as a one-layer PSD or PSB. + * + * Laid out as libvips' own savers are (a base class over a VipsTarget; the file and buffer + * variants make one), so the code can move into libvips/foreign/ (as psdsave.c, the minipsd_vips_ + * names back to vips_foreign_). The image is written as one layer that is also the composite: + * grey, RGB or CMYK, 8 or 16 bits, grey and RGB also 32, transparency kept or made opaque, CMYK + * back to PSD's inverted ink; the resolution, the ICC profile, EXIF, XMP and IPTC kept. The image + * is held whole in memory while it is written. + * + * minipsd's writer is compiled in here, private (MINIPSD_STATIC; the reader left out), so a + * program that uses minipsd itself keeps its own. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#define MINIPSD_STATIC +#define MINIPSD_NO_READER +#define MINIPSD_IMPLEMENTATION +#include "minipsd.h" + +#include "minipsd_vips_psdsave.h" +#include "minipsd_vips_util.h" + +#include + +#include +#include + +/* --- The saver's base class: an image as a one-layer PSD into a target --------------------- */ + +#define PSD_ZIP_LEVEL 6 /* zlib's default */ +#define PSD_DEFAULT_DPI 72.0 /* when the image's resolution is none PSD can hold */ +#define PSD_MAX_DPI 65536.0 /* PSD's resolution is a 16.16 fixed-point number */ +#define PSD_LAYER_NAME "Layer 1" +#define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ + +typedef struct _MinipsdVipsPsdsave { + VipsForeignSave parent_object; + VipsTarget *target; /* set by the subclass's build */ + gboolean psb; /* the option; also a .psb file, or a side past PSD's */ + gboolean zip; /* the option: ZIP the layer's channels */ + char *layer_name; /* the option */ +} MinipsdVipsPsdsave; + +typedef VipsForeignSaveClass MinipsdVipsPsdsaveClass; + +G_DEFINE_ABSTRACT_TYPE(MinipsdVipsPsdsave, minipsd_vips_psdsave, VIPS_TYPE_FOREIGN_SAVE) + +/* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ +static VipsBandFormat minipsd_vips_psdsave_save_formats[] = { + /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ + VIPS_FORMAT_UCHAR, VIPS_FORMAT_UCHAR, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, + VIPS_FORMAT_USHORT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT +}; + +static void minipsd_vips_psdsave_dispose(GObject *gobject) +{ + MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)gobject; + VIPS_UNREF(psd->target); + G_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->dispose(gobject); +} + +static int minipsd_vips_psdsave_to_target(void *user, const void *data, size_t size) +{ + return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ +} + +static int minipsd_vips_psdsave_deflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) +{ + uLongf n = (uLongf)*dst_size; + int rc; + (void)user; + if ((uLong)src_size != src_size || n != *dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ + rc = compress2(dst, &n, src, (uLong)src_size, PSD_ZIP_LEVEL); + *dst_size = n; + return rc != Z_OK; +} + +/* A blob of the image as a resource to keep, when it has one. */ +static void minipsd_vips_psdsave_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, + size_t *count) +{ + const void *data; + size_t size; + if (!vips_image_get_typeof(image, name) || vips_image_get_blob(image, name, &data, &size) || !size) return; + memset(&res[*count], 0, sizeof res[*count]); + res[*count].id = id; + res[*count].data.data = (const uint8_t *)data; + res[*count].data.size = size; + ++*count; +} + +static int minipsd_vips_psdsave_build(VipsObject *object) +{ + VipsForeignSave *save = (VipsForeignSave *)object; + MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; + VipsImage **t = (VipsImage **)vips_object_local_array(object, 1); + VipsImage *in; + minipsd_resource resources[PSD_MAX_RESOURCES]; + minipsd_layer layer; + minipsd_doc doc; + minipsd_write_opts opts; + const void *icc; + const char *filename; + size_t icc_size, bytes, colors, count = 0; + double ratio, full; + void *pixels; + int rc; + + if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->build(object)) return -1; + in = save->ready; + + /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ + memset(&doc, 0, sizeof doc); + if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == 4 || in->Bands == 5)) { + doc.color_mode = MINIPSD_MODE_CMYK; + colors = 4; + } else if (in->Bands == 3 || in->Bands == 4) { + doc.color_mode = MINIPSD_MODE_RGB; + colors = PSD_RGB; + } else if (in->Bands == 1 || in->Bands == 2) { + doc.color_mode = MINIPSD_MODE_GREY; + colors = 1; + } else { + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); + return -1; + } + doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; + + /* A layer always has transparency: opaque, when the image has none. */ + if ((size_t)in->Bands == colors) { + full = doc.depth == 32 ? 1.0 : doc.depth == 16 ? USHRT_MAX : UCHAR_MAX; + if (vips_bandjoin_const1(in, &t[0], full, NULL)) return -1; + in = t[0]; + } + if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; + if (doc.color_mode == MINIPSD_MODE_CMYK) minipsd_vips__invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); + + memset(&layer, 0, sizeof layer); + layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; + layer.pixels = (const uint8_t *)pixels; + layer.opacity = UCHAR_MAX; + + doc.width = (uint32_t)in->Xsize; + doc.height = (uint32_t)in->Ysize; + doc.dpi = in->Xres * MM_PER_INCH; + if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; + doc.layers = &layer; + doc.layer_count = 1; + doc.composite = (const uint8_t *)pixels; /* one layer, at full opacity: the composite itself */ + doc.compression = psd->zip ? MINIPSD_ZIP : MINIPSD_RLE; + /* PSB by the option, a file named .psb (libvips writes files through a target too), or a side + past PSD's. */ + filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); + doc.format = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE + || in->Ysize > MINIPSD_MAX_SIDE + ? MINIPSD_PSB + : MINIPSD_PSD; + if (vips_image_get_typeof(in, VIPS_META_ICC_NAME) && !vips_image_get_blob(in, VIPS_META_ICC_NAME, &icc, &icc_size)) { + doc.icc = (const uint8_t *)icc; + doc.icc_size = icc_size; + } + if (vips_image_get_typeof(in, "psd-pixel-aspect-ratio") && !vips_image_get_double(in, "psd-pixel-aspect-ratio", &ratio) + && ratio > 0.0) + doc.pixel_aspect_ratio = ratio; + minipsd_vips_psdsave_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); + minipsd_vips_psdsave_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); + minipsd_vips_psdsave_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); + doc.resources = resources; + doc.resource_count = count; + + memset(&opts, 0, sizeof opts); + opts.deflate = minipsd_vips_psdsave_deflate; + rc = minipsd_write(&doc, &opts, minipsd_vips_psdsave_to_target, psd->target); + g_free(pixels); + if (rc != MINIPSD_OK) { + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); + return -1; + } + return vips_target_end(psd->target); +} + +static void minipsd_vips_psdsave_class_init(MinipsdVipsPsdsaveClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignSaveClass *save_class = (VipsForeignSaveClass *)class; + + gobject_class->dispose = minipsd_vips_psdsave_dispose; + minipsd_vips__class_properties(class); + + object_class->nickname = "psdsave_base"; + object_class->description = "save PSD and PSB with minipsd"; + object_class->build = minipsd_vips_psdsave_build; + + /* Any bands: the build takes grey, RGB and CMYK, with or without alpha, and refuses the rest. */ +#if VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 17) + save_class->saveable = VIPS_FOREIGN_SAVEABLE_ANY; +#else + save_class->saveable = VIPS_SAVEABLE_ANY; +#endif + save_class->format_table = minipsd_vips_psdsave_save_formats; + + PSD_ARGS_BEGIN + VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, psb), FALSE); + VIPS_ARG_BOOL(class, "zip", 21, "ZIP", "Compress the layer with ZIP instead of RLE", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, zip), FALSE); + VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, layer_name), PSD_LAYER_NAME); + PSD_ARGS_END +} + +static void minipsd_vips_psdsave_init(MinipsdVipsPsdsave *psd) +{ + (void)psd; +} + +/* --- psdsave_target ------------------------------------------------------------------------ */ + +typedef struct _MinipsdVipsPsdsaveTarget { + MinipsdVipsPsdsave parent_object; + VipsTarget *target; +} MinipsdVipsPsdsaveTarget; + +typedef MinipsdVipsPsdsaveClass MinipsdVipsPsdsaveTargetClass; + +G_DEFINE_TYPE(MinipsdVipsPsdsaveTarget, minipsd_vips_psdsave_target, minipsd_vips_psdsave_get_type()) + +static int minipsd_vips_psdsave_target_build(VipsObject *object) +{ + MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; + MinipsdVipsPsdsaveTarget *target = (MinipsdVipsPsdsaveTarget *)object; + if (target->target) { + psd->target = target->target; + g_object_ref(psd->target); + } + return VIPS_OBJECT_CLASS(minipsd_vips_psdsave_target_parent_class)->build(object); +} + +static void minipsd_vips_psdsave_target_class_init(MinipsdVipsPsdsaveTargetClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + minipsd_vips__class_properties(class); + object_class->nickname = "psdsave_target"; + object_class->description = "save PSD and PSB to a target with minipsd"; + object_class->build = minipsd_vips_psdsave_target_build; + foreign_class->suffs = minipsd_vips__suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(MinipsdVipsPsdsaveTarget, target), VIPS_TYPE_TARGET); + PSD_ARGS_END +} + +static void minipsd_vips_psdsave_target_init(MinipsdVipsPsdsaveTarget *target) +{ + (void)target; +} + +/* --- psdsave (a file) ---------------------------------------------------------------------- */ + +typedef struct _MinipsdVipsPsdsaveFile { + MinipsdVipsPsdsave parent_object; + char *filename; +} MinipsdVipsPsdsaveFile; + +typedef MinipsdVipsPsdsaveClass MinipsdVipsPsdsaveFileClass; + +G_DEFINE_TYPE(MinipsdVipsPsdsaveFile, minipsd_vips_psdsave_file, minipsd_vips_psdsave_get_type()) + +static int minipsd_vips_psdsave_file_build(VipsObject *object) +{ + MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; + MinipsdVipsPsdsaveFile *file = (MinipsdVipsPsdsaveFile *)object; + if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; + return VIPS_OBJECT_CLASS(minipsd_vips_psdsave_file_parent_class)->build(object); +} + +static void minipsd_vips_psdsave_file_class_init(MinipsdVipsPsdsaveFileClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + minipsd_vips__class_properties(class); + object_class->nickname = "psdsave"; + object_class->description = "save PSD and PSB to a file with minipsd"; + object_class->build = minipsd_vips_psdsave_file_build; + foreign_class->suffs = minipsd_vips__suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(MinipsdVipsPsdsaveFile, filename), NULL); + PSD_ARGS_END +} + +static void minipsd_vips_psdsave_file_init(MinipsdVipsPsdsaveFile *file) +{ + (void)file; +} + +/* --- psdsave_buffer ------------------------------------------------------------------------ */ + +typedef struct _MinipsdVipsPsdsaveBuffer { + MinipsdVipsPsdsave parent_object; + VipsArea *buf; +} MinipsdVipsPsdsaveBuffer; + +typedef MinipsdVipsPsdsaveClass MinipsdVipsPsdsaveBufferClass; + +G_DEFINE_TYPE(MinipsdVipsPsdsaveBuffer, minipsd_vips_psdsave_buffer, minipsd_vips_psdsave_get_type()) + +static int minipsd_vips_psdsave_buffer_build(VipsObject *object) +{ + MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; + VipsBlob *blob; + if (!(psd->target = vips_target_new_to_memory())) return -1; + if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_buffer_parent_class)->build(object)) return -1; + g_object_get(psd->target, "blob", &blob, NULL); + g_object_set(object, "buffer", blob, NULL); + vips_area_unref(VIPS_AREA(blob)); + return 0; +} + +static void minipsd_vips_psdsave_buffer_class_init(MinipsdVipsPsdsaveBufferClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + minipsd_vips__class_properties(class); + object_class->nickname = "psdsave_buffer"; + object_class->description = "save PSD and PSB to a buffer with minipsd"; + object_class->build = minipsd_vips_psdsave_buffer_build; + foreign_class->suffs = minipsd_vips__suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, + G_STRUCT_OFFSET(MinipsdVipsPsdsaveBuffer, buf), VIPS_TYPE_BLOB); + PSD_ARGS_END +} + +static void minipsd_vips_psdsave_buffer_init(MinipsdVipsPsdsaveBuffer *buffer) +{ + (void)buffer; +} diff --git a/libvips-plugin/minipsd_vips_psdsave.h b/libvips-plugin/minipsd_vips_psdsave.h new file mode 100644 index 0000000..c4c7161 --- /dev/null +++ b/libvips-plugin/minipsd_vips_psdsave.h @@ -0,0 +1,20 @@ +/** + * \file + * \brief Internal to the plugin: the psdsave operations' types (minipsd_vips_psdsave.c). + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#ifndef MINIPSD_VIPS_PSDSAVE_H +#define MINIPSD_VIPS_PSDSAVE_H + +#include + +/* The abstract base, then the three made from it; minipsd_vips_init registers the three. */ +GType minipsd_vips_psdsave_get_type(void); +GType minipsd_vips_psdsave_target_get_type(void); +GType minipsd_vips_psdsave_file_get_type(void); +GType minipsd_vips_psdsave_buffer_get_type(void); + +#endif /* MINIPSD_VIPS_PSDSAVE_H */ diff --git a/libvips-plugin/minipsd_vips_util.c b/libvips-plugin/minipsd_vips_util.c new file mode 100644 index 0000000..5629166 --- /dev/null +++ b/libvips-plugin/minipsd_vips_util.c @@ -0,0 +1,40 @@ +/** + * \file + * \brief The helpers minipsd_vips_util.h declares. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#include "minipsd_vips_util.h" + +#include +#include + +const char *minipsd_vips__suffs[] = { ".psd", ".psb", NULL }; + +void minipsd_vips__class_properties(void *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; +} + +void minipsd_vips__invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) +{ + size_t i; + unsigned c; + for (i = 0; i < count; ++i) + for (c = 0; c < PSD_CMYK; ++c) { + if (depth == 32) { + float *v = (float *)pixels + i * bands + c; + *v = 1.0f - *v; + } else if (depth == 16) { + uint16_t *v = (uint16_t *)pixels + i * bands + c; + *v = (uint16_t)(USHRT_MAX - *v); + } else { + uint8_t *v = (uint8_t *)pixels + i * bands + c; + *v = (uint8_t)(UCHAR_MAX - *v); + } + } +} diff --git a/libvips-plugin/minipsd_vips_util.h b/libvips-plugin/minipsd_vips_util.h new file mode 100644 index 0000000..a1a9f30 --- /dev/null +++ b/libvips-plugin/minipsd_vips_util.h @@ -0,0 +1,49 @@ +/** + * \file + * \brief Internal to the plugin: what the loader and the saver share. + * + * Constants, the PSD_ARGS_* pair, and the few helpers both sides use, defined once in + * minipsd_vips_util.c (prefixed minipsd_vips__, since a static library's names meet the program's). + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#ifndef MINIPSD_VIPS_UTIL_H +#define MINIPSD_VIPS_UTIL_H + +#include + +#include + +#define PSD_RGB 3 /* RGB's colour channels */ +#define PSD_CMYK 4 /* CMYK's colour channels */ +#define MM_PER_INCH 25.4 + +/* libvips' VIPS_ARG_* macros pass vips_argument_get_id()'s int as a guint property id, and + G_STRUCT_OFFSET's glong as a guint offset, which -Wsign-conversion and -Wconversion report in + our code; only around them are they off. */ +#if defined(__GNUC__) +# define PSD_ARGS_BEGIN \ + _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wsign-conversion\"") \ + _Pragma("GCC diagnostic ignored \"-Wconversion\"") +# define PSD_ARGS_END _Pragma("GCC diagnostic pop") +#else +# define PSD_ARGS_BEGIN +# define PSD_ARGS_END +#endif + +/* The suffixes libvips matches files by: .psd and .psb. */ +extern const char *minipsd_vips__suffs[]; + +/* GObject resets set_property and get_property in every class (g_object_base_class_init), so + each class with arguments of its own sets libvips' again. */ +void minipsd_vips__class_properties(void *class); + +/* CMYK the other way round, in place: PSD stores ink inverted (full: no ink), libvips holds it + as TIFF does (0: no ink), as its own jpegload makes Adobe's inverted CMYK and ImageMagick + makes PSD's. The colour channels only, not transparency; both ways, loading and saving. + count pixels of bands samples, each of depth bits (8, 16, or 32 as floats). */ +void minipsd_vips__invert_cmyk(void *pixels, size_t count, unsigned bands, int depth); + +#endif /* MINIPSD_VIPS_UTIL_H */ diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index f4baf55..0a1466b 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -349,7 +349,7 @@ static void check_document(const minipsd_doc *doc, const minipsd_write_opts *opt if (f) fclose(f); loader = vips_foreign_find_load(path); - check(loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0, "found as psdload", e->name); + check(loader && strcmp(loader, "MinipsdVipsPsdloadFile") == 0, "found as psdload", e->name); vips_error_clear(); check_image(vips_image_new_from_file(path, NULL), doc, e, "file", &b); check_image(vips_image_new_from_file(path, "access", VIPS_ACCESS_SEQUENTIAL, NULL), doc, e, "file, sequential", &b); @@ -437,7 +437,7 @@ static void check_saved(VipsImage *image, const char *name, const char *dir, int return; } saver = vips_foreign_find_save(path); - check(saver && strcmp(saver, "VipsForeignSavePsdFile") == 0, "found as psdsave", name); + check(saver && strcmp(saver, "MinipsdVipsPsdsaveFile") == 0, "found as psdsave", name); /* The same bytes three ways. */ file = read_all(path, &file_size); @@ -849,7 +849,7 @@ int main(int argc, char **argv) /* Not a PSD: psdload does not claim it. */ loader = vips_foreign_find_load_buffer(junk, sizeof junk); - check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); + check(!loader || strcmp(loader, "MinipsdVipsPsdloadBuffer") != 0, "junk not claimed", "junk"); vips_error_clear(); if (argc == 3) check_list(argv[2]); From 9a8697bbf11cf9c8d7dadc105d51b2973127620d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 02:18:13 +0200 Subject: [PATCH 08/22] chore(release): 0.3.2 Version 0.3.2 in minipsd.h, the changelog section dated, COMPATIBILITY.md regenerated (no change but the version), the README's release line. INSTALL.md's examples take 0.3.2 and say how a program adds the libvips plugin (MINIPSD_VIPS_PLUGIN, minipsd::vips, its own libvips targets); the manual says sequential loading's memory is a few bands per thread, growing with the width, as measured against magickload. The roadmap ticks S4.11 and adds S4.12, this release. --- README.md | 2 +- docs/CHANGELOG.md | 2 +- docs/CHEATSHEET.md | 6 +++--- docs/COMPATIBILITY.md | 2 +- docs/INSTALL.md | 27 ++++++++++++++++++++++----- docs/MANUAL.md | 3 ++- docs/ROADMAP.md | 10 +++++++--- include/minipsd.h | 4 ++-- 8 files changed, 39 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c221e65..b53b5c8 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Procreate open as layered documents. # Status -**release: 0.3.1** +**release: 0.3.2** The API is [include/minipsd.h](include/minipsd.h); the implementation is in [include/minipsd/](include/minipsd/). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e61419a..b41951f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,7 +3,7 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 rules in [TODO.md](TODO.md): breaking changes bump the MINOR, additions and fixes the PATCH. -## [Unreleased] +## [0.3.2] — 2026-10-25 ### Added - The libvips plugin (`libvips-plugin/`, `MINIPSD_VIPS_PLUGIN`, the static library diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 46d7a43..51e9c26 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -114,9 +114,9 @@ cpack --config build/package/CPackConfig.cmake -G DEB Check and install the `.deb`: ```bash -dpkg-deb -I dist/libminipsd-dev_0.2.0_all.deb # metadata -dpkg-deb -c dist/libminipsd-dev_0.2.0_all.deb # files -sudo apt install ./dist/libminipsd-dev_0.2.0_all.deb +dpkg-deb -I dist/libminipsd-dev_0.3.2_all.deb # metadata +dpkg-deb -c dist/libminipsd-dev_0.3.2_all.deb # files +sudo apt install ./dist/libminipsd-dev_0.3.2_all.deb sudo apt remove libminipsd-dev ``` diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 86011a7..4b7be6b 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -8,7 +8,7 @@ output is covered by the golden test and the `psd-tools` round trip (see the spe [Verification](SPECIFICATION.md#verification)); this page is about everyone else's files. This page is generated by [tools/compat_report.py](../tools/compat_report.py) with minipsd -0.3.1, over 315 files of psd-tools v1.25.0. +0.3.2, over 315 files of psd-tools v1.25.0. ## Credits diff --git a/docs/INSTALL.md b/docs/INSTALL.md index e06c81f..ea1768f 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -13,7 +13,7 @@ attaches the same headers joined into one `minipsd.h`, for those who prefer a si include(FetchContent) FetchContent_Declare(minipsd GIT_REPOSITORY https://github.com/ShadobaDev/minipsd.git - GIT_TAG 0.2.0) + GIT_TAG 0.3.2) FetchContent_MakeAvailable(minipsd) target_link_libraries(my-app PRIVATE minipsd::minipsd) ``` @@ -21,11 +21,11 @@ target_link_libraries(my-app PRIVATE minipsd::minipsd) **[CPM.cmake](https://github.com/cpm-cmake/CPM.cmake)** ```cmake -CPMAddPackage("gh:ShadobaDev/minipsd#0.2.0") +CPMAddPackage("gh:ShadobaDev/minipsd#0.3.2") target_link_libraries(my-app PRIVATE minipsd::minipsd) ``` -Use `#0.2.0`, not `@0.2.0`: the `@` form looks for a tag `v0.2.0`, and minipsd tags have no `v`. +Use `#0.3.2`, not `@0.3.2`: the `@` form looks for a tag `v0.3.2`, and minipsd tags have no `v`. **git submodule** @@ -39,6 +39,23 @@ target_link_libraries(my-app PRIVATE minipsd::minipsd) As a subproject, minipsd builds no tests, fetches nothing and enables no language. +**The libvips plugin.** In any of the three ways above, turn on `MINIPSD_VIPS_PLUGIN` before +adding minipsd and link `minipsd::vips` (then call `minipsd_vips_init()` after `VIPS_INIT`, see +the manual's [PSD in libvips](MANUAL.md#psd-in-libvips-the-plugin)). It builds from source, as +part of your program, against your libvips' headers, since its classes extend libvips' own: + +```cmake +set(MINIPSD_VIPS_PLUGIN ON) +FetchContent_MakeAvailable(minipsd) +target_link_libraries(my-app PRIVATE minipsd::vips) +``` + +It enables C, and finds libvips with pkg-config, or on Windows fetches libvips' prebuilt build +(pinned in [cmake/minipsd_libvips.cmake](../cmake/minipsd_libvips.cmake)). A program with +libvips of its own defines two targets before adding minipsd, and nothing is fetched or found: +`minipsd_libvips` (libvips, glib, gobject) and `minipsd_libvips_zlib` (the zlib that libvips +links, which inflates ZIP layers), each an `INTERFACE` library or `IMPORTED GLOBAL`. + **Installed package.** minipsd installs as a tree with nothing platform-specific in it: ``` @@ -59,11 +76,11 @@ You can get that tree in any of these ways: - **As an archive**, `minipsd-X.Y.Z.tar.gz` or `.zip`: use it unpacked where it is, or extract it into a prefix. The tree is relocatable, so nothing needs regenerating: ```bash - sudo tar -xzf minipsd-0.2.0.tar.gz --strip-components=1 -C /usr/local + sudo tar -xzf minipsd-0.3.2.tar.gz --strip-components=1 -C /usr/local ``` - **As a Debian/Ubuntu package**, `libminipsd-dev_X.Y.Z_all.deb`: ```bash - sudo apt install ./libminipsd-dev_0.2.0_all.deb + sudo apt install ./libminipsd-dev_0.3.2_all.deb ``` The archive and the `.deb` are built with CPack, see diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 8ee4854..b5ddd88 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -814,7 +814,8 @@ vips_image_write_to_file(page, "copy.psd", "layer_name", "Page", NULL); depth. With it: the resolution, the ICC profile, EXIF, XMP, IPTC, `bits-per-sample`, and `psd-*` fields such as `psd-layer-count` and a layer's name, blend mode and opacity (the list: the spec's [Working with libvips](SPECIFICATION.md#working-with-libvips)). Bands of rows are - decoded from libvips' threads at once; with `access=sequential` memory stays at a few bands. + decoded from libvips' threads at once; with `access=sequential` memory stays at a few bands + of rows per thread, so it grows with the width and the threads, not the height. - **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): one layer that is also the composite; grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats; alpha kept, or the layer made opaque. Options: `psb` (also a file named `.psb`), `zip`, `layer_name`. The image is diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 50f8032..6e46526 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -35,10 +35,10 @@ The order of work from 0.1.0 to the design in [CODE-ARCH.md](CODE-ARCH.md). Case | [1. Module split](#1-module-split) | the API stays in `minipsd.h`, the implementation becomes modules | 0.2.0, at S3.4 | | [2. Pull writer](#2-pull-writer) | non-blocking, heap-free writing | 0.2.0, at S3.4 | | [3. I/O out of the core](#3-io-out-of-the-core) | stdio leaves the library; examples instead | **0.2.0**, at S3.4 (breaking) | -| [4. Reader](#4-reader) | buffer to structure, for minipsd's own subset | the next release, at S4.10 | +| [4. Reader](#4-reader) | buffer to structure, for minipsd's own subset | 0.3.0, at S4.10 | | [5. Round trip and conveniences](#5-round-trip-and-conveniences) | layer rectangles, full import, cheap re-save, kept unknown content | batched, when decided | | [6. Format features](#6-format-features) | blend modes, groups, masks, 16-bit, PSB | batched, when decided | -| [7. Interoperability](#7-interoperability) | libvips | batched, when decided | +| [7. Interoperability](#7-interoperability) | libvips | 0.3.0 (S7.1), 0.3.2 (S7.2 to S7.4, at S4.12) | | [8. Test quality](#8-test-quality) | branch coverage of compound conditions | with any release | | [Not scheduled](#not-scheduled) | large cases, only on real demand | — | @@ -189,12 +189,16 @@ Additive. See [Reader](CODE-ARCH.md#reader). the pre-1.0 rules. Before it: regenerate [COMPATIBILITY.md](COMPATIBILITY.md) with the new version, and have one CI job fetch psd-tools' files (`MINIPSD_PSD_TOOLS_FILES`; done: `linux-gcc`), so that Photoshop's ZIP files are checked through zlib there too, not only locally. -- [ ] **S4.11** Release **0.3.1** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): additions and a fix, +- [x] **S4.11** Release **0.3.1** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): additions and a fix, so a PATCH. `MINIPSD_STATIC` without unused-function warnings, `MINIPSD_NO_READER` and `MINIPSD_NO_WRITER`, the typed setters (S6.20), the pixel aspect ratio (S6.21); the release workflow checks CI's run on the tagged commit instead of running it again. Before it: [COMPATIBILITY.md](COMPATIBILITY.md) regenerated with the version (done), the PR merged and CI green on `master`. +- [ ] **S4.12** Release **0.3.2** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): an addition and a + fix, so a PATCH. The libvips plugin (S7.2 to S7.4); the writer names a composite's + transparency where the layer count cannot. Before it: [COMPATIBILITY.md](COMPATIBILITY.md) + regenerated with the version (done), the PR merged and CI green on `master`. ## 5. Round trip and conveniences diff --git a/include/minipsd.h b/include/minipsd.h index c2a2fc3..7b09a56 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -109,11 +109,11 @@ */ #define MINIPSD_VERSION_MAJOR 0 #define MINIPSD_VERSION_MINOR 3 -#define MINIPSD_VERSION_PATCH 1 +#define MINIPSD_VERSION_PATCH 2 #define MPSD__STR(x) #x #define MPSD__XSTR(x) MPSD__STR(x) -/** \brief The version as a string literal, e.g. "0.3.1". */ +/** \brief The version as a string literal, e.g. "0.3.2". */ #define MINIPSD_VERSION_STRING \ MPSD__XSTR(MINIPSD_VERSION_MAJOR) "." MPSD__XSTR(MINIPSD_VERSION_MINOR) "." MPSD__XSTR(MINIPSD_VERSION_PATCH) From 8d759b272418f129f2cbd4ce2baeb3948c014b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 03:14:08 +0100 Subject: [PATCH 09/22] feat(writer): a composite and layers without transparency composite_alpha now says, for every composite (supplied, composed from the layers, or stored), whether it has transparency; 0 writes the mode's colour channels only, the layer count positive, no transparency names or 'Mtrn', so an image without layers is flat, as Photoshop and psd-tools save one. A composed composite without it is matted against white. minipsd_layer.no_alpha writes a layer without a transparency channel, its pixels the colour channels only; the composer takes it as opaque. minipsd_read sets both from the file: such a layer and such a composite decode without transparency, so a round trip keeps them. BREAKING: a program that supplies or composes a composite with transparency sets composite_alpha = 1. The libvips plugin: psdsave writes an image without alpha flat (the option layered: one layer without transparency) instead of adding an opaque band; psdload gives a layer without its channel no alpha band. --- examples/demo.h | 1 + examples/hello_write.c | 3 +- examples/vips_composite.c | 15 +++- include/minipsd.h | 40 +++++---- include/minipsd/detail/composite.h | 37 ++++++-- include/minipsd/read.h | 27 +++--- include/minipsd/writer.h | 39 ++++---- libvips-plugin/minipsd_vips_psdload.c | 9 +- libvips-plugin/minipsd_vips_psdsave.c | 39 ++++---- libvips-plugin/test_vips.c | 59 ++++++------ tests/minipsd_c_smoke.c | 2 +- tests/psd_tools/roundtrip.py | 33 ++++--- tests/support.h | 1 + tests/test_golden.cpp | 8 +- tests/test_reader.cpp | 124 +++++++++++++++++++++----- tests/test_writer.cpp | 12 ++- 16 files changed, 303 insertions(+), 146 deletions(-) diff --git a/examples/demo.h b/examples/demo.h index ede3de8..c35f396 100644 --- a/examples/demo.h +++ b/examples/demo.h @@ -43,6 +43,7 @@ static void demo_init(demo *d) memset(&d->doc, 0, sizeof d->doc); d->doc.width = DEMO_W; d->doc.height = DEMO_H; d->doc.dpi = 300.0; d->doc.layers = d->layers; d->doc.layer_count = 2; d->doc.compression = MINIPSD_RLE; + d->doc.composite_alpha = 1; /* the layers have transparency, and so has the composite composed of them */ } #endif /* EXAMPLES_DEMO_H */ diff --git a/examples/hello_write.c b/examples/hello_write.c index 58b6e6f..63aca7f 100644 --- a/examples/hello_write.c +++ b/examples/hello_write.c @@ -21,7 +21,8 @@ int main(void) static uint8_t rgba[64 * 64 * 4]; /* straight alpha, rows top down */ /* Fields left out are zero, the defaults: visible, the full canvas, RLE. */ minipsd_layer layer = {.name = "Hello", .pixels = rgba, .opacity = 255}; - minipsd_doc doc = {.width = 64, .height = 64, .dpi = 72.0, .layers = &layer, .layer_count = 1}; + minipsd_doc doc = {.width = 64, .height = 64, .dpi = 72.0, .layers = &layer, .layer_count = 1, + .composite_alpha = 1}; /* composed from the layer, with its transparency */ FILE *f; int i, rc; diff --git a/examples/vips_composite.c b/examples/vips_composite.c index fbd6028..d0a56d5 100644 --- a/examples/vips_composite.c +++ b/examples/vips_composite.c @@ -31,7 +31,8 @@ #include #include -#define RGBA 4 /* bands: 8-bit red, green, blue, alpha */ +#define RGB 3 /* bands: 8-bit red, green, blue */ +#define RGBA 4 /* ... and alpha */ #define FULL 255 /* an 8-bit sample's full value */ /* The whole file in a new buffer; NULL when it cannot be read. */ @@ -83,10 +84,17 @@ static VipsBlendMode blend_mode(const char key[4], int *mapped) /* A layer's pixels as a libvips image, wrapped without a copy, its opacity in its alpha. */ static VipsImage *layer_image(const minipsd_layer *l) { - const int w = (int)(l->right - l->left), h = (int)(l->bottom - l->top); + const int w = (int)(l->right - l->left), h = (int)(l->bottom - l->top), bands = l->no_alpha ? RGB : RGBA; VipsImage *wrapped, *srgb, *out; - wrapped = vips_image_new_from_memory(l->pixels, (size_t)w * (size_t)h * RGBA, w, h, RGBA, VIPS_FORMAT_UCHAR); + wrapped = vips_image_new_from_memory(l->pixels, (size_t)w * (size_t)h * (size_t)bands, w, h, bands, VIPS_FORMAT_UCHAR); if (!wrapped) return NULL; + if (l->no_alpha) { /* a layer without transparency: opaque, so that every layer is RGBA */ + VipsImage *opaque; + const int rc = vips_bandjoin_const1(wrapped, &opaque, FULL, NULL); + g_object_unref(wrapped); + if (rc) return NULL; + wrapped = opaque; + } if (vips_copy(wrapped, &srgb, "interpretation", VIPS_INTERPRETATION_sRGB, NULL)) { /* RGBA, not 4 bands of anything */ g_object_unref(wrapped); return NULL; @@ -258,6 +266,7 @@ int main(int argc, char **argv) if (!flat) { fprintf(stderr, "vips_composite: libvips: %s\n", vips_error_buffer()); goto done; } doc->composite = flat; /* ... and minipsd writes them with that image */ doc->composite_matted = 0; /* straight alpha: the writer mattes it, as the format wants */ + doc->composite_alpha = 1; /* RGBA: it has transparency */ f = fopen(argv[1], "wb"); if (!f) rc = MINIPSD_ERR_OPEN; diff --git a/include/minipsd.h b/include/minipsd.h index 7b09a56..afa56dd 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -51,7 +51,7 @@ * * \par What it writes * A document in any colour mode (minipsd_doc.color_mode) at the depths Photoshop has it, as a PSD - * or (minipsd_doc.format) a PSB, with a transparency channel but in the flat modes (bitmap, + * or (minipsd_doc.format) a PSB, with or without transparency (none in the flat modes: bitmap, * indexed, multichannel, which have no layers); no colour is converted: * - one or more layers, bottom first, each a rectangle anywhere on or off the canvas (by default * the full canvas) or empty, with a name, opacity, visibility, blend key, clipping and flags; @@ -60,9 +60,11 @@ * - the composite (merged) image, which is what a reader that ignores layers shows: the caller's * own render (minipsd_doc.composite), the right way, since flattening layers is rendering; or, * without one, a best effort with no guarantee: the visible layers blended with straight-alpha - * "over" at their rectangles, clipped to the canvas. Its transparency rides as the first extra - * channel, flagged by a negative layer count, and its colour is matted against white - * (c * a + 255 * (1 - a)), as Photoshop stores it and readers expect to undo; + * "over" at their rectangles, clipped to the canvas. Its colour is matted against white + * (c * a + 255 * (1 - a)), as Photoshop stores it and readers expect to undo; its transparency, + * when it has one (minipsd_doc.composite_alpha), rides as the first extra channel, flagged by a + * negative layer count. Without layers and without transparency the file is flat, as + * Photoshop saves an image without layers; * - the resolution, in pixels per inch (resource 1005); * - an optional ICC profile (resource 1039); without one, readers assume sRGB; * - resources and the global layer mask and blocks kept from a file, as they are. @@ -374,8 +376,9 @@ typedef struct minipsd_layer { /* NOLINT(clang-analyzer-optin.performance.Paddi const uint8_t *pixels; /*!< (right - left) * (bottom - top) pixels, rows top down, each the mode's colour channels then transparency (RGB: R, G, B, A), straight (not premultiplied) alpha, as minipsd_target_interleaved - decodes them; NULL means an empty layer. A sample is a byte, or - with depth 16 a uint16_t in host order (two bytes). */ + decodes them; without transparency (no_alpha) the colour + channels only. NULL means an empty layer. A sample is a byte, + or with depth 16 a uint16_t in host order (two bytes). */ uint8_t opacity; /*!< 255 is opaque, 0 is fully transparent. */ uint8_t hidden; /*!< Non-zero: the layer starts hidden (and stays out of the composite). */ int32_t top, left, bottom, right; /*!< The layer's rectangle on the canvas, as in @@ -418,6 +421,11 @@ typedef struct minipsd_layer { /* NOLINT(clang-analyzer-optin.performance.Paddi file's names say, they are written as they are, this one and a kept 'luni'; once it changes, the writer's own. Size 0 and no kept 'luni': the writer's own. */ + uint8_t no_alpha; /*!< Non-zero: the layer has no transparency: pixels hold the + colour channels only, and it is written without a transparency + channel (composed as opaque). A stored layer says it by its + transparency of size 0 instead. minipsd_read sets it for a + layer whose file has none. */ } minipsd_layer; /** @@ -438,8 +446,8 @@ typedef struct minipsd_doc { int compression; /*!< MINIPSD_RLE, MINIPSD_RAW, or MINIPSD_ZIP(_PREDICTED) with minipsd_write_opts.deflate. */ const uint8_t *composite; /*!< The composite (merged) image: width * height - pixels laid out as a layer's (the mode's colour - channels then transparency), rows top down, + pixels, the mode's colour channels then, with + composite_alpha, transparency; rows top down, straight alpha, written whatever the layers hold. NULL: composite_stored is copied; without it too, composed from the @@ -491,13 +499,15 @@ typedef struct minipsd_doc { (composite_alpha) and alpha or spot channels, in the file's order. Ignored without composite_stored. */ - uint8_t composite_alpha; /*!< Non-zero: composite_stored's fourth channel is - transparency, and the layer count is written - negative; zero: any fourth channel is an alpha - channel, the count positive. Ignored without - composite_stored: a supplied or composed - composite always has transparency. minipsd_read - sets the file's. */ + uint8_t composite_alpha; /*!< Non-zero: the composite has transparency, and the + layer count is written negative: a supplied + composite's pixels end with it, a composed one + keeps the layers', composite_stored's channel + after the colour ones is it. Zero: the colour + channels only, the count positive (a composed one + matted against white; a stored one's further + channels are alpha channels). Not in the flat + modes. minipsd_read sets the file's. */ uint16_t color_mode; /*!< MINIPSD_MODE_*: what the samples are; 0 is RGB (bitmap with depth 1). minipsd_read sets the file's. */ diff --git a/include/minipsd/detail/composite.h b/include/minipsd/detail/composite.h index 601786f..87a9932 100644 --- a/include/minipsd/detail/composite.h +++ b/include/minipsd/detail/composite.h @@ -45,6 +45,23 @@ MPSD__SHARED static int mpsd__doc_flat(const minipsd_doc *d) MPSD__SHARED static unsigned mpsd__doc_colors(const minipsd_doc *d) { return mpsd__colors(mpsd__doc_mode(d), d->composite_channels); } MPSD__SHARED static unsigned mpsd__doc_pixel(const minipsd_doc *d) { return mpsd__doc_colors(d) + !mpsd__doc_flat(d); } +/* The samples of a layer's pixel: the colour channels, then transparency unless it has none. */ +MPSD__SHARED static unsigned mpsd__layer_pixel(const minipsd_doc *d, const minipsd_layer *l) { return mpsd__doc_colors(d) + !l->no_alpha; } + +/* The samples of a supplied composite's pixel: the colour channels, then transparency when it has + one (never in the flat modes). */ +MPSD__SHARED static unsigned mpsd__supplied_pixel(const minipsd_doc *d) +{ + return mpsd__doc_colors(d) + (d->composite_alpha && !mpsd__doc_flat(d)); +} + +/* The samples a pixel of mpsd__composite_row's row takes: a supplied composite without + transparency is handed out as it is; every other row is a full pixel. */ +MPSD__SHARED static unsigned mpsd__composite_step(const minipsd_doc *d) +{ + return d->composite ? mpsd__supplied_pixel(d) : mpsd__doc_pixel(d); +} + MPSD__SHARED static int mpsd__contributes(const minipsd_layer *l) { return l->pixels && !l->hidden && l->opacity; } /* A layer's rectangle as written, and the size of its pixels: 0 by 0 for a layer without pixels or @@ -80,7 +97,8 @@ MPSD__SHARED static void mpsd__composite_init(mpsd__composite *c, const minipsd_ # define MPSD__COMPOSED() ((void)0) #endif -/* The visible layers' row y blended "over" into row, straight alpha: the best effort. */ +/* The visible layers' row y blended "over" into row, straight alpha: the best effort. A layer + without transparency counts as opaque. */ MPSD__SHARED static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint32_t y) { const unsigned wide = mpsd__doc_shift(d), pixel = (unsigned)MPSD__RGBA << wide; @@ -95,13 +113,15 @@ MPSD__SHARED static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint3 const int64_t ly = (int64_t)y - r.top; const int64_t x0 = r.left > 0 ? r.left : 0; const int64_t x1 = r.right < (int64_t)d->width ? r.right : (int64_t)d->width; + const unsigned lp = mpsd__layer_pixel(d, l) << wide; const uint8_t *src; if (!mpsd__contributes(l) || ly < 0 || ly >= (int64_t)r.height) continue; - src = l->pixels + (size_t)ly * r.width * pixel; + src = l->pixels + (size_t)ly * r.width * lp; for (x = x0; x < x1; ++x) { uint8_t *o = row + (size_t)x * pixel; - const uint8_t *px = src + (size_t)(x - r.left) * pixel; - const float sa = (float)mpsd__sample(px + ((unsigned)MINIPSD_CH_A << wide), wide) / max * (l->opacity / (float)MPSD__FULL_8); + const uint8_t *px = src + (size_t)(x - r.left) * lp; + const float la = l->no_alpha ? max : (float)mpsd__sample(px + ((unsigned)MINIPSD_CH_A << wide), wide); + const float sa = la / max * (l->opacity / (float)MPSD__FULL_8); const float da = (float)mpsd__sample(o + ((unsigned)MINIPSD_CH_A << wide), wide) / max; const float oa = sa + da * (1.0f - sa); unsigned k; @@ -117,9 +137,10 @@ MPSD__SHARED static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint3 } /* The composite's row y as the file stores it, matted against white: a supplied row that is - already matted, or of another mode than RGB, as it is (bitmap packed into the caller's row); - otherwise the supplied row copied, or the layers composed, into the caller's row, then matted - there. */ + already matted, of another mode than RGB, or without transparency (nothing to matte), as it is + (bitmap packed into the caller's row), mpsd__composite_step samples a pixel; otherwise the + supplied row copied, or the layers composed, into the caller's row, then matted there, a full + pixel each, of which a composite without transparency writes the colour. */ MPSD__SHARED static const uint8_t *mpsd__composite_row(mpsd__composite *c, uint32_t y) { const minipsd_doc *d = c->doc; @@ -127,6 +148,8 @@ MPSD__SHARED static const uint8_t *mpsd__composite_row(mpsd__composite *c, uint3 const uint32_t max = mpsd__doc_full(d); const size_t size = (size_t)d->width * pixel; uint32_t x; + if (d->composite && mpsd__supplied_pixel(d) < mpsd__doc_pixel(d)) /* no transparency: colour as given */ + return d->composite + (size_t)y * d->width * (mpsd__supplied_pixel(d) << wide); if (mpsd__doc_depth(d) == MPSD__CH_1BIT) { /* bitmap: eight pixels a byte, the first the high bit, 1 black */ const uint8_t *in = d->composite + (size_t)y * d->width; memset(c->row, 0, (size_t)mpsd__row_bytes(d->width, MPSD__CH_1BIT)); diff --git a/include/minipsd/read.h b/include/minipsd/read.h index e138cbc..5864e92 100644 --- a/include/minipsd/read.h +++ b/include/minipsd/read.h @@ -128,7 +128,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit uint64_t decoded, canvas; uint32_t loss = 0; size_t i, count, n = 0; - unsigned wide, planes; + unsigned wide, planes, cplanes; uint32_t side; int rc, flat_mode; @@ -149,6 +149,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit } wide = r->shift; /* wider samples, aligned for their type */ planes = r->colors + !flat_mode; /* a pixel: the colour channels, then transparency */ + cplanes = r->colors + (!flat_mode && info.composite_has_alpha && info.channels > r->colors); /* the composite's, as the file has it */ side = mpsd__max_side(r->psb); memset(&all, 0, sizeof all); /* every row, un-matted; new fields zero */ all.depth = (int)r->depth; @@ -158,7 +159,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit all.scratch_size = (size_t)a->zip; /* measured: the block holds it */ as_stored = all; as_stored.keep_matte = 1; - canvas = (uint64_t)info.width * info.height * planes << wide; + canvas = (uint64_t)info.width * info.height * cplanes << wide; /* Stored: the composite copied as it is, every channel, never decoded; multichannel's always. */ keep_composite = ((opts && opts->stored) || info.color_mode == MINIPSD_MODE_MULTICHANNEL) && info.channels >= r->colors; decoded = keep_composite ? 0 : canvas; /* the composite */ @@ -182,6 +183,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit uint8_t *mask_pixels = NULL; uint32_t j; int k, keep; + unsigned lplanes; /* decoded: transparency only when the file has it */ rc = minipsd_reader_next_layer(r, &v); if (rc != MINIPSD_MORE) return rc == MINIPSD_OK ? MINIPSD_ERR_FORMAT : rc; if (v.other_channels) loss |= MINIPSD_LOST_CHANNELS; /* TODO: alpha and spot channels (ROADMAP, Not scheduled) */ @@ -198,12 +200,13 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit stored = (minipsd_bytes *)mpsd__take(a, planes * sizeof *stored, 1); for (k = 0; k < (int)planes; ++k) copies[k] = mpsd__copy(a, v.channel[k]); } - pixels = w && h && !keep ? (uint8_t *)mpsd__take(a, w * h * planes << wide, (int)wide) : NULL; + lplanes = r->colors + (v.channel[r->colors].size != 0); + pixels = w && h && !keep ? (uint8_t *)mpsd__take(a, w * h * lplanes << wide, (int)wide) : NULL; extra = mpsd__copy(a, v.additional_info); /* every block, 'luni' too */ pascal = mpsd__copy(a, v.name_pascal); ranges = (minipsd_blending_range *)mpsd__take(a, (uint64_t)v.blending_range_count * sizeof *ranges, 1); - if (pixels) decoded += w * h * planes << wide; - for (k = 0; w && h && !keep && k < (int)planes; ++k) /* decoded: pixels, NULL while measuring */ + if (pixels) decoded += w * h * lplanes << wide; + for (k = 0; w && h && !keep && k < (int)lplanes; ++k) /* decoded: pixels, NULL while measuring */ mpsd__zip_room(a, v.channel[k], w * h << wide); if (v.has_mask) { /* the mask: decoded, or stored with the layer */ const uint64_t mw = (uint64_t)((int64_t)v.mask.right - v.mask.left), mh = (uint64_t)((int64_t)v.mask.bottom - v.mask.top); @@ -228,12 +231,13 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit if (!d) continue; (void)minipsd_layer_name(&v, name, n, NULL); if (pixels) { - mpsd__target(&t, pixels, (size_t)(w * planes << wide), planes, wide); + mpsd__target(&t, pixels, (size_t)(w * lplanes << wide), lplanes, wide); rc = minipsd_decode_layer(r, &v, &t, &all); if (rc != MINIPSD_OK) return rc; } layers[i].name = name; layers[i].pixels = pixels; + layers[i].no_alpha = (uint8_t)(lplanes == r->colors); /* an empty one's too: its channels as the file's */ layers[i].opacity = v.opacity; layers[i].hidden = v.hidden; layers[i].top = v.top; layers[i].left = v.left; @@ -295,16 +299,17 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit if (!d) return MINIPSD_OK; if (pixels) { - mpsd__target(&t, pixels, (size_t)info.width * planes << wide, planes, wide); + mpsd__target(&t, pixels, (size_t)info.width * cplanes << wide, cplanes, wide); rc = minipsd_decode_composite(r, &t, &as_stored); if (rc != MINIPSD_OK) return rc; } - if (flat) { - mpsd__target(&t, flat, (size_t)info.width * planes << wide, planes, wide); - rc = minipsd_decode_composite(r, &t, &all); + if (flat) { /* un-matted; without transparency there is no matte to undo */ + mpsd__target(&t, flat, (size_t)info.width * cplanes << wide, cplanes, wide); + rc = minipsd_decode_composite(r, &t, cplanes > r->colors ? &all : &as_stored); if (rc != MINIPSD_OK) return rc; layers[0].name = "Background"; layers[0].pixels = flat; + layers[0].no_alpha = (uint8_t)(cplanes == r->colors); layers[0].opacity = 255; layers[0].bottom = (int32_t)info.height; /* the rectangle a reader then sees */ layers[0].right = (int32_t)info.width; @@ -320,7 +325,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit d->composite_matted = 1; d->composite_stored = composite; d->composite_channels = keep_composite ? (uint16_t)info.channels : 0; - d->composite_alpha = (uint8_t)(keep_composite && !flat_mode && info.composite_has_alpha && info.channels > r->colors); + d->composite_alpha = (uint8_t)(cplanes > r->colors); /* stored or decoded, as the file has it */ { /* the file's pixel aspect ratio, kept when it is one the writer takes; else 0: none of its own */ minipsd_resource aspect; const double ratio = info.pixel_aspect_ratio; diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 2b0a52f..9bfa7c6 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -88,8 +88,9 @@ static int mpsd__channel_id(const minipsd_doc *d, int k) channels first, transparency last. */ static int mpsd__channel_of(const minipsd_doc *d, int k) { return k ? k - 1 : (int)mpsd__doc_colors(d); } -/* The channels written are [mpsd__first_channel, mpsd__channel_count) of that order: a stored - layer without transparency, as its file had it, starts at the first colour channel. */ +/* The channels written are [mpsd__first_channel, mpsd__channel_count) of that order: a layer + without transparency (stored without it, as its file had it, or no_alpha) starts at the first + colour channel. */ static int mpsd__channel_count(const minipsd_doc *d, const minipsd_layer *l) { return mpsd__ch_mask(d) + (l->mask ? 1 + (l->mask->real.size != 0) : 0); @@ -97,7 +98,7 @@ static int mpsd__channel_count(const minipsd_doc *d, const minipsd_layer *l) static int mpsd__first_channel(const minipsd_doc *d, const minipsd_layer *l) { - return l->stored && !l->stored[mpsd__doc_colors(d)].size; + return l->stored ? !l->stored[mpsd__doc_colors(d)].size : l->no_alpha != 0; } /* Where channel k of a layer comes from: bytes to copy as they are, or samples to encode, step @@ -139,8 +140,8 @@ static mpsd__source mpsd__channel_source(const minipsd_doc *d, const minipsd_lay src.samples = r.height ? full : NULL; src.step = 0; } else { - src.samples = r.height ? l->pixels + ((size_t)at << wide) : NULL; - src.step = (size_t)mpsd__doc_pixel(d) << wide; + src.samples = r.height ? l->pixels + ((size_t)at << wide) : NULL; /* transparency: never asked without */ + src.step = (size_t)mpsd__layer_pixel(d, l) << wide; } src.width = r.width; src.height = r.height; @@ -197,17 +198,19 @@ static uint64_t mpsd__deflate_room(uint64_t n) { return n + (n >> 11) + 16; } /* Is the composite copied as the file stored it? Only when the caller gives none of their own. */ static int mpsd__copies_composite(const minipsd_doc *d) { return !d->composite && d->composite_stored.size; } -/* The composite's channels in the file: the stored one's, or R, G, B and transparency. */ +/* Has the composite transparency, its channel after the colour ones? As composite_alpha says, + whichever the composite: supplied, composed or stored. The layer count says it, negative, or + where it cannot (mpsd__alpha_uncounted) a 'Mtrn' ('Mt16', 'Mt32') block and the channel's + names. */ +static int mpsd__composite_alpha(const minipsd_doc *d) { return d->composite_alpha && !mpsd__doc_flat(d); } + +/* The composite's channels in the file: the stored one's, or the colour channels and, with it, + transparency. */ static unsigned mpsd__composite_channels(const minipsd_doc *d) { - return mpsd__copies_composite(d) ? d->composite_channels : mpsd__doc_pixel(d); + return mpsd__copies_composite(d) ? d->composite_channels : mpsd__doc_colors(d) + (unsigned)mpsd__composite_alpha(d); } -/* Is the composite's fourth channel transparency? Always for a supplied or composed one. The layer - count says it, negative, or where it cannot (mpsd__alpha_uncounted) a 'Mtrn' ('Mt16', 'Mt32') - block and the channel's names. */ -static int mpsd__composite_alpha(const minipsd_doc *d) { return !mpsd__copies_composite(d) || d->composite_alpha; } - /* No source: the pieces outside a layer channel and the composite. */ static mpsd__source mpsd__no_source(void) { @@ -870,7 +873,7 @@ static int mpsd__doc_mode_ok(const minipsd_doc *d) : d->color_mode_data.size && depth != MPSD__CH_32BIT) return MINIPSD_ERR_ARG; if ((mode != MINIPSD_MODE_RGB || depth == MPSD__CH_32BIT) && !d->composite && !d->composite_stored.size) return MINIPSD_ERR_ARG; - if (mpsd__doc_flat(d) && (d->layer_count || (d->composite_stored.size && d->composite_alpha))) return MINIPSD_ERR_ARG; + if (mpsd__doc_flat(d) && (d->layer_count || d->composite_alpha)) return MINIPSD_ERR_ARG; if (mode == MINIPSD_MODE_MULTICHANNEL && (d->composite || !d->composite_channels)) return MINIPSD_ERR_ARG; return MINIPSD_OK; } @@ -1170,13 +1173,13 @@ MINIPSDDEF int minipsd_writer_init(minipsd_writer **out, const minipsd_doc *d, c const mpsd__source src = mpsd__composite_source(d); total += mpsd__stored_size(&src, mpsd__doc_psb(d), w->out) - MPSD__MARKER_SIZE; } else if (d->compression == MINIPSD_RAW) { - total += mpsd__row_bytes(d->width, mpsd__doc_depth(d)) * d->height * mpsd__doc_pixel(d); + total += mpsd__row_bytes(d->width, mpsd__doc_depth(d)) * d->height * mpsd__composite_channels(d); } else { /* each row's count, then the row */ mpsd__composite_init(&comp, d, w->composite_row); for (y = 0; y < d->height; ++y) { const uint8_t *row = mpsd__composite_row(&comp, y); - for (k = 0; k < (int)mpsd__doc_pixel(d); ++k) { - const uint64_t n = mpsd__row_size(d, row + ((size_t)k << wide), (size_t)mpsd__doc_pixel(d) << wide, d->width); + for (k = 0; k < (int)mpsd__composite_channels(d); ++k) { + const uint64_t n = mpsd__row_size(d, row + ((size_t)k << wide), (size_t)mpsd__composite_step(d) << wide, d->width); w->composite_sizes[(size_t)k * d->height + y] = (uint32_t)n; total += mpsd__count_size(w->out) + n; } @@ -1264,7 +1267,7 @@ static void mpsd__writer_piece(minipsd_writer *w, mpsd__sink *s) case MPSD__TAIL: mpsd__tail(s, d, w->content, w->info, w->out); break; case MPSD__COMPOSITE_COUNT: mpsd__putcount(s, w->composite_sizes[(size_t)w->channel * d->height + w->row], w->out); break; default: - mpsd__row(s, d, mpsd__writer_composite(w, w->row) + ((size_t)w->channel << mpsd__doc_shift(d)), (size_t)mpsd__doc_pixel(d) << mpsd__doc_shift(d), d->width); + mpsd__row(s, d, mpsd__writer_composite(w, w->row) + ((size_t)w->channel << mpsd__doc_shift(d)), (size_t)mpsd__composite_step(d) << mpsd__doc_shift(d), d->width); break; } } @@ -1338,7 +1341,7 @@ static void mpsd__writer_next(minipsd_writer *w) default: /* the composite: channel by channel, row by row */ if (++w->row < d->height) return; w->row = 0; - if (++w->channel < (int)mpsd__doc_pixel(d)) return; + if (++w->channel < (int)mpsd__composite_channels(d)) return; w->channel = 0; w->piece = w->piece == MPSD__COMPOSITE_COUNT ? MPSD__COMPOSITE_ROW : MPSD__DONE; return; diff --git a/libvips-plugin/minipsd_vips_psdload.c b/libvips-plugin/minipsd_vips_psdload.c index 8188094..cb5e7f0 100644 --- a/libvips-plugin/minipsd_vips_psdload.c +++ b/libvips-plugin/minipsd_vips_psdload.c @@ -9,8 +9,8 @@ * Pixels are decoded in bands of rows as libvips asks for them, from its threads at once. * * The image is the composite, or with `layer` one layer (its position in xoffset and yoffset), as - * the file stores it, no colour converted: the mode's colour channels, then transparency (a - * layer's always, opaque when it has none). What libvips has no interpretation for (Lab, + * the file stores it, no colour converted: the mode's colour channels, then transparency where it + * has one (a layer's channel, the composite's). What libvips has no interpretation for (Lab, * multichannel) is MULTIBAND; the colour mode data is kept as "psd-color-mode-data". Indexed is * the exception: libvips has no indexed image, so it comes through its palette as sRGB, as * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). CMYK @@ -179,7 +179,10 @@ static int minipsd_vips_psdload_find_layer(MinipsdVipsPsdload *psd) } psd->width = (uint32_t)(psd->view.right - psd->view.left); psd->height = (uint32_t)(psd->view.bottom - psd->view.top); - psd->planes = VIPS_MIN(psd->info.color_channels + 1u, MINIPSD_PLANES); + /* Transparency when the layer has its channel: a layer without one comes without alpha, as it + was saved. */ + psd->planes = psd->info.color_channels; + if (psd->planes < MINIPSD_PLANES && psd->view.channel[psd->planes].size) ++psd->planes; if (minipsd_layer_name(&psd->view, NULL, 0, &needed) == MINIPSD_MORE && (name = (char *)vips_malloc(VIPS_OBJECT(psd), needed)) diff --git a/libvips-plugin/minipsd_vips_psdsave.c b/libvips-plugin/minipsd_vips_psdsave.c index 6919a43..98401b6 100644 --- a/libvips-plugin/minipsd_vips_psdsave.c +++ b/libvips-plugin/minipsd_vips_psdsave.c @@ -1,13 +1,14 @@ /** * \file - * \brief psdsave, psdsave_buffer, psdsave_target: an image as a one-layer PSD or PSB. + * \brief psdsave, psdsave_buffer, psdsave_target: an image as a PSD or PSB, one layer or flat. * * Laid out as libvips' own savers are (a base class over a VipsTarget; the file and buffer * variants make one), so the code can move into libvips/foreign/ (as psdsave.c, the minipsd_vips_ - * names back to vips_foreign_). The image is written as one layer that is also the composite: - * grey, RGB or CMYK, 8 or 16 bits, grey and RGB also 32, transparency kept or made opaque, CMYK - * back to PSD's inverted ink; the resolution, the ICC profile, EXIF, XMP and IPTC kept. The image - * is held whole in memory while it is written. + * names back to vips_foreign_). An image with alpha is written as one layer that is also the + * composite; one without, flat, as Photoshop saves an image without layers: the composite only, + * without transparency (with `layered`, one layer without transparency too). Grey, RGB or CMYK, + * 8 or 16 bits, grey and RGB also 32, CMYK back to PSD's inverted ink; the resolution, the ICC + * profile, EXIF, XMP and IPTC kept. The image is held whole in memory while it is written. * * minipsd's writer is compiled in here, private (MINIPSD_STATIC; the reader left out), so a * program that uses minipsd itself keeps its own. @@ -43,6 +44,7 @@ typedef struct _MinipsdVipsPsdsave { gboolean psb; /* the option; also a .psb file, or a side past PSD's */ gboolean zip; /* the option: ZIP the layer's channels */ char *layer_name; /* the option */ + gboolean layered; /* the option: a layer also for an image without alpha */ } MinipsdVipsPsdsave; typedef VipsForeignSaveClass MinipsdVipsPsdsaveClass; @@ -97,7 +99,6 @@ static int minipsd_vips_psdsave_build(VipsObject *object) { VipsForeignSave *save = (VipsForeignSave *)object; MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; - VipsImage **t = (VipsImage **)vips_object_local_array(object, 1); VipsImage *in; minipsd_resource resources[PSD_MAX_RESOURCES]; minipsd_layer layer; @@ -106,9 +107,9 @@ static int minipsd_vips_psdsave_build(VipsObject *object) const void *icc; const char *filename; size_t icc_size, bytes, colors, count = 0; - double ratio, full; + double ratio; void *pixels; - int rc; + int rc, alpha; if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->build(object)) return -1; in = save->ready; @@ -130,27 +131,25 @@ static int minipsd_vips_psdsave_build(VipsObject *object) } doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; - /* A layer always has transparency: opaque, when the image has none. */ - if ((size_t)in->Bands == colors) { - full = doc.depth == 32 ? 1.0 : doc.depth == 16 ? USHRT_MAX : UCHAR_MAX; - if (vips_bandjoin_const1(in, &t[0], full, NULL)) return -1; - in = t[0]; - } + /* Transparency as the image has it: none, none added. */ + alpha = (size_t)in->Bands > colors; if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; if (doc.color_mode == MINIPSD_MODE_CMYK) minipsd_vips__invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); memset(&layer, 0, sizeof layer); - layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; - layer.pixels = (const uint8_t *)pixels; - layer.opacity = UCHAR_MAX; + layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; + layer.pixels = (const uint8_t *)pixels; + layer.opacity = UCHAR_MAX; + layer.no_alpha = (uint8_t)!alpha; doc.width = (uint32_t)in->Xsize; doc.height = (uint32_t)in->Ysize; doc.dpi = in->Xres * MM_PER_INCH; if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; - doc.layers = &layer; - doc.layer_count = 1; + doc.layers = alpha || psd->layered ? &layer : NULL; /* without alpha: flat, unless asked */ + doc.layer_count = doc.layers ? 1 : 0; doc.composite = (const uint8_t *)pixels; /* one layer, at full opacity: the composite itself */ + doc.composite_alpha = (uint8_t)alpha; doc.compression = psd->zip ? MINIPSD_ZIP : MINIPSD_RLE; /* PSB by the option, a file named .psb (libvips writes files through a target too), or a side past PSD's. */ @@ -211,6 +210,8 @@ static void minipsd_vips_psdsave_class_init(MinipsdVipsPsdsaveClass *class) VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, zip), FALSE); VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, layer_name), PSD_LAYER_NAME); + VIPS_ARG_BOOL(class, "layered", 23, "Layered", "Without alpha, a layer too, without transparency (else flat)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, layered), FALSE); PSD_ARGS_END } diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index 0a1466b..cc44093 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -168,7 +168,8 @@ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *b } width = (uint32_t)(view.right - view.left); height = (uint32_t)(view.bottom - view.top); - planes = info.color_channels + 1u; + planes = info.color_channels; /* then transparency, when the layer has its channel */ + if (planes < MINIPSD_PLANES && view.channel[planes].size) ++planes; } if (planes > MINIPSD_PLANES) planes = MINIPSD_PLANES; bands = planes; @@ -381,6 +382,7 @@ static void check_indexed(const char *path, const uint8_t *indices, const uint8_ } static uint8_t *read_all(const char *path, size_t *size); +static size_t layer_count(const void *file, size_t size); /* The same samples: bytes, or floats within FLOAT_EPSILON. */ static int same_samples(const void *got, const void *want, size_t bytes, int floats) @@ -407,9 +409,10 @@ static VipsImage *make_image(const void *data, int bands, VipsBandFormat format, } /* An image saved through psdsave as a file, into a buffer and into a target, the same bytes each - time, then read back through psdload: the layer is the image (made opaque when it had no - alpha), and so is the composite when alpha is full (the matte undone exactly). options are a - psdsave option and its value, or NULL. */ + time, then read back through psdload as it was, its bands too: with alpha one layer, and the + composite when alpha is full (the matte undone exactly); without, a flat file, the composite + only, or with `layered` a layer without transparency as well. options are a psdsave option and + its value, or NULL. */ static void check_saved(VipsImage *image, const char *name, const char *dir, int full_alpha, const char *option, int value) { @@ -422,7 +425,7 @@ static void check_saved(VipsImage *image, const char *name, const char *dir, int size_t file_size = 0, buffer_size = 0, want_bytes = 0, got_bytes = 0, target_size = 0; const void *target_data = NULL; const char *saver; - int colors, saved, layer; + int colors, saved, layer, layered; if (!image) { check(0, "image made", name); return; @@ -456,24 +459,21 @@ static void check_saved(VipsImage *image, const char *name, const char *dir, int name); vips_error_clear(); - /* Read back: the layer, then the composite. */ - colors = image->Type == VIPS_INTERPRETATION_CMYK ? 4 : image->Bands >= 3 ? 3 : 1; - if (image->Bands == colors) { - vips_bandjoin_const1(image, &want_image, image->BandFmt == VIPS_FORMAT_FLOAT ? 1.0 - : image->BandFmt == VIPS_FORMAT_USHORT ? USHRT_MAX : UCHAR_MAX, NULL); - } else { - want_image = image; - g_object_ref(want_image); - } - if (want_image) want = vips_image_write_to_memory(want_image, &want_bytes); - for (layer = 0; layer >= COMPOSITE; --layer) { + /* Read back: the layer, when there is one, then the composite; the image as it is. */ + colors = image->Type == VIPS_INTERPRETATION_CMYK ? CMYK_COLORS : image->Bands >= 3 ? 3 : 1; + layered = image->Bands > colors || (option && strcmp(option, "layered") == 0 && value); + check(file && layer_count(file, file_size) == (layered ? 1u : 0u), layered ? "one layer" : "flat: no layer", name); + want_image = image; + g_object_ref(want_image); + want = vips_image_write_to_memory(want_image, &want_bytes); + for (layer = layered ? 0 : COMPOSITE; layer >= COMPOSITE; --layer) { if (layer == COMPOSITE && !full_alpha) break; back = vips_image_new_from_file(path, "layer", layer, NULL); got = back ? vips_image_write_to_memory(back, &got_bytes) : NULL; check(want && got && got_bytes == want_bytes && same_samples(got, want, want_bytes, layer == COMPOSITE && image->BandFmt == VIPS_FORMAT_FLOAT), layer == COMPOSITE ? "composite read back as the image" : "layer read back as the image", name); - if (back && layer == 0) + if (back && (layer == 0 || !layered)) check(fabs(vips_image_get_xres(back) - DPI / MM_PER_INCH) < EPSILON, "resolution read back", name); vips_error_clear(); g_free(got); @@ -537,8 +537,9 @@ static void check_bounded(const char *dir) for (i = 0; i < bytes; ++i) composite[i] = (uint8_t)(i % 2 ? UCHAR_MAX : i / 2 % TALL_W); memset(&doc, 0, sizeof doc); doc.width = TALL_W, doc.height = TALL_H, doc.dpi = DPI; - doc.color_mode = MINIPSD_MODE_GREY; - doc.composite = composite; + doc.color_mode = MINIPSD_MODE_GREY; + doc.composite = composite; + doc.composite_alpha = 1; /* grey and transparency */ check(minipsd_write(&doc, NULL, to_buffer, &b) == MINIPSD_OK, "tall image written", "bounded"); free(composite); snprintf(path, sizeof path, "%s/tall.psd", dir); @@ -682,7 +683,7 @@ int main(int argc, char **argv) layer.opacity = UCHAR_MAX; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; - doc.layers = &layer, doc.layer_count = 1; + doc.layers = &layer, doc.layer_count = 1, doc.composite_alpha = 1; /* RGBA, as the layer */ doc.icc = icc, doc.icc_size = sizeof icc; doc.pixel_aspect_ratio = ASPECT; check_document(&doc, NULL, &psd, argv[1], path, sizeof path); @@ -696,7 +697,7 @@ int main(int argc, char **argv) layer.pixels = (const uint8_t *)rgba16; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; - doc.layers = &layer, doc.layer_count = 1; + doc.layers = &layer, doc.layer_count = 1, doc.composite_alpha = 1; /* RGBA, as the layer */ doc.depth = 16; check_document(&doc, NULL, &e, argv[1], path, sizeof path); check_layers(path, &doc, sizeof(uint16_t), e.name); @@ -708,7 +709,7 @@ int main(int argc, char **argv) layer.pixels = (const uint8_t *)grey16; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; - doc.layers = &layer, doc.layer_count = 1; + doc.layers = &layer, doc.layer_count = 1, doc.composite_alpha = 1; /* RGBA, as the layer */ doc.color_mode = MINIPSD_MODE_GREY, doc.depth = 16; doc.composite = (const uint8_t *)grey16; check_document(&doc, NULL, &e, argv[1], path, sizeof path); @@ -719,7 +720,7 @@ int main(int argc, char **argv) layer.pixels = cmyka; memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; - doc.layers = &layer, doc.layer_count = 1; + doc.layers = &layer, doc.layer_count = 1, doc.composite_alpha = 1; /* RGBA, as the layer */ doc.color_mode = MINIPSD_MODE_CMYK; doc.composite = cmyka; check_document(&doc, NULL, &e, argv[1], path, sizeof path); @@ -767,7 +768,7 @@ int main(int argc, char **argv) } memset(&doc, 0, sizeof doc); doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; - doc.layers = zip_layers, doc.layer_count = 2; + doc.layers = zip_layers, doc.layer_count = 2, doc.composite_alpha = 1; /* composed with their transparency */ doc.compression = MINIPSD_ZIP; memset(&opts, 0, sizeof opts); opts.deflate = deflate_zlib; @@ -800,10 +801,11 @@ int main(int argc, char **argv) check(stored_sample(path, 0) == UCHAR_MAX - cmyka[0] && stored_sample(path, 3) == UCHAR_MAX - cmyka[3], "CMYK stored as PSD's inverted ink", path); check_saved(make_image(rgbf, 3, VIPS_FORMAT_FLOAT, VIPS_INTERPRETATION_scRGB), "save-float.psd", argv[1], 1, NULL, 0); - check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 1, "zip", TRUE); + check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-layered.psd", argv[1], 1, "layered", TRUE); + check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 0, "zip", TRUE); snprintf(path, sizeof path, "%s/save-zip.psd", argv[1]); check(first_layer_compression(path) == STORED_ZIP, "ZIP by option", path); - snprintf(path, sizeof path, "%s/save-rgb8.psd", argv[1]); + snprintf(path, sizeof path, "%s/save-rgba8.psd", argv[1]); check(first_layer_compression(path) == STORED_RLE, "RLE by default", path); check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-psb.psd", argv[1], 1, "psb", TRUE); snprintf(path, sizeof path, "%s/save-psb.psd", argv[1]); @@ -815,7 +817,7 @@ int main(int argc, char **argv) image = make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB); check(image && vips_image_write_to_file(image, path, NULL) == 0, "saved", path); if (image) g_object_unref(image); - back = vips_image_new_from_file(path, "layer", 0, NULL); + back = vips_image_new_from_file(path, NULL); check(back != NULL, "read back", path); if (back) g_object_unref(back); file = read_all(path, &size); @@ -829,7 +831,8 @@ int main(int argc, char **argv) want_icc = vips_blob_get(profile, &want_icc_size); if (image) vips_image_set_blob_copy(image, VIPS_META_ICC_NAME, want_icc, want_icc_size); } - check(image && vips_call("psdsave", image, path, "layer_name", "Ink", NULL) == 0, "saved by psdsave", path); + check(image && vips_call("psdsave", image, path, "layer_name", "Ink", "layered", TRUE, NULL) == 0, "saved by psdsave", + path); back = vips_image_new_from_file(path, "layer", 0, NULL); check(back && vips_image_get_string(back, "psd-layer-name", &got_name) == 0 && strcmp(got_name, "Ink") == 0, "layer name kept", path); diff --git a/tests/minipsd_c_smoke.c b/tests/minipsd_c_smoke.c index 005ce48..5b46fd1 100644 --- a/tests/minipsd_c_smoke.c +++ b/tests/minipsd_c_smoke.c @@ -45,7 +45,7 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size) layers[1].name = "Layer"; layers[1].pixels = NULL; layers[1].opacity = 255; layers[1].hidden = 0; layers[1].blending_ranges = &blend_if; layers[1].blending_range_count = 1; memset(&doc, 0, sizeof doc); - doc.width = 3; doc.height = 2; doc.dpi = 300.0; doc.layers = layers; doc.layer_count = 2; + doc.width = 3; doc.height = 2; doc.dpi = 300.0; doc.layers = layers; doc.layer_count = 2; doc.composite_alpha = 1; /* Every function, so a static build has none unused. */ if (!minipsd_error_string(MINIPSD_OK)[0] || minipsd_doc_check(&doc, NULL) != MINIPSD_OK) diff --git a/tests/psd_tools/roundtrip.py b/tests/psd_tools/roundtrip.py index c45331d..cd5c9c1 100644 --- a/tests/psd_tools/roundtrip.py +++ b/tests/psd_tools/roundtrip.py @@ -113,7 +113,7 @@ class Layer(ctypes.Structure): ("additional_info", Bytes), ("stored", ctypes.c_void_p), ("section_divider_type", ctypes.c_uint8), ("mask", ctypes.POINTER(Mask)), ("blending_ranges", ctypes.POINTER(BlendingRange)), ("blending_range_count", ctypes.c_uint32), - ("name_pascal", Bytes)] + ("name_pascal", Bytes), ("no_alpha", ctypes.c_uint8)] class Doc(ctypes.Structure): @@ -323,6 +323,7 @@ def write(lib, path, w, h, dpi, compression, layers, fmt=0): layer[5] if len(layer) > 5 else b"\0\0\0\0") doc = Doc(w, h, dpi, records, len(layers), None, 0, compression) doc.format = fmt + doc.composite_alpha = 1 # composed with the layers' transparency out = bytearray() def collect(_user, data, size): @@ -441,6 +442,7 @@ def pixels(lw, lh, colour): records[i] = Layer(name=name.encode(), pixels=rgba, opacity=255, blend_mode=blend, section_divider_type=kind, mask=m) records[2].blending_ranges, records[2].blending_range_count = ranges, len(BLEND_IF) doc = Doc(w, h, 72.0, records, len(spec), None, 0, MINIPSD_RLE) + doc.composite_alpha = 1 out = bytearray() def collect(_user, data, size): @@ -504,7 +506,7 @@ def samples(values): records[1] = Layer(name=b"Masked", pixels=samples(top), opacity=255, top=5, left=10, bottom=5 + lh, right=10 + lw, mask=ctypes.pointer(mask)) doc = Doc(w, h, 72.0, records, 2, None, 0, compression) - doc.depth = 16 + doc.depth, doc.composite_alpha = 16, 1 out = bytearray() def collect(_user, data, size): @@ -596,6 +598,7 @@ def image(width, height, seed, alpha): records[1] = Layer(name=b"Top", pixels=samples(top), opacity=255, top=3, left=4, bottom=3 + lh, right=4 + lw) doc = Doc(w, h, 72.0, records, 2, None, 0, compression) doc.composite, doc.depth, doc.color_mode = samples(flat), depth, mode + doc.composite_alpha = 1 # the colour channels, then transparency if mode == MODE_DUOTONE: data = ctypes.create_string_buffer(DUOTONE_DATA, len(DUOTONE_DATA)) keep.append(data) @@ -655,9 +658,12 @@ def plane(values, k): MODE_BITMAP, MODE_INDEXED, MODE_MULTICHANNEL = 0, 2, 7 # The flat modes: no layers, no transparency. Bitmap's composite is a byte a pixel, packed by the -# writer; indexed's one palette entry a pixel; multichannel's only copied, as a file stores it. +# writer; indexed's one palette entry a pixel; multichannel's only copied, as a file stores it. And +# RGB and grey without transparency (composite_alpha 0): flat too, as Photoshop saves an image +# without layers, the composite supplied as the mode's channels only. FLAT = [(MODE_BITMAP, 1, MINIPSD_RLE), (MODE_BITMAP, 1, MINIPSD_RAW), (MODE_INDEXED, 8, MINIPSD_RLE), - (MODE_MULTICHANNEL, 8, MINIPSD_RLE), (MODE_MULTICHANNEL, 16, MINIPSD_RLE)] + (MODE_MULTICHANNEL, 8, MINIPSD_RLE), (MODE_MULTICHANNEL, 16, MINIPSD_RLE), + (MODE_RGB, 8, MINIPSD_RLE), (MODE_GREY, 16, MINIPSD_RLE)] PALETTE = bytes(i * 5 % 256 for i in range(768)) MULTICHANNELS = 3 @@ -691,10 +697,15 @@ def buffer(data): want = [bytes(values)] else: spread = ((1 << depth) - 1) // 255 # the 8-bit pattern over the depth's whole range - want = [b"".join(((c * 61 + i * 13) % 256 * spread).to_bytes(sample, "big") for i in range(w * h)) - for c in range(MULTICHANNELS)] - stored = buffer(bytes(2) + b"".join(want)) # the file's marker 0: raw - doc.composite_stored, doc.composite_channels = Bytes(ctypes.cast(stored, ctypes.c_void_p), len(stored)), MULTICHANNELS + channels = MODE_COLORS.get(mode, MULTICHANNELS) + values = [[(c * 61 + i * 13) % 256 * spread for i in range(w * h)] for c in range(channels)] + want = [b"".join(v.to_bytes(sample, "big") for v in plane) for plane in values] + if mode == MODE_MULTICHANNEL: + stored = buffer(bytes(2) + b"".join(want)) # the file's marker 0: raw + doc.composite_stored, doc.composite_channels = Bytes(ctypes.cast(stored, ctypes.c_void_p), len(stored)), channels + else: # supplied, interleaved, in host order: the colour channels and nothing after them + doc.composite = ctypes.cast(buffer(b"".join(values[c][i].to_bytes(sample, sys.byteorder) + for i in range(w * h) for c in range(channels))), ctypes.c_void_p) out = bytearray() def collect(_user, data, size): @@ -787,6 +798,7 @@ def made(fn, *args): layer[0] = Layer(name=b"Layer", pixels=ctypes.cast(pixels, ctypes.c_void_p), opacity=255, additional_info=made(lib.minipsd_layer_make_block_settings, None, 0, ctypes.byref(settings))) doc = Doc(2, 2, 72.0, layer, 1, None, 0, MINIPSD_RLE) + doc.composite_alpha = 1 doc.resources, doc.resource_count = ctypes.cast(resources, ctypes.c_void_p), len(resources) doc.pixel_aspect_ratio = SETTER_ASPECT out = bytearray() @@ -990,9 +1002,8 @@ def data(rec, c, d, version): same(f"layer {i} blocks", blocks(b.tagged_blocks), blocks(a.tagged_blocks)) same(f"layer {i} mask data", b.mask_data.tobytes() if b.mask_data else None, a.mask_data.tobytes() if a.mask_data else None) same(f"layer {i} blending ranges", b.blending_ranges.tobytes(), a.blending_ranges.tobytes()) - ids = [c.id.value for c in b.channel_info] - added = [] if -1 in ids or stored else [-1] # decoded, a layer without transparency gets an opaque one - same(f"layer {i} channels", sorted(added + ids), sorted(c.id.value for c in a.channel_info)) + ids = [c.id.value for c in b.channel_info] # a layer without transparency stays without, decoded too + same(f"layer {i} channels", sorted(ids), sorted(c.id.value for c in a.channel_info)) same(f"layer {i} rectangle", rect(b), rect(a)) same(f"layer {i} names", (b.name, blocks(b.tagged_blocks).get(b"luni")), (a.name, blocks(a.tagged_blocks).get(b"luni"))) same(f"layer {i} opacity", b.opacity, a.opacity) diff --git a/tests/support.h b/tests/support.h index b96f592..29ebcf8 100644 --- a/tests/support.h +++ b/tests/support.h @@ -232,6 +232,7 @@ minipsd_doc docOf(std::uint32_t w, std::uint32_t h, const minipsd_layer *layers, { minipsd_doc d{}; d.width = w; d.height = h; d.dpi = 300.0; d.layers = layers; d.layer_count = n; + d.composite_alpha = n != 0; // with layers: composed with their transparency, as most tests want return d; } diff --git a/tests/test_golden.cpp b/tests/test_golden.cpp index bdcc861..b19ed08 100644 --- a/tests/test_golden.cpp +++ b/tests/test_golden.cpp @@ -136,8 +136,10 @@ TEST(MinipsdGoldenTest, OutputMatchesTheBytesOf010) for (const Case &t : cases) { SCOPED_TRACE(t.golden.name); + minipsd_doc doc = t.doc; + doc.composite_alpha = 1; // 0.1.0 always composed the composite with transparency Bytes out; - const int rc = minipsd_write(&t.doc, nullptr, collect, &out); + const int rc = minipsd_write(&doc, nullptr, collect, &out); ASSERT_EQ(rc, MINIPSD_OK) << minipsd_error_string(rc); EXPECT_EQ(sha256(out), t.golden.sha256) << "the bytes of '" << t.golden.name << "' changed"; // encode(decode(x)) == x, through minipsd_read; the file's buffer is freed before writing. @@ -153,9 +155,9 @@ TEST(MinipsdGoldenTest, OutputMatchesTheBytesOf010) ASSERT_EQ(minipsd_read(out.data(), out.size(), nullptr, &stored, &back, nullptr), MINIPSD_OK); EXPECT_EQ(write(*back), out) << "channels copied as stored: the same bytes"; minipsd_doc_free(back); - EXPECT_EQ(pullTotal(t.doc), out.size()) << "the pull writer knows the size before writing"; + EXPECT_EQ(pullTotal(doc), out.size()) << "the pull writer knows the size before writing"; for (std::size_t cap : {std::size_t(1), std::size_t(7), std::size_t(4096), out.size(), std::size_t(0)}) - EXPECT_EQ(pull(t.doc, cap), out) << "pulled " << (cap ? std::to_string(cap) : "random") << " bytes at a time"; + EXPECT_EQ(pull(doc, cap), out) << "pulled " << (cap ? std::to_string(cap) : "random") << " bytes at a time"; } } diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp index 97f2bc6..daa402b 100644 --- a/tests/test_reader.cpp +++ b/tests/test_reader.cpp @@ -2434,13 +2434,14 @@ TEST(MinipsdReadTest, ItRefusesWhatTheReaderRefusesAndWhatItCannotHold) readDoc(build(big).bytes, &lost, MINIPSD_ERR_UNSUPPORTED); } - // A flat file over the limit: its image counts once as the composite, once as its layer. + // A flat file over the limit: its image counts once as the composite, once as its layer, each + // R, G, B: it has no transparency. Spec flat; flat.channels = 3; flat.count = 0; flat.image = rawImage(3, 2, 2, 0); minipsd_limits small{}; - small.max_decoded_bytes = 2 * 2 * 4 * 2 - 1; + small.max_decoded_bytes = 2 * 2 * 3 * 2 - 1; readDoc(build(flat).bytes, &lost, MINIPSD_ERR_LIMIT, &small); // A file cut inside its layer tail: everything before it is read, then the composite is short. @@ -2636,9 +2637,17 @@ TEST(MinipsdReadTest, AFlatFileBecomesOneBackgroundLayer) EXPECT_DOUBLE_EQ(d->dpi, 72.0) << "no resolution stored"; ASSERT_EQ(d->layer_count, 1u); EXPECT_STREQ(d->layers[0].name, "Background"); - EXPECT_EQ(Bytes(d->layers[0].pixels, d->layers[0].pixels + 16), (Bytes{0x40, 0x40, 0x40, 255, 0x40, 0x40, 0x40, 255, - 0x40, 0x40, 0x40, 255, 0x40, 0x40, 0x40, 255})); + EXPECT_EQ(d->layers[0].no_alpha, 1) << "the file has no transparency, nor has its layer"; + EXPECT_EQ(Bytes(d->layers[0].pixels, d->layers[0].pixels + 12), Bytes(12, 0x40)) << "R, G, B"; + EXPECT_EQ(d->composite_alpha, 0); EXPECT_EQ(minipsd_doc_check(d.get(), nullptr), MINIPSD_OK) << "the writer takes it as it is"; + { // written back: still without transparency, the layer and the composite R, G, B + const Psd p = read(write(*d)); + EXPECT_EQ(p.channels, 3) << "the composite"; + EXPECT_EQ(p.rawLayerCount, 1) << "positive: no transparency"; + ASSERT_EQ(p.layers.size(), 1u); + EXPECT_EQ(p.layers[0].channelIds, (std::vector{0, 1, 2})) << "no channel -1"; + } // Stored: flat as the file is, no layer, its image the composite, copied. With a layer and // mask section or without one (its length 0), it is written with minipsd's empty one. @@ -2664,8 +2673,8 @@ TEST(MinipsdReadTest, AFlatFileBecomesOneBackgroundLayer) } } -// A flat 16-bit file: its one layer decoded at 16 bits, opaque as 65535. -TEST(MinipsdReadTest, AFlat16BitFileIsOpaqueAt65535) +// A flat 16-bit file: its one layer decoded at 16 bits, R, G, B without transparency. +TEST(MinipsdReadTest, AFlat16BitFileDecodesAt16BitsWithoutTransparency) { Spec s; s.channels = 3; @@ -2675,10 +2684,10 @@ TEST(MinipsdReadTest, AFlat16BitFileIsOpaqueAt65535) const Doc d = readDoc(build(s).bytes, nullptr); ASSERT_TRUE(d); EXPECT_EQ(d->depth, 16); - std::uint16_t px[4]; + EXPECT_EQ(d->layers[0].no_alpha, 1); + std::uint16_t px[3 * 4]; // four pixels of R, G, B std::memcpy(px, d->layers[0].pixels, sizeof px); - EXPECT_EQ(px[0], 0x4040); - EXPECT_EQ(px[3], 65535); + for (const std::uint16_t v : px) EXPECT_EQ(v, 0x4040); } // Modes beyond 8- and 16-bit RGB come back as written: a 32-bit document's layers in 'Lr32', its @@ -2730,8 +2739,9 @@ TEST(MinipsdReadTest, EveryModeComesBackAsWritten) EXPECT_EQ(names(again), photoshops) << "kept, not written twice"; const std::vector greyAlpha = {10, 255, 20, 128}; // 2 x 1, grey and transparency minipsd_doc flat = docOf(2, 1, nullptr, 0); - flat.color_mode = MINIPSD_MODE_GREY; - flat.composite = greyAlpha.data(); + flat.color_mode = MINIPSD_MODE_GREY; + flat.composite = greyAlpha.data(); + flat.composite_alpha = 1; const Bytes noLayers = write(flat); EXPECT_EQ(names(noLayers), photoshops) << "no layers"; const Bytes mtrn = {'8', 'B', 'I', 'M', 'M', 't', 'r', 'n', 0, 0, 0, 0}; @@ -2756,6 +2766,54 @@ TEST(MinipsdReadTest, EveryModeComesBackAsWritten) EXPECT_EQ(minipsd_reader_find_resource(o.r, MINIPSD_RES_ALPHA_IDENTIFIERS, &res), MINIPSD_ERR_NOT_FOUND) << "none of the writer's"; } + // Without transparency (composite_alpha 0): the mode's channels only, nothing named, no 'Mtrn', + // as Photoshop saves an image without layers. Read, its one layer has none either; stored, it + // is written back the same bytes. + const std::vector grey = {10, 20}; // 2 x 1, grey only + minipsd_doc plain = docOf(2, 1, nullptr, 0); + plain.color_mode = MINIPSD_MODE_GREY; + plain.composite = grey.data(); + const Bytes noAlpha = write(plain); + { + Open o(noAlpha); + ASSERT_EQ(o.rc, MINIPSD_OK); + EXPECT_EQ(o.info.channels, 1); + EXPECT_EQ(o.info.composite_has_alpha, 0); + minipsd_resource res{}; + EXPECT_EQ(minipsd_reader_find_resource(o.r, MINIPSD_RES_ALPHA_NAMES, &res), MINIPSD_ERR_NOT_FOUND) << "nothing to name"; + } + EXPECT_EQ(std::search(noAlpha.begin(), noAlpha.end(), mtrn.begin(), mtrn.end()), noAlpha.end()) << "no 'Mtrn'"; + const Doc pd = readDoc(noAlpha, nullptr); + ASSERT_TRUE(pd); + EXPECT_EQ(pd->composite_alpha, 0); + ASSERT_EQ(pd->layer_count, 1u); + EXPECT_EQ(pd->layers[0].no_alpha, 1); + EXPECT_EQ(Bytes(pd->layers[0].pixels, pd->layers[0].pixels + grey.size()), Bytes(grey.begin(), grey.end())); + const minipsd_read_opts keep{1}; + const Doc ps = readDoc(noAlpha, nullptr, MINIPSD_OK, nullptr, &keep); + ASSERT_TRUE(ps); + EXPECT_EQ(write(*ps), noAlpha) << "stored: the same bytes"; + + // A layer without transparency (no_alpha), the composite composed without it: R, G, B in both, + // the count positive; read back without it, and a second pass the same bytes. + const Bytes rgb = {200, 100, 50, 10, 20, 30}; // 2 x 1 + const minipsd_layer bg{.name = "Background", .pixels = rgb.data(), .opacity = 255, .no_alpha = 1}; + minipsd_doc layered = docOf(2, 1, &bg, 1); + layered.composite_alpha = 0; + const Bytes opaqueLayer = write(layered); + const Psd q = read(opaqueLayer); + EXPECT_EQ(q.channels, 3); + EXPECT_EQ(q.rawLayerCount, 1); + ASSERT_EQ(q.layers.size(), 1u); + EXPECT_EQ(q.layers[0].channelIds, (std::vector{0, 1, 2})) << "no channel -1"; + for (std::size_t i = 0; i < 2; ++i) + for (std::size_t c = 0; c < 3; ++c) EXPECT_EQ(q.composite[i * 4 + c], rgb[i * 3 + c]) << "opaque: composed as given"; + const Doc lb = readDoc(opaqueLayer, nullptr); + ASSERT_TRUE(lb); + EXPECT_EQ(lb->layers[0].no_alpha, 1); + EXPECT_EQ(Bytes(lb->layers[0].pixels, lb->layers[0].pixels + rgb.size()), rgb); + EXPECT_EQ(write(*lb), opaqueLayer) << "read and written: the same bytes"; + const Bytes bits = {0, 1, 0, 0, 7, 0, 0, 0, 255, 1}; // 10 x 1: 0 white, any other black minipsd_doc b = docOf(10, 1, nullptr, 0); b.depth = 1; @@ -3220,9 +3278,31 @@ void expectConvertsBack(const minipsd_doc &d) EXPECT_EQ(write(*c, MINIPSD_OK, &back), as) << "into the other format and back: the same bytes"; } +// A document's pixels of `planes` samples, `bytes` each, as RGBA: opaque where they have no +// transparency, as the reader's RGBA targets decode a file without it. +Bytes asRgba(const std::uint8_t *p, std::size_t count, unsigned planes, std::size_t bytes) +{ + Bytes out(count * 4 * bytes, 0xFF); + for (std::size_t i = 0; i < count; ++i) + std::copy(p + i * planes * bytes, p + (i + 1) * planes * bytes, out.begin() + static_cast(i * 4 * bytes)); + return out; +} + +// A document's composite as RGBA: its colour, and its transparency when it has one. +Bytes compositeOf(const minipsd_doc &d, std::size_t bytes) +{ + return asRgba(d.composite, std::size_t(d.width) * d.height, 3u + d.composite_alpha, bytes); +} + +// A layer's pixels as RGBA. +Bytes pixelsOf(const minipsd_layer &l, std::size_t bytes) +{ + return asRgba(l.pixels, std::size_t(l.right - l.left) * std::size_t(l.bottom - l.top), 3u + !l.no_alpha, bytes); +} + // A document read with stored: its composite copied as the file ends with it, not decoded, when // it is R, G, B and at most transparency; otherwise decoded, as stored (matted). -void expectStoredComposite(const minipsd_doc &s, const Bytes &file, std::size_t size, const Bytes &stored) +void expectStoredComposite(const minipsd_doc &s, const Bytes &file, std::size_t bytes, const Bytes &stored) { if (s.composite_stored.size) { EXPECT_EQ(s.composite, nullptr) << "copied, not decoded"; @@ -3230,7 +3310,7 @@ void expectStoredComposite(const minipsd_doc &s, const Bytes &file, std::size_t EXPECT_EQ(Bytes(s.composite_stored.data, s.composite_stored.data + s.composite_stored.size), Bytes(file.end() - static_cast(s.composite_stored.size), file.end())); } else { - EXPECT_EQ(Bytes(s.composite, s.composite + size), stored); + EXPECT_EQ(compositeOf(s, bytes), stored); } } @@ -3311,7 +3391,7 @@ TEST(MinipsdReaderTest, FilesFromOtherProgramsAreReadWholeAndInParts) ASSERT_TRUE(s); expectConvertsBack(*s); EXPECT_EQ(s->depth, depth); - expectStoredComposite(*s, file, composite.size(), stored); + expectStoredComposite(*s, file, bytes, stored); const Bytes again = write(*s); expectKeptAlike(file, again); const Doc f = readDoc(again, &lostAgain, MINIPSD_OK, nullptr, &keep); @@ -3337,7 +3417,7 @@ TEST(MinipsdReaderTest, FilesFromOtherProgramsAreReadWholeAndInParts) const Doc d = readDoc(file, &lost); ASSERT_TRUE(d); if (otherChannels) { EXPECT_NE(lost & MINIPSD_LOST_CHANNELS, 0u) << "lost " << lost; } - EXPECT_EQ(Bytes(d->composite, d->composite + composite.size()), stored); + EXPECT_EQ(compositeOf(*d, bytes), stored); EXPECT_EQ(d->depth, depth); const Bytes again = write(*d); std::uint32_t lostAgain = 99; @@ -3351,11 +3431,12 @@ TEST(MinipsdReaderTest, FilesFromOtherProgramsAreReadWholeAndInParts) EXPECT_STREQ(a.name, b.name); EXPECT_EQ(std::tie(a.opacity, a.hidden, a.top, a.left, a.bottom, a.right), std::tie(b.opacity, b.hidden, b.top, b.left, b.bottom, b.right)); - const std::size_t n = std::size_t(a.right - a.left) * std::size_t(a.bottom - a.top) * 4 * bytes; EXPECT_EQ(a.pixels == nullptr, b.pixels == nullptr); - if (a.pixels && b.pixels) { EXPECT_TRUE(std::equal(a.pixels, a.pixels + n, b.pixels)); } + EXPECT_EQ(a.no_alpha, b.no_alpha) << "without transparency, written and read back without"; + if (a.pixels && b.pixels) { EXPECT_EQ(pixelsOf(a, bytes), pixelsOf(b, bytes)); } } - EXPECT_TRUE(std::equal(d->composite, d->composite + composite.size(), e->composite)); + EXPECT_EQ(d->composite_alpha, e->composite_alpha); + EXPECT_EQ(compositeOf(*d, bytes), compositeOf(*e, bytes)); expectKeptAlike(file, again); // Stored: no layer decoded, yet the file written from it decodes to the same pixels. @@ -3363,17 +3444,16 @@ TEST(MinipsdReaderTest, FilesFromOtherProgramsAreReadWholeAndInParts) const Doc s = readDoc(file, &lost, MINIPSD_OK, nullptr, &keep); ASSERT_TRUE(s); expectConvertsBack(*s); - expectStoredComposite(*s, file, composite.size(), stored); + expectStoredComposite(*s, file, bytes, stored); const Doc f = readDoc(write(*s), &lostAgain); ASSERT_TRUE(f); - EXPECT_EQ(Bytes(f->composite, f->composite + composite.size()), stored) << "written back, the same composite"; + EXPECT_EQ(compositeOf(*f, bytes), stored) << "written back, the same composite"; ASSERT_EQ(f->layer_count, d->layer_count); for (std::size_t i = 0; i < d->layer_count; ++i) { const minipsd_layer &a = d->layers[i], &b = f->layers[i]; SCOPED_TRACE(i); - const std::size_t n = std::size_t(a.right - a.left) * std::size_t(a.bottom - a.top) * 4 * bytes; EXPECT_EQ(a.pixels == nullptr, b.pixels == nullptr); - if (a.pixels && b.pixels) { EXPECT_TRUE(std::equal(a.pixels, a.pixels + n, b.pixels)); } + if (a.pixels && b.pixels) { EXPECT_EQ(pixelsOf(a, bytes), pixelsOf(b, bytes)); } } } diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index 8cee7f8..74910d2 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -173,7 +173,8 @@ TEST(MinipsdWriterTest, ASuppliedCompositeIsMattedAndWrittenWithoutComposing) d.composite_stored = {planes.data(), planes.size()}; d.composite_channels = 3; EXPECT_EQ(write(d), withComposite) << "the supplied composite wins"; - d.composite = nullptr; + d.composite = nullptr; + d.composite_alpha = 0; // what composite_alpha says is now the stored one's: R, G, B const Bytes copied = write(d); EXPECT_EQ(composedRows, 0) << "nothing composed"; EXPECT_EQ(Bytes(copied.end() - static_cast(planes.size()), copied.end()), planes) << "copied as it is"; @@ -607,7 +608,7 @@ TEST(MinipsdWriterTest, SixteenBitDocumentsAreWrittenAndReadBack) check(file); // As a document, decoded or stored: the same bytes. A stored layer without transparency - // gets an opaque one. + // reads back without one: R, G, B. const minipsd_read_opts inflating{0, zlib_stored_inflate, nullptr}; for (int keep : {0, 1}) { const minipsd_read_opts opts{keep, zlib_stored_inflate, nullptr}; @@ -629,8 +630,10 @@ TEST(MinipsdWriterTest, SixteenBitDocumentsAreWrittenAndReadBack) minipsd_doc *opaque = nullptr; const Bytes f = write(e, MINIPSD_OK, &zipped); ASSERT_EQ(minipsd_read(f.data(), f.size(), nullptr, &inflating, &opaque, nullptr), MINIPSD_OK); + EXPECT_EQ(opaque->layers[0].no_alpha, 1); const auto *got = reinterpret_cast(opaque->layers[0].pixels); - for (std::size_t i = 0; i < px.size(); ++i) ASSERT_EQ(got[i], i % 4 == 3 ? 65535 : px[i]) << i; + for (std::size_t i = 0; i < px.size() / 4; ++i) + for (std::size_t k = 0; k < 3; ++k) ASSERT_EQ(got[i * 3 + k], px[i * 4 + k]) << i; minipsd_doc_free(opaque); } minipsd_doc_free(back); @@ -1086,7 +1089,8 @@ TEST(MinipsdWriterTest, RefusesBeforeWritingAnything) d = docOf(2, 2, nullptr, 1); EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "a layer count, no layers"; d = docOf(2, 2, &layer, 0); - d.composite = px.data(); + d.composite = px.data(); + d.composite_alpha = 1; // RGBA EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "no layers, a composite: flat"; { const Psd flat = read(write(d)); From cfc3161078c3f597cdfad525c08ba4b236784073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 11:58:55 +0100 Subject: [PATCH 10/22] perf(vips): psdsave encodes every channel once minipsd's writer, keeping nothing between pieces, encodes a layer row three times (its channel's size, its own size, the row) and builds and mattes a composite row five times. psdsave now encodes each channel once, into its own buffer, and hands them to minipsd as stored channels and composite_stored, which it copies: an 8000 x 8000 RGBA image saves in 0.7 s instead of 2.4, the file byte for byte the same; without alpha, flat, in 0.3 s. minipsd gains the two pieces that needs, the writer's own: minipsd_encode_row, a channel's row as the file stores it, PackBits or raw; and minipsd_matte_row, a composite's row matted against white as the writer mattes it (RGB; every other mode as given). The writer itself is unchanged. The plugin's test compares every file psdsave writes with what minipsd_write makes from the pixels: the same bytes. --- include/minipsd.h | 53 +++++ include/minipsd/detail/composite.h | 35 +-- include/minipsd/writer.h | 55 ++++- libvips-plugin/minipsd_vips_psdsave.c | 297 +++++++++++++++++++++----- libvips-plugin/test_vips.c | 65 ++++++ tests/minipsd_c_smoke.c | 9 + tests/test_writer.cpp | 41 ++++ 7 files changed, 485 insertions(+), 70 deletions(-) diff --git a/include/minipsd.h b/include/minipsd.h index afa56dd..6dbf6c5 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -814,6 +814,59 @@ MINIPSDDEF uint64_t minipsd_writer_total(const minipsd_writer *w); * \retval MINIPSD_ERR_ARG w or produced is NULL, or buf is NULL with cap above 0. */ MINIPSDDEF int minipsd_writer_fill(minipsd_writer *w, void *buf, size_t cap, size_t *produced); + +/** \brief The bytes a row of width samples of depth bits (1, 8, 16, 32) takes in a file, raw. */ +#define MINIPSD_ROW_BYTES(width, depth) (((uint64_t)(width) * (uint64_t)(depth) + 7) / 8) + +/** \brief The most bytes minipsd_encode_row writes for that row: raw, or PackBits' worst case, a + header byte for each 128 bytes. */ +#define MINIPSD_ROW_BOUND(width, depth) (MINIPSD_ROW_BYTES(width, depth) + (MINIPSD_ROW_BYTES(width, depth) + 127) / 128) + +/** + * \brief Encodes one row of one channel as minipsd_write encodes it: PackBits, or raw, its samples + * big-endian as the file stores them. + * + * With every row of a channel encoded so, its compression marker (two bytes, big-endian: 1 for + * PackBits, 0 raw) and, for PackBits, each row's size ahead of the rows (two bytes in a PSD, four + * in a PSB), they are the channel as a file stores it: what minipsd_layer.stored and + * minipsd_doc.composite_stored take, written then as they are. A program that holds its image + * whole or reads it once, top to bottom, encodes each channel once this way, where minipsd_write, + * which keeps nothing between pieces, encodes a row again for its size. + * + * \param compression MINIPSD_RLE or MINIPSD_RAW. + * \param depth Bits a sample: 8, 16 (a uint16_t in host order), 32 (a float); at 1, the + * row already packed, eight pixels a byte, step 1. + * \param samples The row's first sample. + * \param step Bytes from a sample to the next: the pixel's size for one channel out of + * interleaved pixels. + * \param width Samples in the row. + * \param out Receives the row; cap bytes, MINIPSD_ROW_BOUND(width, depth) always enough. + * \param cap The size of out. + * \param size Receives the bytes written; with MINIPSD_ERR_SIZE, the bytes it needs. + * \return MINIPSD_OK or an error code. + * + * \retval MINIPSD_ERR_ARG samples, out or size is NULL, the compression is not MINIPSD_RLE or + * MINIPSD_RAW, or the depth not 1, 8, 16 or 32. + * \retval MINIPSD_ERR_SIZE The row does not fit in cap bytes. + */ +MINIPSDDEF int minipsd_encode_row(int compression, int depth, const void *samples, size_t step, uint32_t width, + uint8_t *out, size_t cap, size_t *size); + +/** + * \brief Mattes a row of a composite against white in place, as minipsd_write stores a supplied + * composite with transparency: c * a + max * (max - a), rounded (at 32 bits c * a + 1 - a). + * + * minipsd_write mattes an RGB composite only; every other mode is left as given, and so it is + * here. A program that encodes a composite itself (minipsd_encode_row, for composite_stored) + * mattes each row first, as minipsd_write would. + * + * \param color_mode MINIPSD_MODE_*. + * \param depth Bits a sample: 8, 16 or 32. + * \param pixels width pixels: the mode's colour channels, then transparency. + * \param width Pixels in the row. + * \return MINIPSD_OK, or MINIPSD_ERR_ARG when pixels is NULL or the depth not 8, 16 or 32. + */ +MINIPSDDEF int minipsd_matte_row(int color_mode, int depth, void *pixels, uint32_t width); #endif /* MINIPSD_NO_WRITER */ #ifndef MINIPSD_NO_READER diff --git a/include/minipsd/detail/composite.h b/include/minipsd/detail/composite.h index 87a9932..cc3abd8 100644 --- a/include/minipsd/detail/composite.h +++ b/include/minipsd/detail/composite.h @@ -136,6 +136,27 @@ MPSD__SHARED static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint3 } } +/* width pixels of `pixel` samples matted against white in place, as the merged image is stored: + each of the colour channels before the alpha at `colors`, c * a + max * (max - a) rounded, or at + 32 bits (floats) c * a + 1 - a. */ +MPSD__SHARED static void mpsd__matte(uint8_t *row, uint32_t width, unsigned pixel, unsigned colors, unsigned wide, uint32_t max) +{ + uint32_t x; + for (x = 0; x < width && wide == MPSD__SHIFT_32; ++x) { /* floats: c * a + (1 - a) */ + uint8_t *o = row + ((size_t)x * pixel << wide); + const float a = mpsd__float(o + (colors << wide)); + unsigned k; + for (k = 0; k < colors; ++k) mpsd__set_float(o + (k << wide), mpsd__float(o + (k << wide)) * a + (1.0f - a)); + } + for (x = 0; x < width && wide != MPSD__SHIFT_32; ++x) { + uint8_t *o = row + ((size_t)x * pixel << wide); + const uint32_t a = mpsd__sample(o + (colors << wide), wide); + unsigned k; + for (k = 0; k < colors; ++k) /* at most max * max + max / 2: fits 32 bits */ + mpsd__set_sample(o + (k << wide), wide, (mpsd__sample(o + (k << wide), wide) * a + max * (max - a) + max / 2) / max); + } +} + /* The composite's row y as the file stores it, matted against white: a supplied row that is already matted, of another mode than RGB, or without transparency (nothing to matte), as it is (bitmap packed into the caller's row), mpsd__composite_step samples a pixel; otherwise the @@ -161,19 +182,7 @@ MPSD__SHARED static const uint8_t *mpsd__composite_row(mpsd__composite *c, uint3 else if (d->composite_matted || mpsd__doc_mode(d) != MINIPSD_MODE_RGB) /* other modes: as given */ return d->composite + (size_t)y * size; else memcpy(c->row, d->composite + (size_t)y * size, size); - for (x = 0; x < d->width && wide == MPSD__SHIFT_32; ++x) { /* floats: c * a + (1 - a) */ - uint8_t *o = c->row + (size_t)x * pixel; - const float a = mpsd__float(o + ((unsigned)MINIPSD_CH_A << wide)); - unsigned k; - for (k = 0; k < MINIPSD_CH_A; ++k) mpsd__set_float(o + (k << wide), mpsd__float(o + (k << wide)) * a + (1.0f - a)); - } - for (x = 0; x < d->width && wide != MPSD__SHIFT_32; ++x) { /* the merged image is stored over white */ - uint8_t *o = c->row + (size_t)x * pixel; - const uint32_t a = mpsd__sample(o + ((unsigned)MINIPSD_CH_A << wide), wide); - unsigned k; - for (k = 0; k < MINIPSD_CH_A; ++k) /* at most max * max + max / 2: fits 32 bits */ - mpsd__set_sample(o + (k << wide), wide, (mpsd__sample(o + (k << wide), wide) * a + max * (max - a) + max / 2) / max); - } + mpsd__matte(c->row, d->width, mpsd__doc_pixel(d), MINIPSD_CH_A, wide, max); /* the merged image is stored over white */ return c->row; } diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 9bfa7c6..61493e8 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -53,14 +53,57 @@ static void mpsd__raw(mpsd__sink *s, const uint8_t *src, size_t n, size_t stride } } -/* One row of one channel, width samples of the document's depth, step bytes apart; at 1 bit - already packed, a byte apart. */ +/* One row of one channel, width samples of depth bits, step bytes apart; at 1 bit already packed, + a byte apart: raw with MINIPSD_RAW, else PackBits. */ +static void mpsd__row_as(mpsd__sink *s, int compression, unsigned depth, const uint8_t *row, size_t step, uint32_t width) +{ + const unsigned wide = mpsd__shift(depth); + const size_t n = (size_t)mpsd__row_bytes(width, depth); + if (compression == MINIPSD_RAW) mpsd__raw(s, row, n, step, wide); + else mpsd__packbits(s, row, n, step, wide); +} + +/* ... of the document's depth and compression. */ static void mpsd__row(mpsd__sink *s, const minipsd_doc *d, const uint8_t *row, size_t step, uint32_t width) { - const unsigned wide = mpsd__doc_shift(d); - const size_t n = (size_t)mpsd__row_bytes(width, mpsd__doc_depth(d)); - if (d->compression == MINIPSD_RAW) mpsd__raw(s, row, n, step, wide); - else mpsd__packbits(s, row, n, step, wide); + mpsd__row_as(s, d->compression, mpsd__doc_depth(d), row, step, width); +} + +/* A sink callback into a buffer of cap bytes; full, it takes nothing more (the sink keeps counting). */ +typedef struct mpsd__span { + uint8_t *out; + size_t cap, used; +} mpsd__span; + +static int mpsd__span_out(void *user, const void *data, size_t size) +{ + mpsd__span *b = (mpsd__span *)user; + if (size > b->cap - b->used) return 0; + memcpy(b->out + b->used, data, size); + b->used += size; + return 1; +} + +MINIPSDDEF int minipsd_encode_row(int compression, int depth, const void *samples, size_t step, uint32_t width, + uint8_t *out, size_t cap, size_t *size) +{ + mpsd__span b; + mpsd__sink s; + if (!samples || !out || !size || (compression != MINIPSD_RLE && compression != MINIPSD_RAW) || !mpsd__depth_ok((uint32_t)depth)) + return MINIPSD_ERR_ARG; + b.out = out; b.cap = cap; b.used = 0; + s.fn = mpsd__span_out; s.user = &b; s.count = 0; s.failed = 0; + mpsd__row_as(&s, compression, (unsigned)depth, (const uint8_t *)samples, step, width); + *size = (size_t)s.count; /* past cap: what it needs */ + return s.failed ? MINIPSD_ERR_SIZE : MINIPSD_OK; +} + +MINIPSDDEF int minipsd_matte_row(int color_mode, int depth, void *pixels, uint32_t width) +{ + if (!pixels || depth == MPSD__CH_1BIT || !mpsd__depth_ok((uint32_t)depth)) return MINIPSD_ERR_ARG; + if (color_mode == MINIPSD_MODE_RGB) /* the writer mattes RGB only; every other mode as given */ + mpsd__matte((uint8_t *)pixels, width, MPSD__RGBA, MINIPSD_CH_A, mpsd__shift((unsigned)depth), mpsd__full((unsigned)depth)); + return MINIPSD_OK; } /* The compression marker a channel the writer encodes starts with. */ diff --git a/libvips-plugin/minipsd_vips_psdsave.c b/libvips-plugin/minipsd_vips_psdsave.c index 98401b6..c690d6a 100644 --- a/libvips-plugin/minipsd_vips_psdsave.c +++ b/libvips-plugin/minipsd_vips_psdsave.c @@ -8,7 +8,8 @@ * composite; one without, flat, as Photoshop saves an image without layers: the composite only, * without transparency (with `layered`, one layer without transparency too). Grey, RGB or CMYK, * 8 or 16 bits, grey and RGB also 32, CMYK back to PSD's inverted ink; the resolution, the ICC - * profile, EXIF, XMP and IPTC kept. The image is held whole in memory while it is written. + * profile, EXIF, XMP and IPTC kept. Each channel is encoded once, as minipsd encodes it, and + * handed to minipsd as stored; the image is held whole in memory while it is encoded. * * minipsd's writer is compiled in here, private (MINIPSD_STATIC; the reader left out), so a * program that uses minipsd itself keeps its own. @@ -37,6 +38,7 @@ #define PSD_MAX_DPI 65536.0 /* PSD's resolution is a 16.16 fixed-point number */ #define PSD_LAYER_NAME "Layer 1" #define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ +#define PSD_MAX_PLANES 5 /* the most bands it takes: CMYK and alpha */ typedef struct _MinipsdVipsPsdsave { VipsForeignSave parent_object; @@ -70,17 +72,6 @@ static int minipsd_vips_psdsave_to_target(void *user, const void *data, size_t s return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ } -static int minipsd_vips_psdsave_deflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) -{ - uLongf n = (uLongf)*dst_size; - int rc; - (void)user; - if ((uLong)src_size != src_size || n != *dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ - rc = compress2(dst, &n, src, (uLong)src_size, PSD_ZIP_LEVEL); - *dst_size = n; - return rc != Z_OK; -} - /* A blob of the image as a resource to keep, when it has one. */ static void minipsd_vips_psdsave_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, size_t *count) @@ -95,21 +86,204 @@ static void minipsd_vips_psdsave_keep_blob(VipsImage *image, const char *name, u ++*count; } +/* --- The channels, encoded once: rows as they come, kept compressed, written as stored ------- */ + +#define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ +#define PSD_STORED_RLE 1 /* ... PackBits, its rows' sizes ahead of them */ +#define PSD_STORED_ZIP 2 /* ... zlib, without prediction */ +#define PSD_COUNT_PSD 2 /* an RLE row's size: two bytes in a PSD */ +#define PSD_COUNT_PSB 4 /* ... four in a PSB */ + +/* Bytes that grow, past what guint can count. */ +typedef struct _MinipsdVipsPsdsaveBytes { + uint8_t *data; + size_t size, room; +} MinipsdVipsPsdsaveBytes; + +static int minipsd_vips_psdsave_append(MinipsdVipsPsdsaveBytes *b, const void *data, size_t size) +{ + if (size > b->room - b->size) { + const size_t room = VIPS_MAX(b->room * 2, b->size + size); + uint8_t *grown; + if (room < b->size || !(grown = (uint8_t *)g_try_realloc(b->data, room))) return -1; + b->data = grown; + b->room = room; + } + memcpy(b->data + b->size, data, size); + b->size += size; + return 0; +} + +/* One channel: its rows, encoded one after another, and each one's size. */ +typedef struct _MinipsdVipsPsdsaveChannel { + MinipsdVipsPsdsaveBytes rows; + size_t *sizes; +} MinipsdVipsPsdsaveChannel; + +/* What the build encodes: the layer's channels (the colour ones, then transparency) when there is + a layer, the composite's always (the colour ones, then transparency when the image has it). */ +typedef struct _MinipsdVipsPsdsaveEncoding { + MinipsdVipsPsdsaveChannel layer[PSD_MAX_PLANES], composite[PSD_MAX_PLANES]; + unsigned planes; /* the image's bands: colour channels, then alpha */ + int layered, zip, alpha, depth, mode; + uint32_t width, height; + uint8_t *row; /* one encoded row, MINIPSD_ROW_BOUND */ + size_t row_size; + uint8_t *matted; /* one row of pixels, matted for the composite */ +} MinipsdVipsPsdsaveEncoding; + +static void minipsd_vips_psdsave_encoding_free(MinipsdVipsPsdsaveEncoding *e) +{ + unsigned k; + for (k = 0; k < PSD_MAX_PLANES; ++k) { + g_free(e->layer[k].rows.data); + g_free(e->layer[k].sizes); + g_free(e->composite[k].rows.data); + g_free(e->composite[k].sizes); + } + g_free(e->row); + g_free(e->matted); + memset(e, 0, sizeof *e); +} + +static int minipsd_vips_psdsave_encoding_init(MinipsdVipsPsdsaveEncoding *e, VipsImage *in, int depth, int mode, int alpha, + int layered, int zip) +{ + const size_t sample = (size_t)(depth / CHAR_BIT); + unsigned k; + memset(e, 0, sizeof *e); + e->planes = (unsigned)in->Bands; + e->layered = layered; + e->zip = zip; + e->alpha = alpha; + e->depth = depth; + e->mode = mode; + e->width = (uint32_t)in->Xsize; + e->height = (uint32_t)in->Ysize; + e->row_size = (size_t)MINIPSD_ROW_BOUND(e->width, depth); + e->row = (uint8_t *)g_try_malloc(e->row_size); + e->matted = (uint8_t *)g_try_malloc((size_t)e->width * e->planes * sample); + if (!e->row || !e->matted) return -1; + for (k = 0; k < e->planes; ++k) { + if (!(e->composite[k].sizes = g_try_new(size_t, e->height))) return -1; + if (layered && !(e->layer[k].sizes = g_try_new(size_t, e->height))) return -1; + } + return 0; +} + +/* Row y of channel k out of interleaved pixels, encoded into c: PackBits, or raw for a channel + deflated whole at the end. */ +static int minipsd_vips_psdsave_encode(MinipsdVipsPsdsaveEncoding *e, MinipsdVipsPsdsaveChannel *c, const uint8_t *pixels, + unsigned k, uint32_t y, int raw) +{ + const size_t sample = (size_t)(e->depth / CHAR_BIT); + size_t n; + if (minipsd_encode_row(raw ? MINIPSD_RAW : MINIPSD_RLE, e->depth, pixels + k * sample, e->planes * sample, e->width, e->row, + e->row_size, &n) != MINIPSD_OK + || minipsd_vips_psdsave_append(&c->rows, e->row, n)) + return -1; + c->sizes[y] = n; + return 0; +} + +/* count rows of interleaved pixels, from row y on, stride bytes apart: each channel's rows + encoded, the layer's as they are, the composite's matted (RGB with alpha, as minipsd mattes a + composite it is given). */ +static int minipsd_vips_psdsave_encode_rows(MinipsdVipsPsdsaveEncoding *e, const uint8_t *rows, uint32_t y, uint32_t count, + size_t stride) +{ + const size_t bytes = (size_t)e->width * e->planes * (size_t)(e->depth / CHAR_BIT); + uint32_t i; + unsigned k; + for (i = 0; i < count; ++i, rows += stride) { + const uint8_t *composite = rows; + for (k = 0; e->layered && k < e->planes; ++k) + if (minipsd_vips_psdsave_encode(e, &e->layer[k], rows, k, y + i, e->zip)) return -1; + if (e->alpha) { /* matted as minipsd mattes a composite it is given (RGB; the others as they are) */ + memcpy(e->matted, rows, bytes); + (void)minipsd_matte_row(e->mode, e->depth, e->matted, e->width); + composite = e->matted; + } + for (k = 0; k < e->planes; ++k) + if (minipsd_vips_psdsave_encode(e, &e->composite[k], composite, k, y + i, 0)) return -1; + } + return 0; +} + +/* Puts n, big-endian, in size bytes. */ +static uint8_t *minipsd_vips_psdsave_put(uint8_t *p, uint64_t n, unsigned size) +{ + unsigned i; + for (i = 0; i < size; ++i) p[i] = (uint8_t)(n >> (CHAR_BIT * (size - 1 - i))); + return p + size; +} + +/* Channels as a file stores them, one block: one marker, every channel's row sizes, then every + channel's rows (the composite's layout; a layer channel is the same, one channel). NULL when a + size does not fit its count, or memory runs out. */ +static uint8_t *minipsd_vips_psdsave_stored_rle(MinipsdVipsPsdsaveChannel *c, unsigned channels, uint32_t height, unsigned count, + size_t *size) +{ + const uint64_t max = count == PSD_COUNT_PSD ? UINT16_MAX : UINT32_MAX; + uint8_t *block, *p; + unsigned k; + uint32_t y; + *size = PSD_MARKER_SIZE + (size_t)channels * height * count; + for (k = 0; k < channels; ++k) *size += c[k].rows.size; + if (!(block = (uint8_t *)g_try_malloc(*size))) return NULL; + p = minipsd_vips_psdsave_put(block, PSD_STORED_RLE, PSD_MARKER_SIZE); + for (k = 0; k < channels; ++k) + for (y = 0; y < height; ++y) { + if (c[k].sizes[y] > max) { + g_free(block); + return NULL; + } + p = minipsd_vips_psdsave_put(p, c[k].sizes[y], count); + } + for (k = 0; k < channels; ++k) { + memcpy(p, c[k].rows.data, c[k].rows.size); + p += c[k].rows.size; + g_free(c[k].rows.data); /* copied: the block holds it now */ + memset(&c[k].rows, 0, sizeof c[k].rows); + } + return block; +} + +/* A channel's raw rows deflated as a file stores them: the marker, then the zlib stream. */ +static uint8_t *minipsd_vips_psdsave_stored_zip(MinipsdVipsPsdsaveChannel *c, size_t *size) +{ + uLongf n = compressBound((uLong)c->rows.size); + uint8_t *block; + if ((uLong)c->rows.size != c->rows.size || !(block = (uint8_t *)g_try_malloc(PSD_MARKER_SIZE + (size_t)n))) return NULL; + (void)minipsd_vips_psdsave_put(block, PSD_STORED_ZIP, PSD_MARKER_SIZE); + if (compress2(block + PSD_MARKER_SIZE, &n, c->rows.data, (uLong)c->rows.size, PSD_ZIP_LEVEL) != Z_OK) { + g_free(block); + return NULL; + } + *size = PSD_MARKER_SIZE + (size_t)n; + g_free(c->rows.data); + memset(&c->rows, 0, sizeof c->rows); + return block; +} + static int minipsd_vips_psdsave_build(VipsObject *object) { - VipsForeignSave *save = (VipsForeignSave *)object; - MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; - VipsImage *in; - minipsd_resource resources[PSD_MAX_RESOURCES]; - minipsd_layer layer; - minipsd_doc doc; - minipsd_write_opts opts; - const void *icc; - const char *filename; - size_t icc_size, bytes, colors, count = 0; - double ratio; - void *pixels; - int rc, alpha; + VipsForeignSave *save = (VipsForeignSave *)object; + MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; + VipsImage *in; + minipsd_resource resources[PSD_MAX_RESOURCES]; + minipsd_bytes stored[PSD_MAX_PLANES]; + minipsd_layer layer; + minipsd_doc doc; + MinipsdVipsPsdsaveEncoding e; + uint8_t *blocks[PSD_MAX_PLANES + 1]; /* the layer's channels, then the composite */ + const void *icc; + const char *filename; + size_t icc_size, bytes, colors, count = 0, size = 0; + double ratio; + void *pixels; + unsigned k, table; + int rc, alpha, failed; if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->build(object)) return -1; in = save->ready; @@ -129,28 +303,12 @@ static int minipsd_vips_psdsave_build(VipsObject *object) vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); return -1; } - doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; - - /* Transparency as the image has it: none, none added. */ - alpha = (size_t)in->Bands > colors; - if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; - if (doc.color_mode == MINIPSD_MODE_CMYK) minipsd_vips__invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); - - memset(&layer, 0, sizeof layer); - layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; - layer.pixels = (const uint8_t *)pixels; - layer.opacity = UCHAR_MAX; - layer.no_alpha = (uint8_t)!alpha; - - doc.width = (uint32_t)in->Xsize; - doc.height = (uint32_t)in->Ysize; - doc.dpi = in->Xres * MM_PER_INCH; + doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; + alpha = (size_t)in->Bands > colors; /* transparency as the image has it: none, none added */ + doc.width = (uint32_t)in->Xsize; + doc.height = (uint32_t)in->Ysize; + doc.dpi = in->Xres * MM_PER_INCH; if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; - doc.layers = alpha || psd->layered ? &layer : NULL; /* without alpha: flat, unless asked */ - doc.layer_count = doc.layers ? 1 : 0; - doc.composite = (const uint8_t *)pixels; /* one layer, at full opacity: the composite itself */ - doc.composite_alpha = (uint8_t)alpha; - doc.compression = psd->zip ? MINIPSD_ZIP : MINIPSD_RLE; /* PSB by the option, a file named .psb (libvips writes files through a target too), or a side past PSD's. */ filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); @@ -158,6 +316,44 @@ static int minipsd_vips_psdsave_build(VipsObject *object) || in->Ysize > MINIPSD_MAX_SIDE ? MINIPSD_PSB : MINIPSD_PSD; + table = doc.format == MINIPSD_PSB ? PSD_COUNT_PSB : PSD_COUNT_PSD; + + /* Every channel encoded once, as minipsd encodes it, and handed to it as stored: minipsd copies + it, where it would encode each row again for its size. */ + if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; + if (doc.color_mode == MINIPSD_MODE_CMYK) minipsd_vips__invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); + memset(blocks, 0, sizeof blocks); + memset(stored, 0, sizeof stored); + failed = minipsd_vips_psdsave_encoding_init(&e, in, doc.depth, doc.color_mode, alpha, alpha || psd->layered, psd->zip) + || minipsd_vips_psdsave_encode_rows(&e, (const uint8_t *)pixels, 0, e.height, bytes / e.height); + g_free(pixels); + for (k = 0; !failed && e.layered && k < e.planes; ++k) { /* stored: the colour channels, then transparency */ + blocks[k] = e.zip ? minipsd_vips_psdsave_stored_zip(&e.layer[k], &size) + : minipsd_vips_psdsave_stored_rle(&e.layer[k], 1, e.height, table, &size); + failed = !blocks[k]; + stored[k].data = blocks[k]; + stored[k].size = size; + } + if (!failed) { + blocks[PSD_MAX_PLANES] = minipsd_vips_psdsave_stored_rle(e.composite, e.planes, e.height, table, &size); + failed = !blocks[PSD_MAX_PLANES]; + doc.composite_stored.data = blocks[PSD_MAX_PLANES]; + doc.composite_stored.size = size; + } + minipsd_vips_psdsave_encoding_free(&e); + + memset(&layer, 0, sizeof layer); + layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; + layer.opacity = UCHAR_MAX; + layer.no_alpha = (uint8_t)!alpha; + layer.stored = stored; /* its own rectangle, the canvas: a stored layer gives it */ + layer.bottom = (int32_t)doc.height; + layer.right = (int32_t)doc.width; + doc.layers = alpha || psd->layered ? &layer : NULL; /* without alpha: flat, unless asked */ + doc.layer_count = doc.layers ? 1 : 0; + doc.composite_channels = (uint16_t)in->Bands; + doc.composite_alpha = (uint8_t)alpha; + doc.compression = MINIPSD_RLE; if (vips_image_get_typeof(in, VIPS_META_ICC_NAME) && !vips_image_get_blob(in, VIPS_META_ICC_NAME, &icc, &icc_size)) { doc.icc = (const uint8_t *)icc; doc.icc_size = icc_size; @@ -171,12 +367,11 @@ static int minipsd_vips_psdsave_build(VipsObject *object) doc.resources = resources; doc.resource_count = count; - memset(&opts, 0, sizeof opts); - opts.deflate = minipsd_vips_psdsave_deflate; - rc = minipsd_write(&doc, &opts, minipsd_vips_psdsave_to_target, psd->target); - g_free(pixels); + rc = failed ? MINIPSD_ERR_ALLOC : minipsd_write(&doc, NULL, minipsd_vips_psdsave_to_target, psd->target); + for (k = 0; k <= PSD_MAX_PLANES; ++k) g_free(blocks[k]); if (rc != MINIPSD_OK) { - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", + failed ? "out of memory, or a row too long for a PSD's counts (save as PSB)" : minipsd_error_string(rc)); return -1; } return vips_target_end(psd->target); diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index cc44093..0583c7a 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -408,6 +408,63 @@ static VipsImage *make_image(const void *data, int bands, VipsBandFormat format, return image; } +/* What minipsd_write makes of the image from its pixels, as psdsave wrote it before it encoded the + channels itself: one layer when layered (without transparency when the image has no alpha), + the composite, CMYK as PSD's inverted ink (8-bit, as the tests' images are), the image's + resolution, the EXIF, XMP and IPTC libvips gave psdsave (taken from its file); an option ("zip", + "psb") as psdsave takes it. */ +static int from_pixels(VipsImage *image, unsigned colors, int layered, const char *option, int value, const uint8_t *file, + size_t file_size, out_buffer *b) +{ + static const uint16_t kept[] = { MINIPSD_RES_EXIF_DATA_1, MINIPSD_RES_XMP_METADATA, MINIPSD_RES_IPTC_NAA }; + uint8_t workspace[WORKSPACE_SIZE]; + minipsd_reader *r; + minipsd_info info; + minipsd_resource resources[sizeof kept / sizeof kept[0]]; + minipsd_layer layer; + minipsd_doc doc; + minipsd_write_opts opts; + size_t bytes = 0, i, count = 0; + uint8_t *pixels = (uint8_t *)vips_image_write_to_memory(image, &bytes); + const int alpha = (unsigned)image->Bands > colors; + int rc; + if (!pixels) return MINIPSD_ERR_ALLOC; + if (image->Type == VIPS_INTERPRETATION_CMYK && image->BandFmt == VIPS_FORMAT_UCHAR) + for (i = 0; i < bytes; ++i) + if (i % (size_t)image->Bands < CMYK_COLORS) pixels[i] = (uint8_t)(UCHAR_MAX - pixels[i]); + memset(&layer, 0, sizeof layer); + memset(&doc, 0, sizeof doc); + memset(&opts, 0, sizeof opts); + layer.name = "Layer 1"; + layer.pixels = pixels; + layer.opacity = UCHAR_MAX; + layer.no_alpha = (uint8_t)!alpha; + doc.width = (uint32_t)image->Xsize; + doc.height = (uint32_t)image->Ysize; + doc.dpi = image->Xres * MM_PER_INCH; + doc.color_mode = image->Type == VIPS_INTERPRETATION_CMYK ? MINIPSD_MODE_CMYK : colors == 3 ? MINIPSD_MODE_RGB : MINIPSD_MODE_GREY; + doc.depth = image->BandFmt == VIPS_FORMAT_FLOAT ? 32 : image->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; + doc.layers = layered ? &layer : NULL; + doc.layer_count = layered ? 1 : 0; + doc.composite = pixels; + doc.composite_alpha = (uint8_t)alpha; + doc.compression = option && strcmp(option, "zip") == 0 && value ? MINIPSD_ZIP : MINIPSD_RLE; + doc.format = option && strcmp(option, "psb") == 0 && value ? MINIPSD_PSB : MINIPSD_PSD; + opts.deflate = deflate_zlib; + if (minipsd_reader_init(&r, file, file_size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK) + for (i = 0; i < sizeof kept / sizeof kept[0]; ++i) /* in psdsave's order */ + if (minipsd_reader_find_resource(r, kept[i], &resources[count]) == MINIPSD_OK) { + resources[count].name.data = NULL; + resources[count].name.size = 0; + ++count; + } + doc.resources = resources; + doc.resource_count = count; + rc = minipsd_write(&doc, &opts, to_buffer, b); + g_free(pixels); + return rc; +} + /* An image saved through psdsave as a file, into a buffer and into a target, the same bytes each time, then read back through psdload as it was, its bands too: with alpha one layer, and the composite when alpha is full (the matte undone exactly); without, a flat file, the composite @@ -463,6 +520,14 @@ static void check_saved(VipsImage *image, const char *name, const char *dir, int colors = image->Type == VIPS_INTERPRETATION_CMYK ? CMYK_COLORS : image->Bands >= 3 ? 3 : 1; layered = image->Bands > colors || (option && strcmp(option, "layered") == 0 && value); check(file && layer_count(file, file_size) == (layered ? 1u : 0u), layered ? "one layer" : "flat: no layer", name); + { /* encoded once by psdsave, the bytes minipsd_write makes from the pixels */ + out_buffer old = { NULL, 0, 0 }; + check(file && from_pixels(image, (unsigned)colors, layered, option, value, file, file_size, &old) == MINIPSD_OK + && old.size == file_size + && memcmp(old.data, file, file_size) == 0, + "the bytes minipsd_write makes from the pixels", name); + free(old.data); + } want_image = image; g_object_ref(want_image); want = vips_image_write_to_memory(want_image, &want_bytes); diff --git a/tests/minipsd_c_smoke.c b/tests/minipsd_c_smoke.c index 5b46fd1..c6e63ff 100644 --- a/tests/minipsd_c_smoke.c +++ b/tests/minipsd_c_smoke.c @@ -75,6 +75,15 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size) if (rc != MINIPSD_OK || pulled != b.size || memcmp(again, b.data, b.size) != 0) return -102; + /* A row encoded and a row matted by the caller, as the writer does them. */ + { + uint8_t row[MINIPSD_ROW_BOUND(3, 8)], copy[sizeof px]; + memcpy(copy, px, sizeof copy); + if (minipsd_encode_row(MINIPSD_RLE, 8, px, 4, 3, row, sizeof row, &n) != MINIPSD_OK || !n + || minipsd_matte_row(MINIPSD_MODE_RGB, 8, copy, 3) != MINIPSD_OK) + return -126; + } + /* The reader, over the bytes just written: the file, its resources, its layers, their pixels and their blocks, the composite. */ { diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index 74910d2..7215c7e 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -164,6 +164,47 @@ TEST(MinipsdWriterTest, ASuppliedCompositeIsMattedAndWrittenWithoutComposing) EXPECT_EQ(pullTotal(d), write(d).size()); EXPECT_EQ(composedRows, 0); + // The caller's own encoding, each row matted (minipsd_matte_row) then each channel's rows + // encoded (minipsd_encode_row), given as stored: the bytes minipsd writes from the pixels. + if (compression == MINIPSD_RLE) { + minipsd_doc own = d; + own.composite_matted = 0; + const Bytes fromPixels = write(own); + Bytes px2 = other; + for (std::uint32_t y = 0; y < 5; ++y) + ASSERT_EQ(minipsd_matte_row(MINIPSD_MODE_RGB, 8, px2.data() + std::size_t(y) * 37 * 4, 37), MINIPSD_OK); + EXPECT_EQ(px2, matted(other)) << "matted as the writer mattes"; + Bytes counts{0, 1}, rows; // PackBits: the marker, every channel's row sizes, the rows + Bytes row(MINIPSD_ROW_BOUND(37, 8)); + for (std::size_t k = 0; k < 4; ++k) + for (std::uint32_t y = 0; y < 5; ++y) { + std::size_t n = 0; + ASSERT_EQ(minipsd_encode_row(MINIPSD_RLE, 8, px2.data() + std::size_t(y) * 37 * 4 + k, 4, 37, row.data(), row.size(), &n), + MINIPSD_OK); + counts.push_back(static_cast(n >> 8)); + counts.push_back(static_cast(n)); + rows.insert(rows.end(), row.begin(), row.begin() + static_cast(n)); + } + Bytes block = counts; + block.insert(block.end(), rows.begin(), rows.end()); + own.composite = nullptr; + own.composite_stored = {block.data(), block.size()}; + own.composite_channels = 4; + EXPECT_EQ(write(own), fromPixels) << "encoded by the caller, the same bytes"; + + std::size_t n = 0; + EXPECT_EQ(minipsd_encode_row(MINIPSD_RLE, 8, px2.data(), 4, 37, row.data(), 1, &n), MINIPSD_ERR_SIZE); + EXPECT_GT(n, 1u) << "the bytes it needs"; + EXPECT_EQ(minipsd_encode_row(MINIPSD_ZIP, 8, px2.data(), 4, 37, row.data(), row.size(), &n), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_encode_row(MINIPSD_RLE, 12, px2.data(), 4, 37, row.data(), row.size(), &n), MINIPSD_ERR_ARG); + ASSERT_EQ(minipsd_encode_row(MINIPSD_RAW, 8, px2.data(), 4, 37, row.data(), row.size(), &n), MINIPSD_OK); + EXPECT_EQ(n, 37u) << "raw: the samples"; + Bytes cmyk = other; + EXPECT_EQ(minipsd_matte_row(MINIPSD_MODE_CMYK, 8, cmyk.data(), 37), MINIPSD_OK); + EXPECT_EQ(cmyk, other) << "only RGB is matted"; + EXPECT_EQ(minipsd_matte_row(MINIPSD_MODE_RGB, 1, cmyk.data(), 37), MINIPSD_ERR_ARG); + } + // Stored: R, G, B as a file stores them (raw), copied, never encoded; three channels, the // layer count positive. A supplied composite wins over it. Bytes planes{0, 0}; From 1f95c250445eae09f3c71659d41a782c24a13a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 12:27:16 +0100 Subject: [PATCH 11/22] feat(compression): a layer's own compression; psdsave's compression option minipsd: MINIPSD_COMPRESSION_DEFAULT is 0 and the other constants move up one (RLE 1, RAW 2, ZIP 3, ZIP_PREDICTED 4), so that zero means the default everywhere: a document's RLE, a layer's the document's. minipsd_layer.compression gives a layer's channels, its mask's too, a compression of their own; the writer asks for the deflate when any layer deflates. The composite is unchanged: raw in a raw document, else RLE. BREAKING: the constants' numbers change; code that names them compiles as it was. The libvips plugin: psdsave's compression (rle, zip, none; the enum MinipsdVipsCompression in minipsd_vips.h, by nick in option strings) replaces zip; none writes the composite raw too. Raw is measured no faster than RLE now that each channel is encoded once. --- include/minipsd.h | 30 ++++++--- include/minipsd/writer.h | 87 ++++++++++++++++++--------- libvips-plugin/minipsd_vips.h | 11 ++++ libvips-plugin/minipsd_vips_psdsave.c | 64 ++++++++++++++------ libvips-plugin/test_vips.c | 23 +++++-- tests/psd_tools/roundtrip.py | 10 +-- tests/support.h | 3 + tests/test_writer.cpp | 43 +++++++++++-- 8 files changed, 198 insertions(+), 73 deletions(-) diff --git a/include/minipsd.h b/include/minipsd.h index 6dbf6c5..889ad5e 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -235,16 +235,19 @@ enum { }; /** - * \brief Channel compression. Zero is RLE, so a zero-initialised document gets the compact one. + * \brief Channel compression. Zero is the default: a document's RLE, the compact one; a layer's + * the document's. * * ZIP is for the layers' and masks' channels, through the caller's deflate - * (minipsd_write_opts.deflate); the composite is then RLE, as Photoshop writes it. + * (minipsd_write_opts.deflate); the composite is RLE then, as Photoshop writes it, and raw only in + * a raw document. */ enum { - MINIPSD_RLE = 0, /*!< PackBits run-length encoding. */ - MINIPSD_RAW = 1, /*!< Uncompressed. */ - MINIPSD_ZIP = 2, /*!< zlib, through the caller's deflate. */ - MINIPSD_ZIP_PREDICTED = 3 /*!< zlib of each sample's difference from its left neighbour, + MINIPSD_COMPRESSION_DEFAULT = 0, /*!< A document's: RLE; a layer's: the document's. */ + MINIPSD_RLE = 1, /*!< PackBits run-length encoding. */ + MINIPSD_RAW = 2, /*!< Uncompressed. */ + MINIPSD_ZIP = 3, /*!< zlib, through the caller's deflate. */ + MINIPSD_ZIP_PREDICTED = 4 /*!< zlib of each sample's difference from its left neighbour, as Photoshop writes its 16-bit layers. */ }; @@ -426,12 +429,16 @@ typedef struct minipsd_layer { /* NOLINT(clang-analyzer-optin.performance.Paddi channel (composed as opaque). A stored layer says it by its transparency of size 0 instead. minipsd_read sets it for a layer whose file has none. */ + int compression; /*!< This layer's channels, its mask's too: MINIPSD_RLE, _RAW, + _ZIP or _ZIP_PREDICTED (a ZIP one with + minipsd_write_opts.deflate); 0 (MINIPSD_COMPRESSION_DEFAULT): + the document's. Stored channels keep their own. */ } minipsd_layer; /** * \brief A whole document: canvas, resolution, layers and options. * - * Zero-initialise it and set the fields you need; zero compression selects RLE. Pointers are + * Zero-initialise it and set the fields you need; zero compression is the default, RLE. Pointers are * borrowed for the duration of the write call. */ typedef struct minipsd_doc { @@ -443,8 +450,11 @@ typedef struct minipsd_doc { a flat file, its image the composite. */ const uint8_t *icc; /*!< Optional ICC profile; NULL means none (sRGB). */ size_t icc_size; /*!< Size of icc in bytes; 0 when icc is NULL. */ - int compression; /*!< MINIPSD_RLE, MINIPSD_RAW, or MINIPSD_ZIP(_PREDICTED) - with minipsd_write_opts.deflate. */ + int compression; /*!< The layers' channels: MINIPSD_RLE (or 0, the + default), MINIPSD_RAW, or MINIPSD_ZIP(_PREDICTED) + with minipsd_write_opts.deflate; a layer may have + its own (minipsd_layer.compression). The + composite: raw when this is, else RLE. */ const uint8_t *composite; /*!< The composite (merged) image: width * height pixels, the mode's colour channels then, with composite_alpha, transparency; rows top down, @@ -833,7 +843,7 @@ MINIPSDDEF int minipsd_writer_fill(minipsd_writer *w, void *buf, size_t cap, siz * whole or reads it once, top to bottom, encodes each channel once this way, where minipsd_write, * which keeps nothing between pieces, encodes a row again for its size. * - * \param compression MINIPSD_RLE or MINIPSD_RAW. + * \param compression MINIPSD_RLE (or 0, the default) or MINIPSD_RAW. * \param depth Bits a sample: 8, 16 (a uint16_t in host order), 32 (a float); at 1, the * row already packed, eight pixels a byte, step 1. * \param samples The row's first sample. diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 61493e8..001bfa3 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -63,12 +63,24 @@ static void mpsd__row_as(mpsd__sink *s, int compression, unsigned depth, const u else mpsd__packbits(s, row, n, step, wide); } -/* ... of the document's depth and compression. */ -static void mpsd__row(mpsd__sink *s, const minipsd_doc *d, const uint8_t *row, size_t step, uint32_t width) +/* ... of the document's depth. */ +static void mpsd__row(mpsd__sink *s, const minipsd_doc *d, int compression, const uint8_t *row, size_t step, uint32_t width) { - mpsd__row_as(s, d->compression, mpsd__doc_depth(d), row, step, width); + mpsd__row_as(s, compression, mpsd__doc_depth(d), row, step, width); } +/* A compression as written: the default is RLE. */ +static int mpsd__or_rle(int compression) { return compression == MINIPSD_COMPRESSION_DEFAULT ? MINIPSD_RLE : compression; } + +/* The compression of layer l's channels: its own, or else the document's. */ +static int mpsd__layer_compression(const minipsd_doc *d, const minipsd_layer *l) +{ + return mpsd__or_rle(l->compression != MINIPSD_COMPRESSION_DEFAULT ? l->compression : d->compression); +} + +/* The composite's: raw in a raw document, else RLE (ZIP is for layers, as Photoshop has it). */ +static int mpsd__composite_compression(const minipsd_doc *d) { return mpsd__or_rle(d->compression) == MINIPSD_RAW ? MINIPSD_RAW : MINIPSD_RLE; } + /* A sink callback into a buffer of cap bytes; full, it takes nothing more (the sink keeps counting). */ typedef struct mpsd__span { uint8_t *out; @@ -89,6 +101,7 @@ MINIPSDDEF int minipsd_encode_row(int compression, int depth, const void *sample { mpsd__span b; mpsd__sink s; + compression = mpsd__or_rle(compression); if (!samples || !out || !size || (compression != MINIPSD_RLE && compression != MINIPSD_RAW) || !mpsd__depth_ok((uint32_t)depth)) return MINIPSD_ERR_ARG; b.out = out; b.cap = cap; b.used = 0; @@ -107,12 +120,12 @@ MINIPSDDEF int minipsd_matte_row(int color_mode, int depth, void *pixels, uint32 } /* The compression marker a channel the writer encodes starts with. */ -static unsigned mpsd__marker(const minipsd_doc *d) { return d->compression == MINIPSD_RAW ? MPSD__COMP_RAW : MPSD__COMP_RLE; } +static unsigned mpsd__marker(int compression) { return compression == MINIPSD_RAW ? MPSD__COMP_RAW : MPSD__COMP_RLE; } -static uint64_t mpsd__row_size(const minipsd_doc *d, const uint8_t *row, size_t step, uint32_t width) +static uint64_t mpsd__row_size(const minipsd_doc *d, int compression, const uint8_t *row, size_t step, uint32_t width) { mpsd__sink c = { NULL, NULL, 0, 0 }; - mpsd__row(&c, d, row, step, width); + mpsd__row(&c, d, compression, row, step, width); return c.count; } @@ -217,8 +230,17 @@ static uint64_t mpsd__stored_size(const mpsd__source *src, unsigned from, unsign return src->stored->size - (uint64_t)src->height * mpsd__count_size(from) + (uint64_t)src->height * mpsd__count_size(out); } -/* Does the document compress its layers' and masks' channels with the caller's deflate? */ -static int mpsd__zip(const minipsd_doc *d) { return d->compression == MINIPSD_ZIP || d->compression == MINIPSD_ZIP_PREDICTED; } +/* Is a compression the caller's deflate? */ +static int mpsd__zip(int compression) { return compression == MINIPSD_ZIP || compression == MINIPSD_ZIP_PREDICTED; } + +/* Does the document deflate anything: its compression, or a layer's? */ +static int mpsd__deflates(const minipsd_doc *d) +{ + size_t i; + for (i = 0; i < d->layer_count; ++i) + if (mpsd__zip(mpsd__layer_compression(d, &d->layers[i]))) return 1; + return mpsd__zip(d->compression); +} /* The largest channel the writer deflates, raw: its bytes, 0 for none. */ static uint64_t mpsd__zip_plane(const minipsd_doc *d) @@ -226,8 +248,9 @@ static uint64_t mpsd__zip_plane(const minipsd_doc *d) uint64_t max = 0; size_t i; int k; - for (i = 0; mpsd__zip(d) && i < d->layer_count; ++i) - for (k = 0; k <= mpsd__ch_mask(d) && k < mpsd__channel_count(d, &d->layers[i]); ++k) { + for (i = 0; i < d->layer_count; ++i) + for (k = 0; mpsd__zip(mpsd__layer_compression(d, &d->layers[i])) && k <= mpsd__ch_mask(d) + && k < mpsd__channel_count(d, &d->layers[i]); ++k) { const mpsd__source src = mpsd__channel_source(d, &d->layers[i], k); const uint64_t n = src.samples ? (uint64_t)src.width * src.height << mpsd__doc_shift(d) : 0; if (n > max) max = n; @@ -450,14 +473,16 @@ static uint64_t mpsd__layer_channel_size(const minipsd_doc *d, const minipsd_lay { const mpsd__source src = mpsd__channel_source(d, l, k); const unsigned from = mpsd__doc_psb(d); + const int compression = mpsd__layer_compression(d, l); uint64_t size = MPSD__MARKER_SIZE; uint32_t y; if (src.stored) return mpsd__stored_size(&src, from, out); if (!src.samples) return size; MPSD__ENCODED(l); - if (d->compression == MINIPSD_RAW) return size + ((uint64_t)src.width * src.height << mpsd__doc_shift(d)); + if (compression == MINIPSD_RAW) return size + ((uint64_t)src.width * src.height << mpsd__doc_shift(d)); for (y = 0; y < src.height; ++y) - size += mpsd__count_size(out) + mpsd__row_size(d, src.samples + (size_t)y * src.width * src.step, src.step, src.width); + size += mpsd__count_size(out) + + mpsd__row_size(d, compression, src.samples + (size_t)y * src.width * src.step, src.step, src.width); return size; } @@ -933,7 +958,8 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *d, const minipsd_write_opts res = d->dpi * MPSD__FIXED_ONE + 0.5; if ((d->layer_count && !d->layers) || (!d->layer_count && !d->composite && !d->composite_stored.size) || d->layer_count > MPSD__MAX_LAYERS || !(res >= 1.0 && res < 4294967296.0) || (d->depth != 0 && d->depth != MPSD__CH_1BIT && d->depth != MPSD__CH_8BIT && d->depth != MPSD__CH_16BIT && d->depth != MPSD__CH_32BIT) || d->format > MINIPSD_PSB - || d->compression < MINIPSD_RLE || d->compression > MINIPSD_ZIP_PREDICTED || (mpsd__zip(d) && !(opts && opts->deflate)) + || d->compression < MINIPSD_COMPRESSION_DEFAULT || d->compression > MINIPSD_ZIP_PREDICTED + || (mpsd__deflates(d) && !(opts && opts->deflate)) || (d->color_mode_data.size && !d->color_mode_data.data) || !mpsd__floats_not_negative(d->pixel_aspect_ratio)) return MINIPSD_ERR_ARG; @@ -952,7 +978,9 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *d, const minipsd_write_opts const minipsd_layer *l = &d->layers[i]; const int64_t w = (int64_t)l->right - l->left, h = (int64_t)l->bottom - l->top; int k; - if (w < 0 || h < 0 || (!l->pixels && !l->stored && w && h)) return MINIPSD_ERR_ARG; + if (w < 0 || h < 0 || (!l->pixels && !l->stored && w && h) + || l->compression < MINIPSD_COMPRESSION_DEFAULT || l->compression > MINIPSD_ZIP_PREDICTED) + return MINIPSD_ERR_ARG; if (w > mpsd__max_side(out) || h > mpsd__max_side(out)) return MINIPSD_ERR_SIZE; rc = l->mask ? mpsd__mask_ok(d, l->mask, out) : MINIPSD_OK; for (k = mpsd__ch_mask(d); rc == MINIPSD_OK && k < mpsd__channel_count(d, l); ++k) rc = mpsd__retable_ok(d, l, k, out); @@ -1028,7 +1056,7 @@ static void mpsd__tail(mpsd__sink *s, const minipsd_doc *d, uint64_t content, ui (void)mpsd__tail_range(s, d, 0, d->layer_tail.size, out); /* minipsd_doc_check has accepted it */ if (mpsd__own_merged_block(d)) mpsd__own_block(s, mpsd__merged_key(d), 0, out); } - if (!mpsd__copies_composite(d)) mpsd__u16(s, mpsd__marker(d)); /* a stored composite brings its own */ + if (!mpsd__copies_composite(d)) mpsd__u16(s, mpsd__marker(mpsd__composite_compression(d))); /* a stored one brings its own */ } /* --- the pull writer: everything in the caller's workspace --- */ @@ -1089,7 +1117,7 @@ static size_t mpsd__writer_layout(const minipsd_doc *d, minipsd_workspace *works { const size_t self = mpsd__pad(sizeof(minipsd_writer), MPSD__ALIGN); const size_t sizes = d->layer_count * MPSD__CHANNELS * sizeof(uint64_t); - const size_t rows = d->compression == MINIPSD_RAW ? 0 : (size_t)d->height * mpsd__doc_pixel(d) * sizeof(uint32_t); + const size_t rows = mpsd__composite_compression(d) == MINIPSD_RAW ? 0 : (size_t)d->height * mpsd__doc_pixel(d) * sizeof(uint32_t); const size_t row = (size_t)d->width * mpsd__doc_pixel(d) << mpsd__doc_shift(d); const size_t plane = (size_t)mpsd__zip_plane(d), room = plane ? (size_t)mpsd__deflate_room(plane) : 0; /* doc_check bounds it */ if (workspace) { @@ -1109,7 +1137,7 @@ static size_t mpsd__writer_layout(const minipsd_doc *d, minipsd_workspace *works /* Is layer i's channel k deflated? With ZIP, every channel encoded from samples. */ static int mpsd__zips(const minipsd_doc *d, size_t i, int k) { - return mpsd__zip(d) && k <= mpsd__ch_mask(d) && mpsd__channel_source(d, &d->layers[i], k).samples; + return mpsd__zip(mpsd__layer_compression(d, &d->layers[i])) && k <= mpsd__ch_mask(d) && mpsd__channel_source(d, &d->layers[i], k).samples; } /* Layer i's channel k deflated into zip_out, unless it is there already: its samples gathered as @@ -1121,6 +1149,7 @@ static int mpsd__zip_channel(minipsd_writer *w, size_t i, int k) const mpsd__source src = mpsd__channel_source(d, &d->layers[i], k); const unsigned wide = mpsd__doc_shift(d); const size_t bytes = (size_t)src.width << wide; + const int predicted = mpsd__layer_compression(d, &d->layers[i]) == MINIPSD_ZIP_PREDICTED; size_t x, y, size = w->zip_room; if (w->zip_layer == i && w->zip_channel == k) return MINIPSD_OK; w->zip_layer = (size_t)-1; @@ -1128,9 +1157,9 @@ static int mpsd__zip_channel(minipsd_writer *w, size_t i, int k) const uint8_t *row = src.samples + y * src.width * src.step; uint8_t *out = w->zip_plane + y * bytes; for (x = 0; x < bytes; ++x) /* at 32 bits predicted: shuffled, as the format wants */ - out[d->compression == MINIPSD_ZIP_PREDICTED && wide == MPSD__SHIFT_32 ? mpsd__shuffled(x, src.width) : x] + out[predicted && wide == MPSD__SHIFT_32 ? mpsd__shuffled(x, src.width) : x] = row[mpsd__at(x, (ptrdiff_t)src.step, wide)]; - if (d->compression == MINIPSD_ZIP_PREDICTED) mpsd__predict(out, src.width, mpsd__doc_depth(d)); + if (predicted) mpsd__predict(out, src.width, mpsd__doc_depth(d)); } if (!w->deflate /* never: minipsd_doc_check asks for one; spelled out for static analysers */ || w->deflate(w->deflate_user, w->zip_plane, bytes * src.height, w->zip_out, &size) || size > w->zip_room) @@ -1215,14 +1244,14 @@ MINIPSDDEF int minipsd_writer_init(minipsd_writer **out, const minipsd_doc *d, c if (mpsd__copies_composite(d)) { const mpsd__source src = mpsd__composite_source(d); total += mpsd__stored_size(&src, mpsd__doc_psb(d), w->out) - MPSD__MARKER_SIZE; - } else if (d->compression == MINIPSD_RAW) { + } else if (mpsd__composite_compression(d) == MINIPSD_RAW) { total += mpsd__row_bytes(d->width, mpsd__doc_depth(d)) * d->height * mpsd__composite_channels(d); } else { /* each row's count, then the row */ mpsd__composite_init(&comp, d, w->composite_row); for (y = 0; y < d->height; ++y) { const uint8_t *row = mpsd__composite_row(&comp, y); for (k = 0; k < (int)mpsd__composite_channels(d); ++k) { - const uint64_t n = mpsd__row_size(d, row + ((size_t)k << wide), (size_t)mpsd__composite_step(d) << wide, d->width); + const uint64_t n = mpsd__row_size(d, MINIPSD_RLE, row + ((size_t)k << wide), (size_t)mpsd__composite_step(d) << wide, d->width); w->composite_sizes[(size_t)k * d->height + y] = (uint32_t)n; total += mpsd__count_size(w->out) + n; } @@ -1292,9 +1321,9 @@ static void mpsd__writer_piece(minipsd_writer *w, mpsd__sink *s) switch (w->piece) { case MPSD__HEAD: mpsd__head(s, d, w->info, w->out); break; case MPSD__RECORD: mpsd__layer_record(s, d, l, w->channel_sizes + w->layer * MPSD__CHANNELS, w->out); break; - case MPSD__COMPRESSION: mpsd__u16(s, mpsd__marker(d)); break; - case MPSD__COUNT: mpsd__putcount(s, (uint32_t)mpsd__row_size(d, row, src.step, src.width), w->out); break; - case MPSD__ROW: mpsd__row(s, d, row, src.step, src.width); break; + case MPSD__COMPRESSION: mpsd__u16(s, mpsd__marker(mpsd__layer_compression(d, l))); break; + case MPSD__COUNT: mpsd__putcount(s, (uint32_t)mpsd__row_size(d, mpsd__layer_compression(d, l), row, src.step, src.width), w->out); break; + case MPSD__ROW: mpsd__row(s, d, mpsd__layer_compression(d, l), row, src.step, src.width); break; case MPSD__STORED: mpsd__put(s, src.stored->data, src.stored->size); break; case MPSD__STORED_HEAD: mpsd__put(s, src.stored->data, MPSD__MARKER_SIZE); break; case MPSD__STORED_COUNT: mpsd__putcount(s, mpsd__count(src.stored->data + MPSD__MARKER_SIZE, w->row, mpsd__doc_psb(d)), w->out); break; @@ -1304,13 +1333,14 @@ static void mpsd__writer_piece(minipsd_writer *w, mpsd__sink *s) break; } case MPSD__ZIP: /* minipsd_writer_fill deflated it */ - mpsd__u16(s, d->compression == MINIPSD_ZIP ? MPSD__COMP_ZIP : MPSD__COMP_ZIP_PREDICTED); + mpsd__u16(s, mpsd__layer_compression(d, l) == MINIPSD_ZIP ? MPSD__COMP_ZIP : MPSD__COMP_ZIP_PREDICTED); mpsd__put(s, w->zip_out, w->zip_size); break; case MPSD__TAIL: mpsd__tail(s, d, w->content, w->info, w->out); break; case MPSD__COMPOSITE_COUNT: mpsd__putcount(s, w->composite_sizes[(size_t)w->channel * d->height + w->row], w->out); break; default: - mpsd__row(s, d, mpsd__writer_composite(w, w->row) + ((size_t)w->channel << mpsd__doc_shift(d)), (size_t)mpsd__composite_step(d) << mpsd__doc_shift(d), d->width); + mpsd__row(s, d, mpsd__composite_compression(d), mpsd__writer_composite(w, w->row) + ((size_t)w->channel << mpsd__doc_shift(d)), + (size_t)mpsd__composite_step(d) << mpsd__doc_shift(d), d->width); break; } } @@ -1334,7 +1364,6 @@ static int mpsd__channel_piece(const minipsd_writer *w) static void mpsd__writer_next(minipsd_writer *w) { const minipsd_doc *d = w->doc; - const int rle = d->compression != MINIPSD_RAW; const uint32_t rows = w->copying ? mpsd__composite_source(d).height : w->piece > MPSD__RECORD && w->piece < MPSD__TAIL ? mpsd__channel_source(d, &d->layers[w->layer], w->channel).height : 0; w->offset = 0; @@ -1349,7 +1378,7 @@ static void mpsd__writer_next(minipsd_writer *w) w->piece = mpsd__channel_piece(w); return; case MPSD__COMPRESSION: /* rows follow, unless the layer is empty */ - if (rows) { w->piece = rle ? MPSD__COUNT : MPSD__ROW; return; } + if (rows) { w->piece = mpsd__layer_compression(d, &d->layers[w->layer]) != MINIPSD_RAW ? MPSD__COUNT : MPSD__ROW; return; } break; case MPSD__COUNT: if (++w->row < rows) return; @@ -1379,7 +1408,7 @@ static void mpsd__writer_next(minipsd_writer *w) w->piece = mpsd__source_piece(&src, w); return; } - w->piece = rle ? MPSD__COMPOSITE_COUNT : MPSD__COMPOSITE_ROW; + w->piece = mpsd__composite_compression(d) != MINIPSD_RAW ? MPSD__COMPOSITE_COUNT : MPSD__COMPOSITE_ROW; return; default: /* the composite: channel by channel, row by row */ if (++w->row < d->height) return; diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index 1eec051..a8d96c7 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -19,6 +19,17 @@ extern "C" { #endif +/** + * \brief psdsave's `compression`: of the layer's channels; the composite is RLE, or raw with none. + * + * From the command line or an option string, by its nick: `compression=zip`. + */ +typedef enum { + MINIPSD_VIPS_COMPRESSION_RLE = 0, /*!< PackBits, as Photoshop writes 8-bit layers: the default. */ + MINIPSD_VIPS_COMPRESSION_ZIP = 1, /*!< zlib, without prediction. */ + MINIPSD_VIPS_COMPRESSION_NONE = 2 /*!< Raw: the largest file, the least work. */ +} MinipsdVipsCompression; + /** * \brief Registers minipsd's operations with libvips. Safe to call more than once. * diff --git a/libvips-plugin/minipsd_vips_psdsave.c b/libvips-plugin/minipsd_vips_psdsave.c index c690d6a..0e18d55 100644 --- a/libvips-plugin/minipsd_vips_psdsave.c +++ b/libvips-plugin/minipsd_vips_psdsave.c @@ -23,6 +23,7 @@ #define MINIPSD_IMPLEMENTATION #include "minipsd.h" +#include "minipsd_vips.h" #include "minipsd_vips_psdsave.h" #include "minipsd_vips_util.h" @@ -44,7 +45,7 @@ typedef struct _MinipsdVipsPsdsave { VipsForeignSave parent_object; VipsTarget *target; /* set by the subclass's build */ gboolean psb; /* the option; also a .psb file, or a side past PSD's */ - gboolean zip; /* the option: ZIP the layer's channels */ + MinipsdVipsCompression compression; /* the option: the layer's channels; none: the composite's too */ char *layer_name; /* the option */ gboolean layered; /* the option: a layer also for an image without alpha */ } MinipsdVipsPsdsave; @@ -53,6 +54,22 @@ typedef VipsForeignSaveClass MinipsdVipsPsdsaveClass; G_DEFINE_ABSTRACT_TYPE(MinipsdVipsPsdsave, minipsd_vips_psdsave, VIPS_TYPE_FOREIGN_SAVE) +/* MinipsdVipsCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none"). */ +static GType minipsd_vips_psdsave_compression_get_type(void) +{ + static gsize type = 0; + if (g_once_init_enter(&type)) { + static const GEnumValue values[] = { + { MINIPSD_VIPS_COMPRESSION_RLE, "MINIPSD_VIPS_COMPRESSION_RLE", "rle" }, + { MINIPSD_VIPS_COMPRESSION_ZIP, "MINIPSD_VIPS_COMPRESSION_ZIP", "zip" }, + { MINIPSD_VIPS_COMPRESSION_NONE, "MINIPSD_VIPS_COMPRESSION_NONE", "none" }, + { 0, NULL, NULL }, + }; + g_once_init_leave(&type, g_enum_register_static("MinipsdVipsCompression", values)); + } + return (GType)type; +} + /* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ static VipsBandFormat minipsd_vips_psdsave_save_formats[] = { /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ @@ -89,6 +106,7 @@ static void minipsd_vips_psdsave_keep_blob(VipsImage *image, const char *name, u /* --- The channels, encoded once: rows as they come, kept compressed, written as stored ------- */ #define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ +#define PSD_STORED_RAW 0 /* ... raw */ #define PSD_STORED_RLE 1 /* ... PackBits, its rows' sizes ahead of them */ #define PSD_STORED_ZIP 2 /* ... zlib, without prediction */ #define PSD_COUNT_PSD 2 /* an RLE row's size: two bytes in a PSD */ @@ -125,7 +143,8 @@ typedef struct _MinipsdVipsPsdsaveChannel { typedef struct _MinipsdVipsPsdsaveEncoding { MinipsdVipsPsdsaveChannel layer[PSD_MAX_PLANES], composite[PSD_MAX_PLANES]; unsigned planes; /* the image's bands: colour channels, then alpha */ - int layered, zip, alpha, depth, mode; + int layered, alpha, depth, mode; + MinipsdVipsCompression compression; uint32_t width, height; uint8_t *row; /* one encoded row, MINIPSD_ROW_BOUND */ size_t row_size; @@ -147,14 +166,14 @@ static void minipsd_vips_psdsave_encoding_free(MinipsdVipsPsdsaveEncoding *e) } static int minipsd_vips_psdsave_encoding_init(MinipsdVipsPsdsaveEncoding *e, VipsImage *in, int depth, int mode, int alpha, - int layered, int zip) + int layered, MinipsdVipsCompression compression) { const size_t sample = (size_t)(depth / CHAR_BIT); unsigned k; memset(e, 0, sizeof *e); e->planes = (unsigned)in->Bands; e->layered = layered; - e->zip = zip; + e->compression = compression; e->alpha = alpha; e->depth = depth; e->mode = mode; @@ -198,14 +217,16 @@ static int minipsd_vips_psdsave_encode_rows(MinipsdVipsPsdsaveEncoding *e, const for (i = 0; i < count; ++i, rows += stride) { const uint8_t *composite = rows; for (k = 0; e->layered && k < e->planes; ++k) - if (minipsd_vips_psdsave_encode(e, &e->layer[k], rows, k, y + i, e->zip)) return -1; + if (minipsd_vips_psdsave_encode(e, &e->layer[k], rows, k, y + i, e->compression != MINIPSD_VIPS_COMPRESSION_RLE)) + return -1; if (e->alpha) { /* matted as minipsd mattes a composite it is given (RGB; the others as they are) */ memcpy(e->matted, rows, bytes); (void)minipsd_matte_row(e->mode, e->depth, e->matted, e->width); composite = e->matted; } for (k = 0; k < e->planes; ++k) - if (minipsd_vips_psdsave_encode(e, &e->composite[k], composite, k, y + i, 0)) return -1; + if (minipsd_vips_psdsave_encode(e, &e->composite[k], composite, k, y + i, e->compression == MINIPSD_VIPS_COMPRESSION_NONE)) + return -1; } return 0; } @@ -218,21 +239,21 @@ static uint8_t *minipsd_vips_psdsave_put(uint8_t *p, uint64_t n, unsigned size) return p + size; } -/* Channels as a file stores them, one block: one marker, every channel's row sizes, then every - channel's rows (the composite's layout; a layer channel is the same, one channel). NULL when a - size does not fit its count, or memory runs out. */ -static uint8_t *minipsd_vips_psdsave_stored_rle(MinipsdVipsPsdsaveChannel *c, unsigned channels, uint32_t height, unsigned count, - size_t *size) +/* Channels as a file stores them, one block: one marker, every channel's row sizes (RLE), then + every channel's rows (the composite's layout; a layer channel is the same, one channel). NULL + when a size does not fit its count, or memory runs out. */ +static uint8_t *minipsd_vips_psdsave_stored(MinipsdVipsPsdsaveChannel *c, unsigned channels, uint32_t height, unsigned count, + int raw, size_t *size) { const uint64_t max = count == PSD_COUNT_PSD ? UINT16_MAX : UINT32_MAX; uint8_t *block, *p; unsigned k; uint32_t y; - *size = PSD_MARKER_SIZE + (size_t)channels * height * count; + *size = PSD_MARKER_SIZE + (raw ? 0 : (size_t)channels * height * count); for (k = 0; k < channels; ++k) *size += c[k].rows.size; if (!(block = (uint8_t *)g_try_malloc(*size))) return NULL; - p = minipsd_vips_psdsave_put(block, PSD_STORED_RLE, PSD_MARKER_SIZE); - for (k = 0; k < channels; ++k) + p = minipsd_vips_psdsave_put(block, raw ? PSD_STORED_RAW : PSD_STORED_RLE, PSD_MARKER_SIZE); + for (k = 0; !raw && k < channels; ++k) for (y = 0; y < height; ++y) { if (c[k].sizes[y] > max) { g_free(block); @@ -324,18 +345,20 @@ static int minipsd_vips_psdsave_build(VipsObject *object) if (doc.color_mode == MINIPSD_MODE_CMYK) minipsd_vips__invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); memset(blocks, 0, sizeof blocks); memset(stored, 0, sizeof stored); - failed = minipsd_vips_psdsave_encoding_init(&e, in, doc.depth, doc.color_mode, alpha, alpha || psd->layered, psd->zip) + failed = minipsd_vips_psdsave_encoding_init(&e, in, doc.depth, doc.color_mode, alpha, alpha || psd->layered, psd->compression) || minipsd_vips_psdsave_encode_rows(&e, (const uint8_t *)pixels, 0, e.height, bytes / e.height); g_free(pixels); for (k = 0; !failed && e.layered && k < e.planes; ++k) { /* stored: the colour channels, then transparency */ - blocks[k] = e.zip ? minipsd_vips_psdsave_stored_zip(&e.layer[k], &size) - : minipsd_vips_psdsave_stored_rle(&e.layer[k], 1, e.height, table, &size); + blocks[k] = e.compression == MINIPSD_VIPS_COMPRESSION_ZIP + ? minipsd_vips_psdsave_stored_zip(&e.layer[k], &size) + : minipsd_vips_psdsave_stored(&e.layer[k], 1, e.height, table, e.compression == MINIPSD_VIPS_COMPRESSION_NONE, &size); failed = !blocks[k]; stored[k].data = blocks[k]; stored[k].size = size; } if (!failed) { - blocks[PSD_MAX_PLANES] = minipsd_vips_psdsave_stored_rle(e.composite, e.planes, e.height, table, &size); + blocks[PSD_MAX_PLANES] = minipsd_vips_psdsave_stored(e.composite, e.planes, e.height, table, + e.compression == MINIPSD_VIPS_COMPRESSION_NONE, &size); failed = !blocks[PSD_MAX_PLANES]; doc.composite_stored.data = blocks[PSD_MAX_PLANES]; doc.composite_stored.size = size; @@ -401,8 +424,9 @@ static void minipsd_vips_psdsave_class_init(MinipsdVipsPsdsaveClass *class) PSD_ARGS_BEGIN VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, psb), FALSE); - VIPS_ARG_BOOL(class, "zip", 21, "ZIP", "Compress the layer with ZIP instead of RLE", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, zip), FALSE); + VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layer's compression: rle, zip or none (the composite too)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, compression), + minipsd_vips_psdsave_compression_get_type(), MINIPSD_VIPS_COMPRESSION_RLE); VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, layer_name), PSD_LAYER_NAME); VIPS_ARG_BOOL(class, "layered", 23, "Layered", "Without alpha, a layer too, without transparency (else flat)", diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index 0583c7a..a0a2bf3 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -51,7 +51,8 @@ #define TALL_W 1024 /* the tall image: PSD's highest, 60 MB of grey and transparency; */ #define TALL_H MINIPSD_MAX_SIDE /* libvips caches rows, so its memory follows the width */ #define BOUND 8 /* read in bands, libvips holds less than an eighth of it */ -#define STORED_RLE 1 /* a channel's stored compression markers */ +#define STORED_RAW 0 /* a channel's stored compression markers */ +#define STORED_RLE 1 #define STORED_ZIP 2 /* The ZIP document's two layers: one reaching past the canvas's left edge, one inside. */ @@ -411,7 +412,7 @@ static VipsImage *make_image(const void *data, int bands, VipsBandFormat format, /* What minipsd_write makes of the image from its pixels, as psdsave wrote it before it encoded the channels itself: one layer when layered (without transparency when the image has no alpha), the composite, CMYK as PSD's inverted ink (8-bit, as the tests' images are), the image's - resolution, the EXIF, XMP and IPTC libvips gave psdsave (taken from its file); an option ("zip", + resolution, the EXIF, XMP and IPTC libvips gave psdsave (taken from its file); an option ("compression", "psb") as psdsave takes it. */ static int from_pixels(VipsImage *image, unsigned colors, int layered, const char *option, int value, const uint8_t *file, size_t file_size, out_buffer *b) @@ -448,7 +449,10 @@ static int from_pixels(VipsImage *image, unsigned colors, int layered, const cha doc.layer_count = layered ? 1 : 0; doc.composite = pixels; doc.composite_alpha = (uint8_t)alpha; - doc.compression = option && strcmp(option, "zip") == 0 && value ? MINIPSD_ZIP : MINIPSD_RLE; + doc.compression = !option || strcmp(option, "compression") != 0 ? MINIPSD_RLE + : value == MINIPSD_VIPS_COMPRESSION_ZIP ? MINIPSD_ZIP + : value == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW + : MINIPSD_RLE; doc.format = option && strcmp(option, "psb") == 0 && value ? MINIPSD_PSB : MINIPSD_PSD; opts.deflate = deflate_zlib; if (minipsd_reader_init(&r, file, file_size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK) @@ -867,7 +871,18 @@ int main(int argc, char **argv) "CMYK stored as PSD's inverted ink", path); check_saved(make_image(rgbf, 3, VIPS_FORMAT_FLOAT, VIPS_INTERPRETATION_scRGB), "save-float.psd", argv[1], 1, NULL, 0); check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-layered.psd", argv[1], 1, "layered", TRUE); - check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 0, "zip", TRUE); + check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 0, "compression", + MINIPSD_VIPS_COMPRESSION_ZIP); + check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-raw.psd", argv[1], 0, "compression", + MINIPSD_VIPS_COMPRESSION_NONE); + snprintf(path, sizeof path, "%s/save-raw.psd", argv[1]); + check(first_layer_compression(path) == STORED_RAW, "raw by option", path); + image = make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB); /* by its nick, in an option string */ + snprintf(path, sizeof path, "%s/save-nick.psd[compression=zip]", argv[1]); + check(image && vips_image_write_to_file(image, path, NULL) == 0, "saved with compression=zip", path); + if (image) g_object_unref(image); + snprintf(path, sizeof path, "%s/save-nick.psd", argv[1]); + check(first_layer_compression(path) == STORED_ZIP, "zip by its nick", path); snprintf(path, sizeof path, "%s/save-zip.psd", argv[1]); check(first_layer_compression(path) == STORED_ZIP, "ZIP by option", path); snprintf(path, sizeof path, "%s/save-rgba8.psd", argv[1]); diff --git a/tests/psd_tools/roundtrip.py b/tests/psd_tools/roundtrip.py index cd5c9c1..237657e 100644 --- a/tests/psd_tools/roundtrip.py +++ b/tests/psd_tools/roundtrip.py @@ -34,7 +34,8 @@ from psd_tools import PSDImage from psd_tools.constants import Compression, Resource as ResourceId -MINIPSD_OK, MINIPSD_RLE, MINIPSD_RAW, MINIPSD_ZIP, MINIPSD_ZIP_PREDICTED = 0, 0, 1, 2, 3 +MINIPSD_OK, MINIPSD_RLE, MINIPSD_RAW, MINIPSD_ZIP, MINIPSD_ZIP_PREDICTED = 0, 1, 2, 3, 4 +MARKER = {MINIPSD_RAW: 0, MINIPSD_RLE: 1, MINIPSD_ZIP: 2, MINIPSD_ZIP_PREDICTED: 3} # the file's numbers MINIPSD_ERR_UNSUPPORTED = -7 WRITE_FN = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t) INFLATE_FN = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, @@ -113,7 +114,8 @@ class Layer(ctypes.Structure): ("additional_info", Bytes), ("stored", ctypes.c_void_p), ("section_divider_type", ctypes.c_uint8), ("mask", ctypes.POINTER(Mask)), ("blending_ranges", ctypes.POINTER(BlendingRange)), ("blending_range_count", ctypes.c_uint32), - ("name_pascal", Bytes), ("no_alpha", ctypes.c_uint8)] + ("name_pascal", Bytes), ("no_alpha", ctypes.c_uint8), + ("compression", ctypes.c_int)] class Doc(ctypes.Structure): @@ -1308,7 +1310,7 @@ def main(): found = check(name, psd, w, h, dpi, layers) if psd.version != (2 if fmt else 1): found.append(f"{name}: version {psd.version}") - if compression >= MINIPSD_ZIP and compressions(psd) != {Compression(compression)}: + if compression >= MINIPSD_ZIP and compressions(psd) != {Compression(MARKER[compression])}: found.append(f"{name}: layer channels compressed as {compressions(psd)}") errors += found print(f"{name}: {'FAILED' if found else 'ok'}") @@ -1330,7 +1332,7 @@ def main(): path = out / f"{name}.psd" rc, expected = deep(lib, path, compression) found = [f"{name}: minipsd_write returned {rc}"] if rc != MINIPSD_OK else check_deep(PSDImage.open(path), expected) - if rc == MINIPSD_OK and compression and compressions(PSDImage.open(path)) != {Compression(compression)}: + if rc == MINIPSD_OK and compression >= MINIPSD_ZIP and compressions(PSDImage.open(path)) != {Compression(MARKER[compression])}: found.append(f"{name}: layer channels compressed as {compressions(PSDImage.open(path))}") errors += found print(f"{name}: {'FAILED' if found else 'ok'}") diff --git a/tests/support.h b/tests/support.h index 29ebcf8..4544b70 100644 --- a/tests/support.h +++ b/tests/support.h @@ -97,6 +97,9 @@ Bytes unpack(Reader &r, std::size_t packed, std::size_t expected) return out; } +// The compression markers a file stores (the format's numbers, not the API's MINIPSD_*). +constexpr int RAW_MARKER = 0, RLE_MARKER = 1, ZIP_MARKER = 2, ZIP_PREDICTED_MARKER = 3; + // One channel's rows (raw or RLE, counts read by the caller), scattered into interleaved RGBA. void readPlane(Reader &r, int compression, const std::vector &counts, std::uint32_t w, std::uint32_t h, Bytes &rgba, int offset) diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index 7215c7e..1bbfdc0 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -617,9 +617,10 @@ TEST(MinipsdWriterTest, SixteenBitDocumentsAreWrittenAndReadBack) ASSERT_EQ(info.layer_count, 2u); minipsd_layer_view v; ASSERT_EQ(minipsd_reader_next_layer(r, &v), MINIPSD_MORE); - if (f == file && compression >= MINIPSD_ZIP) { // RLE and RAW markers are 1 and 0 - EXPECT_EQ(v.channel[0].data[1], compression) << "the layer's marker"; - EXPECT_EQ(v.mask.stored.data[1], compression) << "the mask's"; + if (f == file && compression >= MINIPSD_ZIP) { // the file's markers: 2 ZIP, 3 predicted + const int marker = compression == MINIPSD_ZIP ? ZIP_MARKER : ZIP_PREDICTED_MARKER; + EXPECT_EQ(v.channel[0].data[1], marker) << "the layer's marker"; + EXPECT_EQ(v.mask.stored.data[1], marker) << "the mask's"; } std::vector got(px.size()), gotMask(ramp.size()); minipsd_target t; @@ -1328,19 +1329,49 @@ TEST(MinipsdWriterTest, ZipChannelsGoThroughTheCallersDeflate) ASSERT_EQ(minipsd_reader_init(&r, file.data(), file.size(), workspace.data(), workspace.size(), nullptr, &info), MINIPSD_OK); minipsd_layer_view v; ASSERT_EQ(minipsd_reader_next_layer(r, &v), MINIPSD_MORE); - EXPECT_EQ(v.channel[MINIPSD_CH_R].data[1], MINIPSD_ZIP_PREDICTED); + EXPECT_EQ(v.channel[MINIPSD_CH_R].data[1], ZIP_PREDICTED_MARKER); minipsd_target t; minipsd_target_rgba(&t, out.data(), std::size_t(w) * 4); const minipsd_decode_opts zip{0, 0, 0, 8, zlib_stored_inflate, nullptr, scratch.data(), scratch.size()}; ASSERT_EQ(minipsd_decode_layer(r, &v, &t, &zip), MINIPSD_OK); EXPECT_EQ(out, px); - // Refused: no deflate, a compression that is none of the four. + // A layer's own compression: ZIP in an RLE document, and RLE in a ZIP one; the composite RLE + // in both, the deflate asked for whenever a layer deflates. + { + minipsd_layer zipped = layer; + zipped.compression = MINIPSD_ZIP; + minipsd_doc rle = docOf(w, h, &zipped, 1); + EXPECT_EQ(minipsd_doc_check(&rle, nullptr), MINIPSD_ERR_ARG) << "a ZIP layer without deflate"; + const minipsd_write_opts stored{0, zlib_stored_compress, nullptr}; + const Bytes zf = write(rle, MINIPSD_OK, &stored); + Bytes zws(256); + minipsd_reader *zr = nullptr; + minipsd_info zi{}; + ASSERT_EQ(minipsd_reader_init(&zr, zf.data(), zf.size(), zws.data(), zws.size(), nullptr, &zi), MINIPSD_OK); + minipsd_layer_view zv; + ASSERT_EQ(minipsd_reader_next_layer(zr, &zv), MINIPSD_MORE); + for (int k = 0; k < 4; ++k) EXPECT_EQ(zv.channel[k].data[1], ZIP_MARKER) << "the layer's own, channel " << k; + minipsd_layer plain = layer; + plain.compression = MINIPSD_RLE; + minipsd_doc zipDoc = docOf(w, h, &plain, 1); + zipDoc.compression = MINIPSD_ZIP; + const Psd r2 = read(write(zipDoc, MINIPSD_OK, &stored)); + EXPECT_EQ(r2.layers[0].compression, (std::vector(4, RLE_MARKER))) << "the layer's own, in a ZIP document"; + EXPECT_EQ(r2.layers[0].pixels, px); + EXPECT_EQ(r2.compositeCompression, RLE_MARKER) << "the composite: RLE in a ZIP document"; + } + + // Refused: no deflate, a compression that is none of the four, the document's or a layer's. EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG); - for (int bad : {-1, 4}) { + for (int bad : {-1, MINIPSD_ZIP_PREDICTED + 1}) { minipsd_doc b = d; b.compression = bad; EXPECT_EQ(minipsd_doc_check(&b, &opts), MINIPSD_ERR_ARG) << bad; + minipsd_layer l = layer; + l.compression = bad; + b = docOf(w, h, &l, 1); + EXPECT_EQ(minipsd_doc_check(&b, &opts), MINIPSD_ERR_ARG) << "a layer's " << bad; } // A failing deflate, while sizing or while writing; one whose second stream is longer. From 5e8f432d7945444ec1271a930c0e5655899be054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 13:18:39 +0100 Subject: [PATCH 12/22] fix(channels): bound a channel's sides when reading; refuse an RLE row a PSD cannot count The reader refuses a channel with a side above either format's before sizing its plane: a 2^31 x 2^31 mask at 32 bits made a plane of 2^64 bytes, which wrapped to 0 and passed. minipsd_read bounds a mask's sides as a layer's. The writer refuses (MINIPSD_ERR_SIZE) a layer's or the composite's RLE row above 65535 bytes in a PSD, where it sizes rows: 32-bit rows wider than about 16 380 px may not fit the two-byte count, and the file came out with wrong counts and no error. A PSB takes them. minipsd_write's buffer holds the widest row: a row cut by the buffer's end is encoded again from its start, so 4096 bytes cut a long row many times. 30000 x 400 RGBA noise: 1406 ms to 796 ms, the same bytes. --- include/minipsd/read.h | 1 + include/minipsd/reader.h | 2 ++ include/minipsd/writer.h | 49 +++++++++++++++++++++++++++++++--------- tests/test_reader.cpp | 25 +++++++++++++++++--- tests/test_writer.cpp | 26 +++++++++++++++++++++ 5 files changed, 89 insertions(+), 14 deletions(-) diff --git a/include/minipsd/read.h b/include/minipsd/read.h index 5864e92..cc26404 100644 --- a/include/minipsd/read.h +++ b/include/minipsd/read.h @@ -218,6 +218,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit kept.stored.data = NULL; kept.stored.size = 0; if (copy) kept.stored = mpsd__copy(a, v.mask.stored); + if (mw > side || mh > side) return MINIPSD_ERR_UNSUPPORTED; /* the writer's limit, as a layer's */ if (!copy && mw && mh) { mask_pixels = (uint8_t *)mpsd__take(a, mw * mh << wide, (int)wide); /* NULL while measuring */ decoded += mw * mh << wide; diff --git a/include/minipsd/reader.h b/include/minipsd/reader.h index eef3e0f..d1b2da0 100644 --- a/include/minipsd/reader.h +++ b/include/minipsd/reader.h @@ -732,6 +732,8 @@ static int mpsd__chan_open(const minipsd_reader *r, minipsd_bytes c, uint32_t wi const uint64_t plane = mpsd__row_bytes(width, r->depth) * height; /* the raw rows' */ uint64_t left, sum = 0; uint32_t y; + if (width > MINIPSD_MAX_SIDE_PSB || height > MINIPSD_MAX_SIDE_PSB) /* above either format's: plane could wrap */ + return MINIPSD_ERR_UNSUPPORTED; left = c.size - MPSD__MARKER_SIZE; /* next_layer saw the marker */ mpsd__chan_init(r, ch); switch (mpsd__get16(c.data)) { diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 001bfa3..3597e72 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -119,6 +119,8 @@ MINIPSDDEF int minipsd_matte_row(int color_mode, int depth, void *pixels, uint32 return MINIPSD_OK; } +#define MPSD__MAX_COUNT_PSD 0xFFFFu /* a PSD's RLE row count: two bytes */ + /* The compression marker a channel the writer encodes starts with. */ static unsigned mpsd__marker(int compression) { return compression == MINIPSD_RAW ? MPSD__COMP_RAW : MPSD__COMP_RLE; } @@ -468,7 +470,8 @@ static void mpsd__layer_extra(mpsd__sink *s, const minipsd_layer *l, unsigned fr /* What layer channel k's data takes in the out format: its compression, then (RLE) the row byte counts, two bytes each or four in a PSB, then the rows; or the stored channel, its count table - re-written when the formats differ. */ + re-written when the formats differ. 0 (never a size: the marker alone is 2) when a row's count + does not fit a PSD's two bytes, as 32-bit rows wider than about 16 380 px may not. */ static uint64_t mpsd__layer_channel_size(const minipsd_doc *d, const minipsd_layer *l, int k, unsigned out) { const mpsd__source src = mpsd__channel_source(d, l, k); @@ -480,9 +483,11 @@ static uint64_t mpsd__layer_channel_size(const minipsd_doc *d, const minipsd_lay if (!src.samples) return size; MPSD__ENCODED(l); if (compression == MINIPSD_RAW) return size + ((uint64_t)src.width * src.height << mpsd__doc_shift(d)); - for (y = 0; y < src.height; ++y) - size += mpsd__count_size(out) - + mpsd__row_size(d, compression, src.samples + (size_t)y * src.width * src.step, src.step, src.width); + for (y = 0; y < src.height; ++y) { + const uint64_t n = mpsd__row_size(d, compression, src.samples + (size_t)y * src.width * src.step, src.step, src.width); + if (!out && n > MPSD__MAX_COUNT_PSD) return 0; + size += mpsd__count_size(out) + n; + } return size; } @@ -866,7 +871,7 @@ static int mpsd__table_ok(const mpsd__source *src, unsigned from, unsigned out) if (!mpsd__stored_ok(*src->stored, 0, src->height, MPSD__CH_8BIT, from)) return MINIPSD_ERR_ARG; /* a real mask's table: mpsd__mask_ok knew no rows */ for (y = 0; !out && y < src->height; ++y) /* a PSD's counts: two bytes */ - if (mpsd__count(src->stored->data + MPSD__MARKER_SIZE, y, 1) > 0xFFFFu) return MINIPSD_ERR_SIZE; + if (mpsd__count(src->stored->data + MPSD__MARKER_SIZE, y, 1) > MPSD__MAX_COUNT_PSD) return MINIPSD_ERR_SIZE; return MINIPSD_OK; } @@ -974,7 +979,7 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *d, const minipsd_write_opts if (mpsd__zip_plane(d) > ((size_t)-1 >> 2)) return MINIPSD_ERR_SIZE; /* the workspace holds it twice */ rc = mpsd__composite_ok(d, out); if (rc != MINIPSD_OK) return rc; - for (i = 0; i < d->layer_count; ++i) { /* a side within the format's: so is any RLE row count */ + for (i = 0; i < d->layer_count; ++i) { /* a side within the format's; RLE row counts are checked as measured */ const minipsd_layer *l = &d->layers[i]; const int64_t w = (int64_t)l->right - l->left, h = (int64_t)l->bottom - l->top; int k; @@ -1193,6 +1198,7 @@ static int mpsd__measure_layers(minipsd_writer *w) sizes[i * MPSD__CHANNELS + (size_t)k] = MPSD__MARKER_SIZE + (uint64_t)w->zip_size; } else { sizes[i * MPSD__CHANNELS + (size_t)k] = mpsd__layer_channel_size(d, &d->layers[i], k, psb); + if (!sizes[i * MPSD__CHANNELS + (size_t)k]) return MINIPSD_ERR_SIZE; /* a row too long for its count */ } w->content += sizes[i * MPSD__CHANNELS + (size_t)k]; } @@ -1252,6 +1258,7 @@ MINIPSDDEF int minipsd_writer_init(minipsd_writer **out, const minipsd_doc *d, c const uint8_t *row = mpsd__composite_row(&comp, y); for (k = 0; k < (int)mpsd__composite_channels(d); ++k) { const uint64_t n = mpsd__row_size(d, MINIPSD_RLE, row + ((size_t)k << wide), (size_t)mpsd__composite_step(d) << wide, d->width); + if (!w->out && n > MPSD__MAX_COUNT_PSD) return MINIPSD_ERR_SIZE; /* a row too long for its count */ w->composite_sizes[(size_t)k * d->height + y] = (uint32_t)n; total += mpsd__count_size(w->out) + n; } @@ -1462,23 +1469,43 @@ MINIPSDDEF int minipsd_writer_fill(minipsd_writer *w, void *buf, size_t cap, siz /* --- the push writer: the pull writer driven to the end on an allocated workspace --- */ -#define MPSD__CHUNK 4096 /* bytes per callback; on the stack */ +#define MPSD__CHUNK 4096 /* bytes per callback, at least; more when the widest row needs it */ + +/* The widest row the writer encodes: the canvas's, a layer's or a mask's. fill encodes a row cut + by the buffer's end again from its start, so a chunk that holds the widest row cuts each row at + most once; a smaller one cuts a long row many times. */ +static uint32_t mpsd__widest(const minipsd_doc *d) +{ + uint32_t widest = d->width; + size_t i; + for (i = 0; i < d->layer_count; ++i) { + const minipsd_layer *l = &d->layers[i]; + const uint32_t w = (uint32_t)((int64_t)l->right - l->left); /* checked: within a side */ + if (w > widest) widest = w; + if (l->mask && (uint32_t)((int64_t)l->mask->right - l->mask->left) > widest) + widest = (uint32_t)((int64_t)l->mask->right - l->mask->left); + } + return widest; +} MINIPSDDEF int minipsd_write(const minipsd_doc *d, const minipsd_write_opts *opts, minipsd_write_fn fn, void *user) { minipsd_writer *w; void *workspace; - uint8_t chunk[MPSD__CHUNK]; - size_t size = 0, n; + uint8_t *chunk; + size_t size = 0, cap, n; int rc = d ? minipsd_writer_workspace_size(d, opts, &size) : MINIPSD_ERR_ARG; /* d spelled out for static analysers */ if (rc != MINIPSD_OK) return rc; if (!fn) return MINIPSD_ERR_ARG; - workspace = MINIPSD_MALLOC(size); + cap = (size_t)MINIPSD_ROW_BOUND(mpsd__widest(d), mpsd__doc_depth(d)); /* the sides checked: it fits */ + if (cap < MPSD__CHUNK) cap = MPSD__CHUNK; + workspace = MINIPSD_MALLOC(size + cap); /* the chunk after the workspace */ if (!workspace) return MINIPSD_ERR_ALLOC; + chunk = (uint8_t *)workspace + size; rc = minipsd_writer_init(&w, d, opts, workspace, size); if (rc == MINIPSD_OK) { do { /* never n == 0: the call that produces the last byte already returns MINIPSD_OK */ - rc = minipsd_writer_fill(w, chunk, sizeof chunk, &n); + rc = minipsd_writer_fill(w, chunk, cap, &n); if (!fn(user, chunk, n)) rc = MINIPSD_ERR_WRITE; } while (rc == MINIPSD_MORE); } diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp index daa402b..86ac883 100644 --- a/tests/test_reader.cpp +++ b/tests/test_reader.cpp @@ -2423,11 +2423,19 @@ TEST(MinipsdReadTest, ItRefusesWhatTheReaderRefusesAndWhatItCannotHold) s.count = 1; readDoc(build(s).bytes, &lost, MINIPSD_ERR_UNSUPPORTED); - // A layer the writer could not take back: a side above MINIPSD_MAX_SIDE, wide or tall. - for (const bool wide : {true, false}) { + // A layer or a mask the writer could not take back: a side above MINIPSD_MAX_SIDE, wide or tall. + for (const bool wide : {true, false}) + for (const bool masked : {false, true}) { + SCOPED_TRACE(testing::Message() << "wide " << wide << ", masked " << masked); Spec big; Rec r = simpleRec(); - (wide ? r.right : r.bottom) = MINIPSD_MAX_SIDE + 1; + if (masked) { + const std::uint32_t right = wide ? MINIPSD_MAX_SIDE + 1 : 1, bottom = wide ? 1 : MINIPSD_MAX_SIDE + 1; + r.mask = Out().u32(0).u32(0).u32(bottom).u32(right).u8(0).u8(0).zeros(2).b; + r.channels.push_back(chan(-2, 6, 0)); + } else { + (wide ? r.right : r.bottom) = MINIPSD_MAX_SIDE + 1; + } big.layers = {r}; big.count = -1; big.image = rawImage(4, 2, 2, 0); @@ -2935,6 +2943,17 @@ TEST(MinipsdReaderTest, MaskAndKindAtTheEdges) const minipsd_plane p{plane.data(), 1, 2}; EXPECT_EQ(minipsd_decode_mask(b.o.r, &b.v, &p, nullptr), MINIPSD_ERR_FORMAT); + // A mask's rectangle is the file's: a side above either format's is refused before its plane + // is sized. 2^31 x 2^31 at 32 bits is a plane of 2^64 bytes, which wrapped to 0 and passed. + constexpr std::int32_t half = std::int32_t(1) << 30; + Rec wrapping = simpleRec(); + wrapping.mask = maskData(-half, -half, half, half); + wrapping.channels.push_back(chan(-2, 6, 0)); + OneLayer c(wrapping, 32); + minipsd_decode_opts deep{}; + deep.depth = 32; + EXPECT_EQ(minipsd_decode_mask(c.o.r, &c.v, &p, &deep), MINIPSD_ERR_UNSUPPORTED); + auto lsct = [](std::uint32_t type) { return block("lsct", Out().u32(type).b); }; Rec unknown = simpleRec(); unknown.blocks = Out().raw(block("lsct", {})).raw(lsct(7)).b; diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index 1bbfdc0..f6ff01d 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -1164,6 +1164,32 @@ TEST(MinipsdWriterTest, RefusesBeforeWritingAnything) EXPECT_EQ(minipsd_writer_init(&w, &d, nullptr, workspace.data(), size), MINIPSD_ERR_SIZE) << "init sizes it too"; EXPECT_EQ(w, nullptr) << "no writer for a refused document"; + // A PSD counts an RLE row's bytes in 16 bits: a 32-bit row of noise wider than about 16 380 px + // does not fit, the layer's or the composite's. A PSB counts in 32. + { + constexpr std::uint32_t wide = 16400, samples = wide * 4; + std::vector noise(samples), zeros(samples); // zeros: a composite that packs, every byte 0 + std::uint32_t seed = 1; + for (float &f : noise) f = float((seed = seed * 1664525u + 1013904223u) >> 8) / float(1u << 24); + minipsd_layer l{"N", reinterpret_cast(noise.data()), 255, 0}; + l.right = wide; l.bottom = 1; + minipsd_doc deep = docOf(wide, 1, &l, 1); + deep.depth = 32; + deep.composite = reinterpret_cast(zeros.data()); // at 32 bits supplied, never composed + deep.composite_alpha = 0; // with it, matted 1.0f: 3F 80 00 00 again and again, which PackBits cannot pack + EXPECT_EQ(minipsd_write(&deep, nullptr, collect, &out), MINIPSD_ERR_SIZE) << "the layer's rows"; + minipsd_layer raw = l; + raw.compression = MINIPSD_RAW; + deep.layers = &raw; + deep.composite = l.pixels; + EXPECT_EQ(minipsd_write(&deep, nullptr, collect, &out), MINIPSD_ERR_SIZE) << "the composite's rows"; + minipsd_write_opts psb{}; + psb.format = MINIPSD_PSB; + Bytes big; + deep.layers = &l; + EXPECT_EQ(minipsd_write(&deep, &psb, collect, &big), MINIPSD_OK) << "a PSB holds them"; + } + EXPECT_TRUE(out.empty()) << "nothing is written for a refused document"; } From 4907dd0e9438791e5f91f10d5b3cdf11f8b9e3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 15:01:05 +0100 Subject: [PATCH 13/22] perf(vips): psdsave reads the image once, in bands; ZIP with prediction minipsd: minipsd_predict_row, a row as minipsd_write hands it to the deflate for MINIPSD_ZIP_PREDICTED (big-endian, shuffled at 32 bits, each unit its difference from its left neighbour); the writer's own ZIP rows go through the same code. psdsave reads the image once, top to bottom, in the bands libvips hands it, and encodes each channel's rows as they come, so the raw image is never held: at 20000 x 20000 RGB, 1994 MB to 1215 MB for an 813 MB file, 2.1 s to 1.5 s. Each channel keeps room ahead of its rows for its marker and row sizes, so the block a PSD or a PSB stores is written in place. ZIP is deflated as the rows come, a stream a channel; zlib's stream depends on how its input is cut, so these bytes differ from a one-call deflate's. compression=zip-predicted (MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED). psdsave saves a PSB by itself when a PSD cannot hold the file: a row longer than its two-byte count, a layer section past 4 GiB, or a file past the 2 GB Photoshop opens as a PSD. --- include/minipsd.h | 25 ++ include/minipsd/writer.h | 36 ++- libvips-plugin/minipsd_vips.h | 5 +- libvips-plugin/minipsd_vips_psdsave.c | 360 +++++++++++++++++--------- libvips-plugin/test_vips.c | 70 ++++- tests/minipsd_c_smoke.c | 5 +- tests/test_writer.cpp | 46 ++++ 7 files changed, 401 insertions(+), 146 deletions(-) diff --git a/include/minipsd.h b/include/minipsd.h index 889ad5e..e070f71 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -877,6 +877,31 @@ MINIPSDDEF int minipsd_encode_row(int compression, int depth, const void *sample * \return MINIPSD_OK, or MINIPSD_ERR_ARG when pixels is NULL or the depth not 8, 16 or 32. */ MINIPSDDEF int minipsd_matte_row(int color_mode, int depth, void *pixels, uint32_t width); + +/** + * \brief A row of one channel as minipsd_write hands it to the deflate for MINIPSD_ZIP_PREDICTED: + * its samples big-endian, each the difference from its left neighbour. + * + * At 8 bits the differences are the bytes'; at 16 the samples'; at 32 the row is first shuffled + * (every sample's first byte, then every second byte, and so on) and the differences are the + * shuffled bytes'. Every row of a channel so, deflated as one zlib stream after the compression + * marker (two bytes, big-endian: 3), is the channel as a file stores it, for + * minipsd_layer.stored. For MINIPSD_ZIP, without prediction, the rows are minipsd_encode_row's + * raw ones. A program that deflates as rows come (zlib's deflate, fed row by row) holds no raw + * channel. + * + * \param depth Bits a sample: 8, 16 (a uint16_t in host order) or 32 (a float). + * \param samples The row's first sample. + * \param step Bytes from a sample to the next, as minipsd_encode_row's. + * \param width Samples in the row. + * \param out Receives the row: MINIPSD_ROW_BYTES(width, depth) bytes. + * \param cap The size of out. + * \return MINIPSD_OK or an error code. + * + * \retval MINIPSD_ERR_ARG samples or out is NULL, or the depth not 8, 16 or 32. + * \retval MINIPSD_ERR_SIZE cap is less than MINIPSD_ROW_BYTES(width, depth). + */ +MINIPSDDEF int minipsd_predict_row(int depth, const void *samples, size_t step, uint32_t width, uint8_t *out, size_t cap); #endif /* MINIPSD_NO_WRITER */ #ifndef MINIPSD_NO_READER diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 3597e72..98664f6 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -111,6 +111,26 @@ MINIPSDDEF int minipsd_encode_row(int compression, int depth, const void *sample return s.failed ? MINIPSD_ERR_SIZE : MINIPSD_OK; } +/* A row of samples, step bytes apart, as a ZIP channel's raw row: the file's bytes (big-endian), + shuffled at 32 bits and made differences when predicted. */ +static void mpsd__zip_row(uint8_t *out, const uint8_t *row, size_t step, uint32_t width, unsigned depth, int predicted) +{ + const unsigned wide = mpsd__shift(depth); + const size_t bytes = (size_t)width << wide; + size_t x; + for (x = 0; x < bytes; ++x) /* at 32 bits predicted: shuffled, as the format wants */ + out[predicted && wide == MPSD__SHIFT_32 ? mpsd__shuffled(x, width) : x] = row[mpsd__at(x, (ptrdiff_t)step, wide)]; + if (predicted) mpsd__predict(out, width, depth); +} + +MINIPSDDEF int minipsd_predict_row(int depth, const void *samples, size_t step, uint32_t width, uint8_t *out, size_t cap) +{ + if (!samples || !out || depth == MPSD__CH_1BIT || !mpsd__depth_ok((uint32_t)depth)) return MINIPSD_ERR_ARG; + if (cap < mpsd__row_bytes(width, (unsigned)depth)) return MINIPSD_ERR_SIZE; + mpsd__zip_row(out, (const uint8_t *)samples, step, width, (unsigned)depth, 1); + return MINIPSD_OK; +} + MINIPSDDEF int minipsd_matte_row(int color_mode, int depth, void *pixels, uint32_t width) { if (!pixels || depth == MPSD__CH_1BIT || !mpsd__depth_ok((uint32_t)depth)) return MINIPSD_ERR_ARG; @@ -1152,20 +1172,14 @@ static int mpsd__zip_channel(minipsd_writer *w, size_t i, int k) { const minipsd_doc *d = w->doc; const mpsd__source src = mpsd__channel_source(d, &d->layers[i], k); - const unsigned wide = mpsd__doc_shift(d); - const size_t bytes = (size_t)src.width << wide; + const size_t bytes = (size_t)src.width << mpsd__doc_shift(d); const int predicted = mpsd__layer_compression(d, &d->layers[i]) == MINIPSD_ZIP_PREDICTED; - size_t x, y, size = w->zip_room; + size_t y, size = w->zip_room; if (w->zip_layer == i && w->zip_channel == k) return MINIPSD_OK; w->zip_layer = (size_t)-1; - for (y = 0; y < src.height; ++y) { - const uint8_t *row = src.samples + y * src.width * src.step; - uint8_t *out = w->zip_plane + y * bytes; - for (x = 0; x < bytes; ++x) /* at 32 bits predicted: shuffled, as the format wants */ - out[predicted && wide == MPSD__SHIFT_32 ? mpsd__shuffled(x, src.width) : x] - = row[mpsd__at(x, (ptrdiff_t)src.step, wide)]; - if (predicted) mpsd__predict(out, src.width, mpsd__doc_depth(d)); - } + for (y = 0; y < src.height; ++y) + mpsd__zip_row(w->zip_plane + y * bytes, src.samples + y * src.width * src.step, src.step, src.width, mpsd__doc_depth(d), + predicted); if (!w->deflate /* never: minipsd_doc_check asks for one; spelled out for static analysers */ || w->deflate(w->deflate_user, w->zip_plane, bytes * src.height, w->zip_out, &size) || size > w->zip_room) return MINIPSD_ERR_WRITE; diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index a8d96c7..fa196eb 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -27,7 +27,10 @@ extern "C" { typedef enum { MINIPSD_VIPS_COMPRESSION_RLE = 0, /*!< PackBits, as Photoshop writes 8-bit layers: the default. */ MINIPSD_VIPS_COMPRESSION_ZIP = 1, /*!< zlib, without prediction. */ - MINIPSD_VIPS_COMPRESSION_NONE = 2 /*!< Raw: the largest file, the least work. */ + MINIPSD_VIPS_COMPRESSION_NONE = 2, /*!< Raw: the largest file. */ + MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED = 3 /*!< zlib of each sample's difference from its left + neighbour, as Photoshop writes 16-bit layers: + the smaller file at 16 and 32 bits. */ } MinipsdVipsCompression; /** diff --git a/libvips-plugin/minipsd_vips_psdsave.c b/libvips-plugin/minipsd_vips_psdsave.c index 0e18d55..9f9ac5e 100644 --- a/libvips-plugin/minipsd_vips_psdsave.c +++ b/libvips-plugin/minipsd_vips_psdsave.c @@ -8,8 +8,11 @@ * composite; one without, flat, as Photoshop saves an image without layers: the composite only, * without transparency (with `layered`, one layer without transparency too). Grey, RGB or CMYK, * 8 or 16 bits, grey and RGB also 32, CMYK back to PSD's inverted ink; the resolution, the ICC - * profile, EXIF, XMP and IPTC kept. Each channel is encoded once, as minipsd encodes it, and - * handed to minipsd as stored; the image is held whole in memory while it is encoded. + * profile, EXIF, XMP and IPTC kept. The image is read once, top to bottom, in bands; each + * channel is encoded as its rows come, as minipsd encodes it, kept compressed and handed to + * minipsd as stored: memory about the file's size, not the image's. PSB when asked, for a .psb + * file, a side past PSD's, or when a PSD cannot hold the file (a row its counts cannot hold, a + * layer section past 4 GiB, a file past the 2 GB Photoshop opens as a PSD). * * minipsd's writer is compiled in here, private (MINIPSD_STATIC; the reader left out), so a * program that uses minipsd itself keeps its own. @@ -27,6 +30,7 @@ #include "minipsd_vips_psdsave.h" #include "minipsd_vips_util.h" +#define ZLIB_CONST /* next_in const: rows are only read */ #include #include @@ -54,7 +58,8 @@ typedef VipsForeignSaveClass MinipsdVipsPsdsaveClass; G_DEFINE_ABSTRACT_TYPE(MinipsdVipsPsdsave, minipsd_vips_psdsave, VIPS_TYPE_FOREIGN_SAVE) -/* MinipsdVipsCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none"). */ +/* MinipsdVipsCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none", + "zip-predicted"). */ static GType minipsd_vips_psdsave_compression_get_type(void) { static gsize type = 0; @@ -63,6 +68,7 @@ static GType minipsd_vips_psdsave_compression_get_type(void) { MINIPSD_VIPS_COMPRESSION_RLE, "MINIPSD_VIPS_COMPRESSION_RLE", "rle" }, { MINIPSD_VIPS_COMPRESSION_ZIP, "MINIPSD_VIPS_COMPRESSION_ZIP", "zip" }, { MINIPSD_VIPS_COMPRESSION_NONE, "MINIPSD_VIPS_COMPRESSION_NONE", "none" }, + { MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED, "MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED", "zip-predicted" }, { 0, NULL, NULL }, }; g_once_init_leave(&type, g_enum_register_static("MinipsdVipsCompression", values)); @@ -105,12 +111,15 @@ static void minipsd_vips_psdsave_keep_blob(VipsImage *image, const char *name, u /* --- The channels, encoded once: rows as they come, kept compressed, written as stored ------- */ -#define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ -#define PSD_STORED_RAW 0 /* ... raw */ -#define PSD_STORED_RLE 1 /* ... PackBits, its rows' sizes ahead of them */ -#define PSD_STORED_ZIP 2 /* ... zlib, without prediction */ -#define PSD_COUNT_PSD 2 /* an RLE row's size: two bytes in a PSD */ -#define PSD_COUNT_PSB 4 /* ... four in a PSB */ +#define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ +#define PSD_STORED_RAW 0 /* ... raw */ +#define PSD_STORED_RLE 1 /* ... PackBits, its rows' sizes ahead of them */ +#define PSD_STORED_ZIP 2 /* ... zlib, without prediction */ +#define PSD_STORED_ZIP_PREDICTED 3 /* ... zlib of each sample's difference from its left neighbour */ +#define PSD_COUNT_PSD 2 /* an RLE row's size: two bytes in a PSD */ +#define PSD_COUNT_PSB 4 /* ... four in a PSB */ +#define PSD_MAX_FILE INT32_MAX /* a PSD Photoshop opens: 2 GB at most, as Adobe documents it */ +#define PSD_ZIP_ROOM 65536 /* deflate's output grows by at least this */ /* Bytes that grow, past what guint can count. */ typedef struct _MinipsdVipsPsdsaveBytes { @@ -118,24 +127,36 @@ typedef struct _MinipsdVipsPsdsaveBytes { size_t size, room; } MinipsdVipsPsdsaveBytes; -static int minipsd_vips_psdsave_append(MinipsdVipsPsdsaveBytes *b, const void *data, size_t size) +/* Room for extra bytes more: exactly, or at least doubled. */ +static int minipsd_vips_psdsave_grow(MinipsdVipsPsdsaveBytes *b, size_t extra, int exact) { - if (size > b->room - b->size) { - const size_t room = VIPS_MAX(b->room * 2, b->size + size); + if (extra > b->room - b->size) { + const size_t room = exact ? b->size + extra : VIPS_MAX(b->room * 2, b->size + extra); uint8_t *grown; if (room < b->size || !(grown = (uint8_t *)g_try_realloc(b->data, room))) return -1; b->data = grown; b->room = room; } + return 0; +} + +static int minipsd_vips_psdsave_append(MinipsdVipsPsdsaveBytes *b, const void *data, size_t size) +{ + if (minipsd_vips_psdsave_grow(b, size, 0)) return -1; memcpy(b->data + b->size, data, size); b->size += size; return 0; } -/* One channel: its rows, encoded one after another, and each one's size. */ +/* One channel: room ahead (head) for its marker and its rows' sizes, the most a PSB needs, then + its rows encoded one after another; each one's size (RLE), or its deflate (ZIP). The block a + file stores is then written in front of the rows, where they lie: never copied. */ typedef struct _MinipsdVipsPsdsaveChannel { MinipsdVipsPsdsaveBytes rows; + size_t head; size_t *sizes; + z_stream zip; + int zipping; } MinipsdVipsPsdsaveChannel; /* What the build encodes: the layer's channels (the colour ones, then transparency) when there is @@ -144,70 +165,126 @@ typedef struct _MinipsdVipsPsdsaveEncoding { MinipsdVipsPsdsaveChannel layer[PSD_MAX_PLANES], composite[PSD_MAX_PLANES]; unsigned planes; /* the image's bands: colour channels, then alpha */ int layered, alpha, depth, mode; - MinipsdVipsCompression compression; + int layer_compression, composite_compression; /* MINIPSD_RLE, _RAW, _ZIP, _ZIP_PREDICTED */ uint32_t width, height; + size_t longest; /* the longest RLE row: a PSD counts to 65535 */ uint8_t *row; /* one encoded row, MINIPSD_ROW_BOUND */ size_t row_size; + uint8_t *ink; /* one row of pixels, CMYK inverted to PSD's ink */ uint8_t *matted; /* one row of pixels, matted for the composite */ + int failed; /* memory ran out, or deflate failed, while the bands came */ } MinipsdVipsPsdsaveEncoding; +static void minipsd_vips_psdsave_channel_free(MinipsdVipsPsdsaveChannel *c) +{ + g_free(c->rows.data); + g_free(c->sizes); + if (c->zipping) (void)deflateEnd(&c->zip); + memset(c, 0, sizeof *c); +} + static void minipsd_vips_psdsave_encoding_free(MinipsdVipsPsdsaveEncoding *e) { unsigned k; for (k = 0; k < PSD_MAX_PLANES; ++k) { - g_free(e->layer[k].rows.data); - g_free(e->layer[k].sizes); - g_free(e->composite[k].rows.data); - g_free(e->composite[k].sizes); + minipsd_vips_psdsave_channel_free(&e->layer[k]); + minipsd_vips_psdsave_channel_free(&e->composite[k]); } g_free(e->row); + g_free(e->ink); g_free(e->matted); memset(e, 0, sizeof *e); } +/* A channel ready for rows: the head for the tables of channels channels (the composite's first + holds them all, the others none), raw rows' room known ahead, ZIP's deflate started. */ +static int minipsd_vips_psdsave_channel_init(MinipsdVipsPsdsaveChannel *c, int compression, unsigned channels, uint32_t height, + size_t row_bytes) +{ + const int rle = compression == MINIPSD_RLE, raw = compression == MINIPSD_RAW; + c->head = channels ? PSD_MARKER_SIZE + (rle ? (size_t)channels * height * PSD_COUNT_PSB : 0) : 0; + if (rle && !(c->sizes = g_try_new(size_t, height))) return -1; + if (!rle && !raw) { + if (deflateInit(&c->zip, PSD_ZIP_LEVEL) != Z_OK) return -1; + c->zipping = 1; + } + if (minipsd_vips_psdsave_grow(&c->rows, c->head + (raw ? (size_t)(channels ? channels : 1u) * height * row_bytes : 0), 1)) return -1; + c->rows.size = c->head; + return 0; +} + static int minipsd_vips_psdsave_encoding_init(MinipsdVipsPsdsaveEncoding *e, VipsImage *in, int depth, int mode, int alpha, int layered, MinipsdVipsCompression compression) { + static const int layer_compression[] = { MINIPSD_RLE, MINIPSD_ZIP, MINIPSD_RAW, MINIPSD_ZIP_PREDICTED }; /* by option */ const size_t sample = (size_t)(depth / CHAR_BIT); + size_t row_bytes, pixels; unsigned k; memset(e, 0, sizeof *e); e->planes = (unsigned)in->Bands; e->layered = layered; - e->compression = compression; e->alpha = alpha; e->depth = depth; e->mode = mode; e->width = (uint32_t)in->Xsize; e->height = (uint32_t)in->Ysize; + e->layer_compression = layer_compression[compression]; + e->composite_compression = compression == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE; + row_bytes = (size_t)MINIPSD_ROW_BYTES(e->width, depth); + pixels = (size_t)e->width * e->planes * sample; e->row_size = (size_t)MINIPSD_ROW_BOUND(e->width, depth); e->row = (uint8_t *)g_try_malloc(e->row_size); - e->matted = (uint8_t *)g_try_malloc((size_t)e->width * e->planes * sample); - if (!e->row || !e->matted) return -1; + e->ink = (uint8_t *)g_try_malloc(pixels); + e->matted = (uint8_t *)g_try_malloc(pixels); + if (!e->row || !e->ink || !e->matted) return -1; for (k = 0; k < e->planes; ++k) { - if (!(e->composite[k].sizes = g_try_new(size_t, e->height))) return -1; - if (layered && !(e->layer[k].sizes = g_try_new(size_t, e->height))) return -1; + if (minipsd_vips_psdsave_channel_init(&e->composite[k], e->composite_compression, k ? 0 : e->planes, e->height, row_bytes)) + return -1; + if (layered && minipsd_vips_psdsave_channel_init(&e->layer[k], e->layer_compression, 1, e->height, row_bytes)) return -1; } return 0; } -/* Row y of channel k out of interleaved pixels, encoded into c: PackBits, or raw for a channel - deflated whole at the end. */ +/* data deflated onto the channel's rows; with Z_FINISH, the stream ended. */ +static int minipsd_vips_psdsave_deflate(MinipsdVipsPsdsaveChannel *c, const uint8_t *data, size_t size, int flush) +{ + int rc; + c->zip.next_in = data; + c->zip.avail_in = (uInt)size; /* a row: within uInt */ + do { + if (minipsd_vips_psdsave_grow(&c->rows, PSD_ZIP_ROOM, 0)) return -1; + c->zip.next_out = c->rows.data + c->rows.size; + c->zip.avail_out = (uInt)VIPS_MIN(c->rows.room - c->rows.size, (size_t)UINT_MAX); + rc = deflate(&c->zip, flush); + c->rows.size = (size_t)(c->zip.next_out - c->rows.data); + if (rc != Z_OK && rc != Z_STREAM_END && rc != Z_BUF_ERROR) return -1; + } while (flush == Z_FINISH ? rc != Z_STREAM_END : c->zip.avail_out == 0); + return 0; +} + +/* Row y of channel k out of interleaved pixels, encoded into c: PackBits, raw, or deflated (raw + or predicted rows into the channel's stream). */ static int minipsd_vips_psdsave_encode(MinipsdVipsPsdsaveEncoding *e, MinipsdVipsPsdsaveChannel *c, const uint8_t *pixels, - unsigned k, uint32_t y, int raw) + unsigned k, uint32_t y, int compression) { const size_t sample = (size_t)(e->depth / CHAR_BIT); - size_t n; - if (minipsd_encode_row(raw ? MINIPSD_RAW : MINIPSD_RLE, e->depth, pixels + k * sample, e->planes * sample, e->width, e->row, - e->row_size, &n) != MINIPSD_OK - || minipsd_vips_psdsave_append(&c->rows, e->row, n)) - return -1; - c->sizes[y] = n; - return 0; + size_t n = (size_t)MINIPSD_ROW_BYTES(e->width, e->depth); + const int rc = compression == MINIPSD_ZIP_PREDICTED + ? minipsd_predict_row(e->depth, pixels + k * sample, e->planes * sample, e->width, e->row, e->row_size) + : minipsd_encode_row(compression == MINIPSD_RLE ? MINIPSD_RLE : MINIPSD_RAW, e->depth, pixels + k * sample, + e->planes * sample, e->width, e->row, e->row_size, &n); + if (rc != MINIPSD_OK) return -1; + if (c->zipping) return minipsd_vips_psdsave_deflate(c, e->row, n, Z_NO_FLUSH); + if (c->sizes) { + c->sizes[y] = n; + if (n > e->longest) e->longest = n; + } + return minipsd_vips_psdsave_append(&c->rows, e->row, n); } -/* count rows of interleaved pixels, from row y on, stride bytes apart: each channel's rows - encoded, the layer's as they are, the composite's matted (RGB with alpha, as minipsd mattes a - composite it is given). */ +/* count rows of interleaved pixels, from row y on, stride bytes apart: CMYK inverted to PSD's ink, + each channel's rows encoded, the layer's as they are, the composite's matted (RGB with alpha, as + minipsd mattes a composite it is given). */ static int minipsd_vips_psdsave_encode_rows(MinipsdVipsPsdsaveEncoding *e, const uint8_t *rows, uint32_t y, uint32_t count, size_t stride) { @@ -215,18 +292,58 @@ static int minipsd_vips_psdsave_encode_rows(MinipsdVipsPsdsaveEncoding *e, const uint32_t i; unsigned k; for (i = 0; i < count; ++i, rows += stride) { - const uint8_t *composite = rows; + const uint8_t *pixels = rows, *composite; + if (e->mode == MINIPSD_MODE_CMYK) { + memcpy(e->ink, rows, bytes); + minipsd_vips__invert_cmyk(e->ink, e->width, e->planes, e->depth); + pixels = e->ink; + } for (k = 0; e->layered && k < e->planes; ++k) - if (minipsd_vips_psdsave_encode(e, &e->layer[k], rows, k, y + i, e->compression != MINIPSD_VIPS_COMPRESSION_RLE)) - return -1; + if (minipsd_vips_psdsave_encode(e, &e->layer[k], pixels, k, y + i, e->layer_compression)) return -1; + composite = pixels; if (e->alpha) { /* matted as minipsd mattes a composite it is given (RGB; the others as they are) */ - memcpy(e->matted, rows, bytes); + memcpy(e->matted, pixels, bytes); (void)minipsd_matte_row(e->mode, e->depth, e->matted, e->width); composite = e->matted; } for (k = 0; k < e->planes; ++k) - if (minipsd_vips_psdsave_encode(e, &e->composite[k], composite, k, y + i, e->compression == MINIPSD_VIPS_COMPRESSION_NONE)) - return -1; + if (minipsd_vips_psdsave_encode(e, &e->composite[k], composite, k, y + i, e->composite_compression)) return -1; + } + return 0; +} + +/* A band of the image, as libvips hands it: once, top to bottom; the region is the band. */ +static int minipsd_vips_psdsave_band(VipsRegion *region, VipsRect *area, void *user) +{ + MinipsdVipsPsdsaveEncoding *e = (MinipsdVipsPsdsaveEncoding *)user; + if (minipsd_vips_psdsave_encode_rows(e, VIPS_REGION_ADDR_TOPLEFT(region), (uint32_t)area->top, (uint32_t)area->height, + (size_t)VIPS_REGION_LSKIP(region))) { + e->failed = 1; + return -1; + } + return 0; +} + +/* Every row in: the deflates ended, the composite's channels joined behind the first's head (the + file stores them as one block). */ +static int minipsd_vips_psdsave_finish(MinipsdVipsPsdsaveEncoding *e) +{ + MinipsdVipsPsdsaveBytes *first = &e->composite[0].rows; + size_t total = first->size; + unsigned k; + for (k = 0; k < e->planes; ++k) + if (e->layer[k].zipping) { + if (minipsd_vips_psdsave_deflate(&e->layer[k], NULL, 0, Z_FINISH)) return -1; + (void)deflateEnd(&e->layer[k].zip); + e->layer[k].zipping = 0; + } + for (k = 1; k < e->planes; ++k) total += e->composite[k].rows.size; + if (minipsd_vips_psdsave_grow(first, total - first->size, 1)) return -1; + for (k = 1; k < e->planes; ++k) { + memcpy(first->data + first->size, e->composite[k].rows.data, e->composite[k].rows.size); + first->size += e->composite[k].rows.size; + g_free(e->composite[k].rows.data); /* copied: the first holds it now */ + memset(&e->composite[k].rows, 0, sizeof e->composite[k].rows); } return 0; } @@ -239,52 +356,54 @@ static uint8_t *minipsd_vips_psdsave_put(uint8_t *p, uint64_t n, unsigned size) return p + size; } -/* Channels as a file stores them, one block: one marker, every channel's row sizes (RLE), then - every channel's rows (the composite's layout; a layer channel is the same, one channel). NULL - when a size does not fit its count, or memory runs out. */ -static uint8_t *minipsd_vips_psdsave_stored(MinipsdVipsPsdsaveChannel *c, unsigned channels, uint32_t height, unsigned count, - int raw, size_t *size) +/* The block a file stores for channels c[0] .. c[channels - 1], whose rows c[0] holds: the + marker, an RLE one's row sizes in count bytes each, every channel's in turn, then the rows. + Written into c[0]'s head, just ahead of the rows. */ +static minipsd_bytes minipsd_vips_psdsave_block(MinipsdVipsPsdsaveChannel *c, unsigned channels, uint32_t height, unsigned count, + int compression) { - const uint64_t max = count == PSD_COUNT_PSD ? UINT16_MAX : UINT32_MAX; - uint8_t *block, *p; - unsigned k; - uint32_t y; - *size = PSD_MARKER_SIZE + (raw ? 0 : (size_t)channels * height * count); - for (k = 0; k < channels; ++k) *size += c[k].rows.size; - if (!(block = (uint8_t *)g_try_malloc(*size))) return NULL; - p = minipsd_vips_psdsave_put(block, raw ? PSD_STORED_RAW : PSD_STORED_RLE, PSD_MARKER_SIZE); - for (k = 0; !raw && k < channels; ++k) - for (y = 0; y < height; ++y) { - if (c[k].sizes[y] > max) { - g_free(block); - return NULL; - } - p = minipsd_vips_psdsave_put(p, c[k].sizes[y], count); - } - for (k = 0; k < channels; ++k) { - memcpy(p, c[k].rows.data, c[k].rows.size); - p += c[k].rows.size; - g_free(c[k].rows.data); /* copied: the block holds it now */ - memset(&c[k].rows, 0, sizeof c[k].rows); - } + const int rle = compression == MINIPSD_RLE; + const size_t table = rle ? (size_t)channels * height * count : 0; + const size_t start = c->head - PSD_MARKER_SIZE - table; + const uint8_t marker = rle ? PSD_STORED_RLE : compression == MINIPSD_RAW ? PSD_STORED_RAW + : compression == MINIPSD_ZIP ? PSD_STORED_ZIP : PSD_STORED_ZIP_PREDICTED; + minipsd_bytes block; + uint8_t *p = minipsd_vips_psdsave_put(c->rows.data + start, marker, PSD_MARKER_SIZE); + unsigned k; + uint32_t y; + for (k = 0; rle && k < channels; ++k) + for (y = 0; y < height; ++y) p = minipsd_vips_psdsave_put(p, c[k].sizes[y], count); + block.data = c->rows.data + start; + block.size = c->rows.size - start; return block; } -/* A channel's raw rows deflated as a file stores them: the marker, then the zlib stream. */ -static uint8_t *minipsd_vips_psdsave_stored_zip(MinipsdVipsPsdsaveChannel *c, size_t *size) +/* The document's channels as a PSD or a PSB stores them. */ +static void minipsd_vips_psdsave_stored(MinipsdVipsPsdsaveEncoding *e, minipsd_doc *doc, minipsd_bytes *stored, int psb) { - uLongf n = compressBound((uLong)c->rows.size); - uint8_t *block; - if ((uLong)c->rows.size != c->rows.size || !(block = (uint8_t *)g_try_malloc(PSD_MARKER_SIZE + (size_t)n))) return NULL; - (void)minipsd_vips_psdsave_put(block, PSD_STORED_ZIP, PSD_MARKER_SIZE); - if (compress2(block + PSD_MARKER_SIZE, &n, c->rows.data, (uLong)c->rows.size, PSD_ZIP_LEVEL) != Z_OK) { - g_free(block); - return NULL; - } - *size = PSD_MARKER_SIZE + (size_t)n; - g_free(c->rows.data); - memset(&c->rows, 0, sizeof c->rows); - return block; + const unsigned count = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; + unsigned k; + doc->format = psb ? MINIPSD_PSB : MINIPSD_PSD; + for (k = 0; e->layered && k < e->planes; ++k) /* the colour channels, then transparency */ + stored[k] = minipsd_vips_psdsave_block(&e->layer[k], 1, e->height, count, e->layer_compression); + doc->composite_stored = minipsd_vips_psdsave_block(e->composite, e->planes, e->height, count, e->composite_compression); +} + +/* Whether a PSD holds the document: its layer section within the 4 GiB its length counts + (minipsd refuses one past it, MINIPSD_ERR_SIZE), the file within PSD_MAX_FILE. Any other + refusal is minipsd_write's to report. */ +static int minipsd_vips_psdsave_fits_psd(const minipsd_doc *doc) +{ + minipsd_writer *w; + void *workspace; + size_t size = 0; + int rc = minipsd_writer_workspace_size(doc, NULL, &size), fits; + if (rc != MINIPSD_OK) return rc != MINIPSD_ERR_SIZE; + if (!(workspace = g_try_malloc(size))) return 1; /* minipsd_write will not find it either */ + rc = minipsd_writer_init(&w, doc, NULL, workspace, size); + fits = rc == MINIPSD_OK ? minipsd_writer_total(w) <= PSD_MAX_FILE : rc != MINIPSD_ERR_SIZE; + g_free(workspace); + return fits; } static int minipsd_vips_psdsave_build(VipsObject *object) @@ -297,14 +416,11 @@ static int minipsd_vips_psdsave_build(VipsObject *object) minipsd_layer layer; minipsd_doc doc; MinipsdVipsPsdsaveEncoding e; - uint8_t *blocks[PSD_MAX_PLANES + 1]; /* the layer's channels, then the composite */ const void *icc; const char *filename; - size_t icc_size, bytes, colors, count = 0, size = 0; + size_t icc_size, colors, count = 0; double ratio; - void *pixels; - unsigned k, table; - int rc, alpha, failed; + int rc, alpha, psb, memory; if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->build(object)) return -1; in = save->ready; @@ -330,41 +446,25 @@ static int minipsd_vips_psdsave_build(VipsObject *object) doc.height = (uint32_t)in->Ysize; doc.dpi = in->Xres * MM_PER_INCH; if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; - /* PSB by the option, a file named .psb (libvips writes files through a target too), or a side - past PSD's. */ - filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); - doc.format = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE - || in->Ysize > MINIPSD_MAX_SIDE - ? MINIPSD_PSB - : MINIPSD_PSD; - table = doc.format == MINIPSD_PSB ? PSD_COUNT_PSB : PSD_COUNT_PSD; - - /* Every channel encoded once, as minipsd encodes it, and handed to it as stored: minipsd copies - it, where it would encode each row again for its size. */ - if (!(pixels = vips_image_write_to_memory(in, &bytes))) return -1; - if (doc.color_mode == MINIPSD_MODE_CMYK) minipsd_vips__invert_cmyk(pixels, (size_t)in->Xsize * (size_t)in->Ysize, (unsigned)in->Bands, doc.depth); - memset(blocks, 0, sizeof blocks); - memset(stored, 0, sizeof stored); - failed = minipsd_vips_psdsave_encoding_init(&e, in, doc.depth, doc.color_mode, alpha, alpha || psd->layered, psd->compression) - || minipsd_vips_psdsave_encode_rows(&e, (const uint8_t *)pixels, 0, e.height, bytes / e.height); - g_free(pixels); - for (k = 0; !failed && e.layered && k < e.planes; ++k) { /* stored: the colour channels, then transparency */ - blocks[k] = e.compression == MINIPSD_VIPS_COMPRESSION_ZIP - ? minipsd_vips_psdsave_stored_zip(&e.layer[k], &size) - : minipsd_vips_psdsave_stored(&e.layer[k], 1, e.height, table, e.compression == MINIPSD_VIPS_COMPRESSION_NONE, &size); - failed = !blocks[k]; - stored[k].data = blocks[k]; - stored[k].size = size; + + /* Every channel encoded once, as minipsd encodes it, while libvips hands the image over in + bands, top to bottom: memory about the file's size, never the whole image's. */ + memory = minipsd_vips_psdsave_encoding_init(&e, in, doc.depth, doc.color_mode, alpha, alpha || psd->layered, psd->compression); + if (!memory && vips_sink_disc(in, minipsd_vips_psdsave_band, &e)) { + memory = e.failed; + if (!memory) { /* the image's own error, reported where it arose */ + minipsd_vips_psdsave_encoding_free(&e); + return -1; + } } - if (!failed) { - blocks[PSD_MAX_PLANES] = minipsd_vips_psdsave_stored(e.composite, e.planes, e.height, table, - e.compression == MINIPSD_VIPS_COMPRESSION_NONE, &size); - failed = !blocks[PSD_MAX_PLANES]; - doc.composite_stored.data = blocks[PSD_MAX_PLANES]; - doc.composite_stored.size = size; + if (!memory) memory = minipsd_vips_psdsave_finish(&e); + if (memory) { + minipsd_vips_psdsave_encoding_free(&e); + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(MINIPSD_ERR_ALLOC)); + return -1; } - minipsd_vips_psdsave_encoding_free(&e); + memset(stored, 0, sizeof stored); /* past the image's bands: none (transparency of size 0) */ memset(&layer, 0, sizeof layer); layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; layer.opacity = UCHAR_MAX; @@ -372,7 +472,7 @@ static int minipsd_vips_psdsave_build(VipsObject *object) layer.stored = stored; /* its own rectangle, the canvas: a stored layer gives it */ layer.bottom = (int32_t)doc.height; layer.right = (int32_t)doc.width; - doc.layers = alpha || psd->layered ? &layer : NULL; /* without alpha: flat, unless asked */ + doc.layers = e.layered ? &layer : NULL; /* without alpha: flat, unless asked */ doc.layer_count = doc.layers ? 1 : 0; doc.composite_channels = (uint16_t)in->Bands; doc.composite_alpha = (uint8_t)alpha; @@ -390,11 +490,19 @@ static int minipsd_vips_psdsave_build(VipsObject *object) doc.resources = resources; doc.resource_count = count; - rc = failed ? MINIPSD_ERR_ALLOC : minipsd_write(&doc, NULL, minipsd_vips_psdsave_to_target, psd->target); - for (k = 0; k <= PSD_MAX_PLANES; ++k) g_free(blocks[k]); + /* PSB by the option, a file named .psb (libvips writes files through a target too), a side + past PSD's; then, the channels known, a row a PSD cannot count, a layer section past its + 4 GiB, or a file past what Photoshop opens as a PSD. */ + filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); + psb = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE + || in->Ysize > MINIPSD_MAX_SIDE || e.longest > UINT16_MAX; + minipsd_vips_psdsave_stored(&e, &doc, stored, psb); + if (!psb && !minipsd_vips_psdsave_fits_psd(&doc)) minipsd_vips_psdsave_stored(&e, &doc, stored, 1); + + rc = minipsd_write(&doc, NULL, minipsd_vips_psdsave_to_target, psd->target); + minipsd_vips_psdsave_encoding_free(&e); if (rc != MINIPSD_OK) { - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", - failed ? "out of memory, or a row too long for a PSD's counts (save as PSB)" : minipsd_error_string(rc)); + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); return -1; } return vips_target_end(psd->target); @@ -424,7 +532,7 @@ static void minipsd_vips_psdsave_class_init(MinipsdVipsPsdsaveClass *class) PSD_ARGS_BEGIN VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, psb), FALSE); - VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layer's compression: rle, zip or none (the composite too)", + VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layer's compression: rle, zip, zip-predicted, or none (the composite too)", VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, compression), minipsd_vips_psdsave_compression_get_type(), MINIPSD_VIPS_COMPRESSION_RLE); VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index a0a2bf3..ca0530e 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -20,6 +20,7 @@ #include "minipsd_vips.h" #include +#define ZLIB_CONST /* next_in const */ #include #include @@ -54,6 +55,11 @@ #define STORED_RAW 0 /* a channel's stored compression markers */ #define STORED_RLE 1 #define STORED_ZIP 2 +#define STORED_ZIP_PREDICTED 3 +#define WIDE_W 16400 /* 32-bit noise rows longer than a PSD's two-byte counts hold */ +#define WIDE_H 2 +#define PSD_RGB_BANDS 3 +#define PSD_VERSION_AT 5 /* the header's version, 1 PSD or 2 PSB: its low byte */ /* The ZIP document's two layers: one reaching past the canvas's left edge, one inside. */ #define ZIP_W 200 @@ -101,6 +107,29 @@ static int deflate_zlib(void *user, const uint8_t *src, size_t src_size, uint8_t return rc != Z_OK; } +/* zlib fed one row at a time (user: the row's bytes), as psdsave deflates rows as they come: the + stream zlib makes depends on how its input is cut. */ +static int deflate_rows(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) +{ + const size_t row = *(const size_t *)user; + z_stream z; + size_t at; + int rc = Z_OK; + memset(&z, 0, sizeof z); + if (deflateInit(&z, ZIP_LEVEL) != Z_OK) return 1; + z.next_out = dst; + z.avail_out = (uInt)*dst_size; + for (at = 0; rc == Z_OK && at < src_size; at += row) { + z.next_in = src + at; + z.avail_in = (uInt)(src_size - at < row ? src_size - at : row); + rc = deflate(&z, Z_NO_FLUSH); + } + if (rc == Z_OK) rc = deflate(&z, Z_FINISH); + *dst_size = (size_t)z.total_out; + (void)deflateEnd(&z); + return rc != Z_STREAM_END; +} + static int inflate_zlib(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) { uLongf n = (uLongf)dst_size; @@ -412,8 +441,9 @@ static VipsImage *make_image(const void *data, int bands, VipsBandFormat format, /* What minipsd_write makes of the image from its pixels, as psdsave wrote it before it encoded the channels itself: one layer when layered (without transparency when the image has no alpha), the composite, CMYK as PSD's inverted ink (8-bit, as the tests' images are), the image's - resolution, the EXIF, XMP and IPTC libvips gave psdsave (taken from its file); an option ("compression", - "psb") as psdsave takes it. */ + resolution, the EXIF, XMP and IPTC libvips gave psdsave (taken from its file), its format as + psdsave chose it (the file's); a "compression" option as psdsave takes it, ZIP deflated a row at + a time as psdsave deflates it. */ static int from_pixels(VipsImage *image, unsigned colors, int layered, const char *option, int value, const uint8_t *file, size_t file_size, out_buffer *b) { @@ -428,6 +458,7 @@ static int from_pixels(VipsImage *image, unsigned colors, int layered, const cha size_t bytes = 0, i, count = 0; uint8_t *pixels = (uint8_t *)vips_image_write_to_memory(image, &bytes); const int alpha = (unsigned)image->Bands > colors; + size_t row = (size_t)image->Xsize * vips_format_sizeof(image->BandFmt); /* a channel's row */ int rc; if (!pixels) return MINIPSD_ERR_ALLOC; if (image->Type == VIPS_INTERPRETATION_CMYK && image->BandFmt == VIPS_FORMAT_UCHAR) @@ -452,9 +483,11 @@ static int from_pixels(VipsImage *image, unsigned colors, int layered, const cha doc.compression = !option || strcmp(option, "compression") != 0 ? MINIPSD_RLE : value == MINIPSD_VIPS_COMPRESSION_ZIP ? MINIPSD_ZIP : value == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW + : value == MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED ? MINIPSD_ZIP_PREDICTED : MINIPSD_RLE; - doc.format = option && strcmp(option, "psb") == 0 && value ? MINIPSD_PSB : MINIPSD_PSD; - opts.deflate = deflate_zlib; + doc.format = file_size > PSD_VERSION_AT && file[PSD_VERSION_AT] == MINIPSD_PSB ? MINIPSD_PSB : MINIPSD_PSD; + opts.deflate = deflate_rows; + opts.deflate_user = &row; if (minipsd_reader_init(&r, file, file_size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK) for (i = 0; i < sizeof kept / sizeof kept[0]; ++i) /* in psdsave's order */ if (minipsd_reader_find_resource(r, kept[i], &resources[count]) == MINIPSD_OK) { @@ -875,6 +908,31 @@ int main(int argc, char **argv) MINIPSD_VIPS_COMPRESSION_ZIP); check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-raw.psd", argv[1], 0, "compression", MINIPSD_VIPS_COMPRESSION_NONE); + check_saved(make_image(grey16, 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16), "save-predicted.psd", argv[1], 0, + "compression", MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED); + snprintf(path, sizeof path, "%s/save-predicted.psd", argv[1]); + check(first_layer_compression(path) == STORED_ZIP_PREDICTED, "ZIP with prediction by option", path); + { /* 32-bit noise, rows a PSD cannot count: a PSB, unasked */ + const size_t samples = (size_t)WIDE_W * WIDE_H * PSD_RGB_BANDS; + float *noise = g_new(float, samples); + guint32 seed = 1; + VipsImage *memory; + size_t s; + for (s = 0; s < samples; ++s) noise[s] = (float)((seed = seed * 1664525u + 1013904223u) >> CHAR_BIT) / (float)(1u << 24); + memory = vips_image_new_from_memory_copy(noise, samples * sizeof *noise, WIDE_W, WIDE_H, PSD_RGB_BANDS, VIPS_FORMAT_FLOAT); + g_free(noise); + image = NULL; + if (memory) { + vips_copy(memory, &image, "interpretation", VIPS_INTERPRETATION_scRGB, "xres", DPI / MM_PER_INCH, "yres", + DPI / MM_PER_INCH, NULL); + g_object_unref(memory); + } + check_saved(image, "save-wide.psd", argv[1], 1, NULL, 0); + snprintf(path, sizeof path, "%s/save-wide.psd", argv[1]); + file = read_all(path, &size); + check(file && size > PSD_VERSION_AT && file[PSD_VERSION_AT] == MINIPSD_PSB, "PSB when a PSD cannot count its rows", path); + free(file); + } snprintf(path, sizeof path, "%s/save-raw.psd", argv[1]); check(first_layer_compression(path) == STORED_RAW, "raw by option", path); image = make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB); /* by its nick, in an option string */ @@ -890,7 +948,7 @@ int main(int argc, char **argv) check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-psb.psd", argv[1], 1, "psb", TRUE); snprintf(path, sizeof path, "%s/save-psb.psd", argv[1]); file = read_all(path, &size); - check(file && size > 5 && file[5] == MINIPSD_PSB, "PSB by option", path); + check(file && size > PSD_VERSION_AT && file[PSD_VERSION_AT] == MINIPSD_PSB, "PSB by option", path); free(file); /* A file named .psb is a PSB; a buffer or a target in memory has no name: psb says it. */ snprintf(path, sizeof path, "%s/save-suffix.psb", argv[1]); @@ -901,7 +959,7 @@ int main(int argc, char **argv) check(back != NULL, "read back", path); if (back) g_object_unref(back); file = read_all(path, &size); - check(file && size > 5 && file[5] == MINIPSD_PSB, "PSB by suffix", path); + check(file && size > PSD_VERSION_AT && file[PSD_VERSION_AT] == MINIPSD_PSB, "PSB by suffix", path); free(file); snprintf(path, sizeof path, "%s/save-named.psd", argv[1]); diff --git a/tests/minipsd_c_smoke.c b/tests/minipsd_c_smoke.c index c6e63ff..52bd096 100644 --- a/tests/minipsd_c_smoke.c +++ b/tests/minipsd_c_smoke.c @@ -75,12 +75,13 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size) if (rc != MINIPSD_OK || pulled != b.size || memcmp(again, b.data, b.size) != 0) return -102; - /* A row encoded and a row matted by the caller, as the writer does them. */ + /* A row encoded, a row matted and a row predicted by the caller, as the writer does them. */ { uint8_t row[MINIPSD_ROW_BOUND(3, 8)], copy[sizeof px]; memcpy(copy, px, sizeof copy); if (minipsd_encode_row(MINIPSD_RLE, 8, px, 4, 3, row, sizeof row, &n) != MINIPSD_OK || !n - || minipsd_matte_row(MINIPSD_MODE_RGB, 8, copy, 3) != MINIPSD_OK) + || minipsd_matte_row(MINIPSD_MODE_RGB, 8, copy, 3) != MINIPSD_OK + || minipsd_predict_row(8, px, 4, 3, row, sizeof row) != MINIPSD_OK) return -126; } diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index f6ff01d..d71d0b5 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -28,6 +28,7 @@ static std::set encodedLayers; #include #include +#include extern "C" int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size); // minipsd_c_smoke.c @@ -1388,6 +1389,51 @@ TEST(MinipsdWriterTest, ZipChannelsGoThroughTheCallersDeflate) EXPECT_EQ(r2.compositeCompression, RLE_MARKER) << "the composite: RLE in a ZIP document"; } + // minipsd_predict_row: the rows the writer deflates for ZIP_PREDICTED, at each depth (shuffled + // at 32); after the marker, deflated, the channel as the file has it. + for (const int depth : {8, 16, 32}) { + SCOPED_TRACE(depth); + const std::size_t sample = std::size_t(depth) / 8, pixel = 4 * sample, row = std::size_t(w) * sample; + Bytes pixels(std::size_t(w) * h * pixel); + for (std::size_t i = 0; i < std::size_t(w) * h * 4; ++i) { // a sample each, as its depth holds it + const std::uint16_t s16 = std::uint16_t(i * 997); + const float s32 = float(i % 101) / 100.0f; + std::memcpy(pixels.data() + i * sample, depth == 8 ? static_cast(&px[i % px.size()]) + : depth == 16 ? static_cast(&s16) : static_cast(&s32), + sample); + } + minipsd_layer l{"P", pixels.data(), 255, 0}; + minipsd_doc pd = docOf(w, h, &l, 1); + pd.depth = std::uint16_t(depth); + pd.composite = pixels.data(); // at 32 bits supplied, never composed + pd.compression = MINIPSD_ZIP_PREDICTED; + const minipsd_write_opts stored{0, zlib_stored_compress, nullptr}; + const Bytes pf = write(pd, MINIPSD_OK, &stored); + Bytes pws(256); + minipsd_reader *pr = nullptr; + minipsd_info pi{}; + ASSERT_EQ(minipsd_reader_init(&pr, pf.data(), pf.size(), pws.data(), pws.size(), nullptr, &pi), MINIPSD_OK); + minipsd_layer_view pv; + ASSERT_EQ(minipsd_reader_next_layer(pr, &pv), MINIPSD_MORE); + Bytes rows(row * h), stream(rows.size() * 2 + 64); + for (std::uint32_t y = 0; y < h; ++y) // red: the pixel's first sample + ASSERT_EQ(minipsd_predict_row(depth, pixels.data() + y * w * pixel, pixel, w, rows.data() + y * row, row), MINIPSD_OK); + std::size_t n = stream.size(); + ASSERT_EQ(zlib_stored_compress(nullptr, rows.data(), rows.size(), stream.data(), &n), 0); + Bytes want{0, ZIP_PREDICTED_MARKER}; + want.insert(want.end(), stream.begin(), stream.begin() + std::ptrdiff_t(n)); + EXPECT_EQ(Bytes(pv.channel[MINIPSD_CH_R].data, pv.channel[MINIPSD_CH_R].data + pv.channel[MINIPSD_CH_R].size), want); + } + { + std::uint8_t one[4] = {}; + EXPECT_EQ(minipsd_predict_row(8, px.data(), 4, 4, one, sizeof one), MINIPSD_OK); + EXPECT_EQ(minipsd_predict_row(8, px.data(), 4, 4, one, sizeof one - 1), MINIPSD_ERR_SIZE) << "a byte short"; + EXPECT_EQ(minipsd_predict_row(1, px.data(), 1, 8, one, sizeof one), MINIPSD_ERR_ARG) << "bits are never predicted"; + EXPECT_EQ(minipsd_predict_row(12, px.data(), 4, 4, one, sizeof one), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_predict_row(8, nullptr, 4, 4, one, sizeof one), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_predict_row(8, px.data(), 4, 4, nullptr, sizeof one), MINIPSD_ERR_ARG); + } + // Refused: no deflate, a compression that is none of the four, the document's or a layer's. EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG); for (int bad : {-1, MINIPSD_ZIP_PREDICTED + 1}) { From 47a9a62f98339ded2c096e97e20e7fa5a4ea9c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 15:30:17 +0100 Subject: [PATCH 14/22] build(release): the libvips plugin as minipsd-vips-X.Y.Z.zip A CMake project that builds as it is: the plugin's sources, minipsd.h amalgamated and private to the plugin (a program's own minipsd, of any version, is left alone), cmake/minipsd_libvips.cmake to find libvips, a README and example.c, which saves a PSD through the plugin, reads it back, and is the zip's test. tools/package_vips.cmake packs it from libvips-plugin/release/. --- .github/workflows/ci.yml | 18 ++++++++ .github/workflows/release.yml | 29 ++++++++++-- cmake/minipsd_libvips.cmake | 5 +- libvips-plugin/release/CMakeLists.txt | 39 ++++++++++++++++ libvips-plugin/release/README.md | 66 +++++++++++++++++++++++++++ libvips-plugin/release/example.c | 46 +++++++++++++++++++ tools/package_vips.cmake | 51 +++++++++++++++++++++ 7 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 libvips-plugin/release/CMakeLists.txt create mode 100644 libvips-plugin/release/README.md create mode 100644 libvips-plugin/release/example.c create mode 100644 tools/package_vips.cmake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e7d358..0982024 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,24 @@ jobs: cmake --build --preset ${{ matrix.preset }} ctest --preset ${{ matrix.preset }} + # The plugin as released (minipsd-vips-X.Y.Z.zip, tools/package_vips.cmake): built from the + # zip alone, outside the repository, its example run; libvips as fetched above. The release + # builds it again on Linux before publishing. + - name: Check the released plugin builds and runs + if: matrix.vips == 'ON' + shell: pwsh + run: | + $PSNativeCommandUseErrorActionPreference = $true + cmake -DOUTPUT_DIR="$env:RUNNER_TEMP/dist" -P tools/package_vips.cmake + $zip = Get-Item "$env:RUNNER_TEMP/dist/minipsd-vips-*.zip" + cmake -E chdir "$env:RUNNER_TEMP" cmake -E tar xf $zip.FullName + $deps = "$env:GITHUB_WORKSPACE/build/${{ matrix.preset }}/_deps" + cmake -S (Join-Path $env:RUNNER_TEMP $zip.BaseName) -B "$env:RUNNER_TEMP/plugin" ` + "-DFETCHCONTENT_SOURCE_DIR_MINIPSD_VIPS=$deps/minipsd_vips-src" ` + "-DFETCHCONTENT_SOURCE_DIR_MINIPSD_ZLIB_HEADERS=$deps/minipsd_zlib_headers-src" + cmake --build "$env:RUNNER_TEMP/plugin" + ctest --test-dir "$env:RUNNER_TEMP/plugin" -C Debug --output-on-failure + # The same tests, instrumented, on Linux: # linux-asan AddressSanitizer + UBSan: out-of-bounds access, overflow, undefined shifts. # No round trip: Python would need the sanitizer runtime preloaded. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2a8475..c94ac89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,8 @@ # Release: a pushed tag X.Y.Z (bare semver) checks that CI already passed on its commit on master, # checks the tag against the header version and the changelog, and publishes minipsd.h (include/ joined into one file by -# tools/amalgamate.cmake) + minipsd.h.sha256 as a GitHub Release with a build-provenance attestation. "Run workflow" (workflow_dispatch) does everything except publish, +# tools/amalgamate.cmake) + minipsd.h.sha256, and the libvips plugin as a CMake project that builds as it is +# (minipsd-vips-X.Y.Z.zip, tools/package_vips.cmake, built and run here first) + its .sha256, as a GitHub Release +# with build-provenance attestations. "Run workflow" (workflow_dispatch) does everything except publish, # so it is a dry run. See docs/GITHUB-RELEASE.md. name: Release @@ -63,10 +65,29 @@ jobs: cmake -DOUTPUT=dist/minipsd.h -P tools/amalgamate.cmake cd dist && sha256sum minipsd.h > minipsd.h.sha256 && cat minipsd.h.sha256 - # Verifiable with: gh attestation verify minipsd.h --repo ShadobaDev/minipsd + # The plugin's sources, the single-file header, how libvips is found and an example, one zip. + - name: Stage the libvips plugin and its checksum + run: | + cmake -DOUTPUT_DIR=dist -P tools/package_vips.cmake + cd dist && for zip in minipsd-vips-*.zip; do sha256sum "$zip" > "$zip.sha256" && cat "$zip.sha256"; done + + # Built from the zip alone, outside the repository, against the system's libvips; its example + # saves a PSD through the plugin and reads it back. + - name: Check the plugin builds and runs as released + run: | + sudo apt-get update && sudo apt-get install -y --no-install-recommends libvips-dev ninja-build + mkdir "$RUNNER_TEMP/plugin" && cd "$RUNNER_TEMP/plugin" + cmake -E tar xf "$GITHUB_WORKSPACE"/dist/minipsd-vips-*.zip + cmake -S minipsd-vips-* -B build -G Ninja + cmake --build build + ctest --test-dir build --output-on-failure + + # Verifiable with: gh attestation verify minipsd.h --repo ShadobaDev/minipsd (the zip alike) - uses: actions/attest-build-provenance@v4 with: - subject-path: dist/minipsd.h + subject-path: | + dist/minipsd.h + dist/minipsd-vips-*.zip - name: Publish the GitHub Release if: github.ref_type == 'tag' @@ -78,3 +99,5 @@ jobs: files: | dist/minipsd.h dist/minipsd.h.sha256 + dist/minipsd-vips-*.zip + dist/minipsd-vips-*.zip.sha256 diff --git a/cmake/minipsd_libvips.cmake b/cmake/minipsd_libvips.cmake index 35598e3..ba774f1 100644 --- a/cmake/minipsd_libvips.cmake +++ b/cmake/minipsd_libvips.cmake @@ -1,6 +1,7 @@ # --------------------------------------------------------------------------- -# libvips for what uses it here: examples/vips_composite.c (MINIPSD_VIPS_EXAMPLE) and the plugin -# in libvips-plugin/ (MINIPSD_VIPS_PLUGIN). minipsd itself never depends on libvips. +# libvips for what uses it: minipsd's libvips plugin (libvips-plugin/, MINIPSD_VIPS_PLUGIN; or the +# plugin as released, minipsd-vips-.zip) and examples/vips_composite.c +# (MINIPSD_VIPS_EXAMPLE). minipsd itself never depends on libvips. # # Windows fetches libvips' official prebuilt "web" build, pinned (the newest release when set); # elsewhere pkg-config finds the installed one. Gives: diff --git a/libvips-plugin/release/CMakeLists.txt b/libvips-plugin/release/CMakeLists.txt new file mode 100644 index 0000000..9d267c4 --- /dev/null +++ b/libvips-plugin/release/CMakeLists.txt @@ -0,0 +1,39 @@ +# --------------------------------------------------------------------------- +# minipsd's libvips plugin, as released: psdload and psdsave for a program that links libvips. +# +# add_subdirectory(minipsd-vips-) +# target_link_libraries(my-app PRIVATE minipsd::vips) +# +# then minipsd_vips_init() after VIPS_INIT() (README.md). libvips: fetched on Windows (libvips' +# official prebuilt build, pinned), found with pkg-config elsewhere; or the program's own, as the +# targets minipsd_libvips and minipsd_libvips_zlib, defined before this (cmake/minipsd_libvips.cmake). +# minipsd is compiled into the plugin from include/minipsd.h, private to it: a program's own +# minipsd, of any version, is left alone. +# +# minipsd::vips the plugin, a static library +# minipsd-vips-example example.c: an image saved and read back through the plugin (its test) +# --------------------------------------------------------------------------- +cmake_minimum_required(VERSION 3.21) # PROJECT_IS_TOP_LEVEL +project(minipsd_vips LANGUAGES C) + +option(MINIPSD_VIPS_EXAMPLE "Build example.c, and its test" ${PROJECT_IS_TOP_LEVEL}) + +include(cmake/minipsd_libvips.cmake) + +file(GLOB _minipsd_vips_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h") +add_library(minipsd_vips STATIC ${_minipsd_vips_sources}) +add_library(minipsd::vips ALIAS minipsd_vips) +target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd_libvips_zlib) +set_target_properties(minipsd_vips PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + +if(MINIPSD_VIPS_EXAMPLE) + add_executable(minipsd-vips-example example.c) + target_link_libraries(minipsd-vips-example PRIVATE minipsd::vips) + set_target_properties(minipsd-vips-example PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + enable_testing() + add_test(NAME minipsd-vips-example COMMAND minipsd-vips-example "${CMAKE_CURRENT_BINARY_DIR}/example.psd") + if(MINIPSD_LIBVIPS_ENV) # Windows: libvips' DLLs on PATH + set_tests_properties(minipsd-vips-example PROPERTIES ENVIRONMENT "${MINIPSD_LIBVIPS_ENV}") + endif() +endif() diff --git a/libvips-plugin/release/README.md b/libvips-plugin/release/README.md new file mode 100644 index 0000000..463bcd4 --- /dev/null +++ b/libvips-plugin/release/README.md @@ -0,0 +1,66 @@ +# minipsd's libvips plugin + +minipsd as libvips' loader and saver of PSD and PSB: `vips_image_new_from_file` and +`vips_image_write_to_file` take `.psd` and `.psb` in your program. MIT, as minipsd +(`LICENSE.MIT`). Project: ; the full description is its +manual's "PSD in libvips: the plugin". + +## What is here + +| | | +|---|---| +| `src/` | the plugin; `minipsd_vips.h` is its API | +| `include/minipsd.h` | minipsd, one file, compiled into the plugin and private to it | +| `cmake/minipsd_libvips.cmake` | how libvips is found: Windows fetches libvips' official prebuilt build (pinned), elsewhere pkg-config | +| `CMakeLists.txt` | builds `minipsd::vips`, a static library | +| `example.c` | an image saved and read back through the plugin: its test | + +## Build + +In your CMake project: + +```cmake +add_subdirectory(minipsd-vips-) +target_link_libraries(my-app PRIVATE minipsd::vips) +``` + +A project with libvips of its own defines two targets before `add_subdirectory`, and nothing is +fetched or found: `minipsd_libvips` (libvips, glib, gobject) and `minipsd_libvips_zlib` (the zlib +libvips links), each an `INTERFACE` library or `IMPORTED GLOBAL`. + +On its own, with its example as the test: + +```bash +cmake -S minipsd-vips- -B build +cmake --build build +ctest --test-dir build # Windows: add -C Debug (or the configuration built) +``` + +Without CMake, compile `src/*.c` with `include/` on the include path and libvips' and zlib's +flags (`pkg-config --cflags vips zlib`), and link the objects into your program. + +## Use + +Register the plugin once, after `VIPS_INIT` (libvips' prebuilt Windows builds load no modules): + +```c +#include "minipsd_vips.h" + +if (VIPS_INIT(argv[0])) vips_error_exit(NULL); +minipsd_vips_init(); +VipsImage *page = vips_image_new_from_file("page.psd", NULL); /* the composite */ +VipsImage *third = vips_image_new_from_file("page.psd", "layer", 2, NULL); /* the third layer up */ +vips_image_write_to_file(page, "copy.psd[compression=zip]", NULL); +``` + +- **Loading** (`psdload`, `psdload_buffer`, `psdload_source`): the composite, or with `layer` one + layer, bottom first. +- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): an image with alpha as one layer and + the composite; without, flat (the composite only), or with `layered` one layer as well. Grey, + RGB or CMYK at 8 or 16 bits, grey and RGB as floats. Options: `compression` (`rle`, the + default; `zip`; `zip-predicted`; `none`), `psb`, `layer_name`, `layered`. The image is read + once, in bands: memory about the file's size. A PSB when asked, for a `.psb` file, or when a + PSD cannot hold the image. + +minipsd is compiled into the plugin privately, so a program that also uses minipsd itself, at +any version, keeps its own. diff --git a/libvips-plugin/release/example.c b/libvips-plugin/release/example.c new file mode 100644 index 0000000..6206e59 --- /dev/null +++ b/libvips-plugin/release/example.c @@ -0,0 +1,46 @@ +/** + * \file + * \brief minipsd's libvips plugin in a program: an RGB image with alpha saved as a PSD by psdsave, + * then read back by psdload, its size and bands as saved. + * + * minipsd-vips-example out.psd + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#include + +#include "minipsd_vips.h" + +#include +#include + +#define SIDE 64 /* pixels a side */ +#define BANDS 4 /* RGB and alpha: one layer and the composite */ + +int main(int argc, char **argv) +{ + VipsImage *image = NULL, *back = NULL; + const char *loader; + int ok; + if (argc != 2) { + fprintf(stderr, "usage: %s out.psd\n", argv[0]); + return 2; + } + if (VIPS_INIT(argv[0])) vips_error_exit(NULL); + minipsd_vips_init(); /* psdload and psdsave, after VIPS_INIT */ + + if (vips_black(&image, SIDE, SIDE, "bands", BANDS, NULL) || vips_image_write_to_file(image, argv[1], NULL) + || !(back = vips_image_new_from_file(argv[1], NULL))) + vips_error_exit(NULL); + loader = vips_foreign_find_load(argv[1]); + ok = loader && strcmp(loader, "MinipsdVipsPsdloadFile") == 0 && back->Xsize == SIDE && back->Ysize == SIDE + && back->Bands == BANDS; + printf("%s: %dx%d, %d bands, read by %s: %s\n", argv[1], back->Xsize, back->Ysize, back->Bands, loader ? loader : "nothing", + ok ? "ok" : "FAILED"); + g_object_unref(back); + g_object_unref(image); + vips_shutdown(); + return ok ? 0 : 1; +} diff --git a/tools/package_vips.cmake b/tools/package_vips.cmake new file mode 100644 index 0000000..1453eb3 --- /dev/null +++ b/tools/package_vips.cmake @@ -0,0 +1,51 @@ +# Packs the libvips plugin as released: /minipsd-vips-.zip, a CMake project +# that builds as it is (libvips-plugin/release/README.md). Its tree: +# +# CMakeLists.txt, README.md, example.c libvips-plugin/release/ +# src/ libvips-plugin/minipsd_vips*.c and .h (not its test) +# include/minipsd.h include/ joined by tools/amalgamate.cmake +# cmake/minipsd_libvips.cmake as the repository finds libvips +# LICENSE.MIT +# +# cmake -DOUTPUT_DIR= -P tools/package_vips.cmake +cmake_minimum_required(VERSION 3.21) + +if(NOT OUTPUT_DIR) + message(FATAL_ERROR "usage: cmake -DOUTPUT_DIR= -P tools/package_vips.cmake") +endif() +get_filename_component(_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) +get_filename_component(_out "${OUTPUT_DIR}" ABSOLUTE) + +# The version, as the project reads it: from minipsd.h. +file(STRINGS "${_root}/include/minipsd.h" _lines REGEX "^#define MINIPSD_VERSION_(MAJOR|MINOR|PATCH) +[0-9]+$") +set(_version "") +foreach(_part MAJOR MINOR PATCH) + if(NOT _lines MATCHES "MINIPSD_VERSION_${_part} +([0-9]+)") + message(FATAL_ERROR "minipsd.h does not define MINIPSD_VERSION_${_part}") + endif() + list(APPEND _version "${CMAKE_MATCH_1}") +endforeach() +list(JOIN _version "." _version) + +set(_name "minipsd-vips-${_version}") +set(_stage "${_out}/${_name}") +file(REMOVE_RECURSE "${_stage}") +file(REMOVE "${_out}/${_name}.zip") + +file(GLOB _sources "${_root}/libvips-plugin/minipsd_vips*.c" "${_root}/libvips-plugin/minipsd_vips*.h") +file(COPY ${_sources} DESTINATION "${_stage}/src") +file(COPY "${_root}/libvips-plugin/release/CMakeLists.txt" "${_root}/libvips-plugin/release/README.md" + "${_root}/libvips-plugin/release/example.c" "${_root}/LICENSE.MIT" DESTINATION "${_stage}") +file(COPY "${_root}/cmake/minipsd_libvips.cmake" DESTINATION "${_stage}/cmake") +execute_process(COMMAND "${CMAKE_COMMAND}" "-DOUTPUT=${_stage}/include/minipsd.h" -P "${_root}/tools/amalgamate.cmake" + RESULT_VARIABLE _rc) +if(_rc) + message(FATAL_ERROR "tools/amalgamate.cmake failed") +endif() + +execute_process(COMMAND "${CMAKE_COMMAND}" -E tar cf "${_name}.zip" --format=zip "${_name}" + WORKING_DIRECTORY "${_out}" RESULT_VARIABLE _rc) +if(_rc) + message(FATAL_ERROR "zipping ${_stage} failed") +endif() +message(STATUS "${_out}/${_name}.zip") From 7995af76b0824c6809bf0175f336e9040d64e5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 20:03:35 +0100 Subject: [PATCH 15/22] docs(misc): 0.4.0 The version is 0.4.0: breaking, so a MINOR. The changelog says what breaks and how to migrate: composite_alpha for every composite, the compression constants renumbered, minipsd_read giving transparency as the file has it. The manual, the spec, the header and INSTALL describe layers and composites without transparency, a layer's own compression, channels encoded by the program (the row functions), the writer's buffers, untrusted files, and the libvips plugin as it saves now and as released, with how a program that fetches libvips itself keeps it to one (FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS). cmake/minipsd_libvips.cmake fetches libvips as minipsd_libvips, its target's name: as minipsd_vips, a program's FetchContent of the plugin's zip under that natural name would have won, and the plugin got its own zip for libvips. ROADMAP: S7.5 done, S4.12 releases 0.4.0. --- .github/workflows/ci.yml | 2 +- README.md | 2 +- cmake/minipsd_libvips.cmake | 10 ++- docs/CHANGELOG.md | 50 ++++++++++++- docs/CHEATSHEET.md | 6 +- docs/CODE-ARCH.md | 17 ++++- docs/COMPATIBILITY.md | 2 +- docs/GITHUB-RELEASE.md | 20 +++-- docs/INSTALL.md | 89 ++++++++++++++++------ docs/MANUAL.md | 122 ++++++++++++++++++++++++------- docs/ROADMAP.md | 39 ++++++++-- docs/SPECIFICATION.md | 116 ++++++++++++++++++++--------- docs/USE-CASES.md | 4 +- include/minipsd.h | 26 ++++--- libvips-plugin/release/README.md | 28 ++++++- tests/test_writer.cpp | 2 +- 16 files changed, 408 insertions(+), 127 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0982024..659147b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: cmake -E chdir "$env:RUNNER_TEMP" cmake -E tar xf $zip.FullName $deps = "$env:GITHUB_WORKSPACE/build/${{ matrix.preset }}/_deps" cmake -S (Join-Path $env:RUNNER_TEMP $zip.BaseName) -B "$env:RUNNER_TEMP/plugin" ` - "-DFETCHCONTENT_SOURCE_DIR_MINIPSD_VIPS=$deps/minipsd_vips-src" ` + "-DFETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS=$deps/minipsd_libvips-src" ` "-DFETCHCONTENT_SOURCE_DIR_MINIPSD_ZLIB_HEADERS=$deps/minipsd_zlib_headers-src" cmake --build "$env:RUNNER_TEMP/plugin" ctest --test-dir "$env:RUNNER_TEMP/plugin" -C Debug --output-on-failure diff --git a/README.md b/README.md index b53b5c8..2f3674f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Procreate open as layered documents. # Status -**release: 0.3.2** +**release: 0.4.0** The API is [include/minipsd.h](include/minipsd.h); the implementation is in [include/minipsd/](include/minipsd/). diff --git a/cmake/minipsd_libvips.cmake b/cmake/minipsd_libvips.cmake index ba774f1..57aff1d 100644 --- a/cmake/minipsd_libvips.cmake +++ b/cmake/minipsd_libvips.cmake @@ -13,7 +13,9 @@ # MINIPSD_LIBVIPS_ENV the environment its tests run in (Windows: the DLLs on PATH), or "" # # A program with libvips of its own defines both targets before adding minipsd, visible to it (an -# INTERFACE library, or IMPORTED GLOBAL); nothing is then fetched or found here. +# INTERFACE library, or IMPORTED GLOBAL); nothing is then fetched or found here. A program that +# fetched libvips' Windows build itself sets FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS to where it is +# unpacked: that one is used, and only zlib's headers are fetched. # --------------------------------------------------------------------------- if(TARGET minipsd_libvips AND TARGET minipsd_libvips_zlib) @@ -24,13 +26,13 @@ endif() if(WIN32) set(MINIPSD_VIPS_VERSION 8.18.7) include(FetchContent) - FetchContent_Declare(minipsd_vips + FetchContent_Declare(minipsd_libvips # named as its target: a program may call the plugin minipsd_vips URL https://github.com/libvips/build-win64-mxe/releases/download/v${MINIPSD_VIPS_VERSION}/vips-dev-x64-web-${MINIPSD_VIPS_VERSION}.zip URL_HASH SHA256=3a122eb3d588690008216f786338e2f9329dba3c3c2768b5933cf7299d549241 DOWNLOAD_EXTRACT_TIMESTAMP TRUE SOURCE_SUBDIR no-cmake-project) # files only: nothing to add as a subproject - FetchContent_MakeAvailable(minipsd_vips) - set(_vips "${minipsd_vips_SOURCE_DIR}") + FetchContent_MakeAvailable(minipsd_libvips) + set(_vips "${minipsd_libvips_SOURCE_DIR}") add_library(minipsd_libvips INTERFACE IMPORTED) if(MSVC) # the zip's import libraries target_link_libraries(minipsd_libvips INTERFACE diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b41951f..0849152 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,18 +3,62 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 rules in [TODO.md](TODO.md): breaking changes bump the MINOR, additions and fixes the PATCH. -## [0.3.2] — 2026-10-25 +## [0.4.0] — 2026-10-25 + +**Breaking:** `minipsd_doc.composite_alpha` now says, for every composite (given, composed from +the layers, or stored), whether it has transparency; before, a given or composed composite always +had it. 0, the default, is a composite of the colour channels only, a composed one matted against +white. To migrate, a document whose composite has transparency (most layered ones, and every +`composite` given as RGBA) sets `composite_alpha = 1`; nothing else changes. + +**Breaking:** the compression constants move up one, so that zero means the default everywhere: +`MINIPSD_COMPRESSION_DEFAULT` 0 (a document's RLE, a layer's the document's), `MINIPSD_RLE` 1, +`MINIPSD_RAW` 2, `MINIPSD_ZIP` 3, `MINIPSD_ZIP_PREDICTED` 4. Code that names them compiles as it +was; numbers written out (a binding, a stored setting) move. They are not the markers a file +stores (0 raw, 1 RLE, 2 ZIP, 3 ZIP with prediction). + +**Breaking:** `minipsd_read` gives transparency as the file has it: a layer without a +transparency channel has `no_alpha` set and its pixels are the colour channels only, and the +composite has the channels the file stores, `composite_alpha` saying whether one is transparency +(before, both came as RGBA, transparency made opaque). A file without layers gets its +"Background" layer the same way. To migrate, read a layer's pixels by `no_alpha`, the +composite's by `composite_alpha`. ### Added +- Images without transparency: `minipsd_layer.no_alpha`, a layer whose pixels are its colour + channels only, written without a transparency channel and composed as opaque; with + `composite_alpha` 0, a document without layers is a flat file, as Photoshop saves an image that + has neither layers nor transparency. +- `minipsd_layer.compression`: a layer's channels, its mask's too, in a compression of their own + (0: the document's); the deflate is asked for whenever any layer deflates. +- Channels encoded by the program, row by row, for `stored` and `composite_stored`: + `minipsd_encode_row` (PackBits or raw, as the writer encodes a row), `minipsd_predict_row` (the + row `MINIPSD_ZIP_PREDICTED` deflates), `minipsd_matte_row` (a composite's row matted as the + writer mattes it), and `MINIPSD_ROW_BYTES`, `MINIPSD_ROW_BOUND` for their buffers. - The libvips plugin (`libvips-plugin/`, `MINIPSD_VIPS_PLUGIN`, the static library `minipsd::vips`): minipsd as libvips' loader and saver of PSD and PSB, registered with `minipsd_vips_init()` after `VIPS_INIT`. `psdload` gives the composite or, with `layer`, one layer placed by its offset, decoded in bands of rows from libvips' threads; grey, RGB, CMYK, bitmap, indexed (through its palette), Lab and multichannel at every depth, with the - resolution, ICC profile, EXIF, XMP, IPTC and `psd-*` fields. `psdsave` writes an image as one - layer with its composite, with `psb`, `zip` and `layer_name`. + resolution, ICC profile, EXIF, XMP, IPTC and `psd-*` fields. `psdsave` writes an image with + alpha as one layer and its composite, one without as a flat file (with `layered`, a layer + without transparency too), reading the image once, in bands, each channel encoded as its rows + come: memory about the file's size. Options: `compression` (`rle`, `zip`, `zip-predicted`, + `none`), `psb`, `layer_name`, `layered`; a PSB by itself when a PSD cannot hold the file. +- The plugin released as `minipsd-vips-X.Y.Z.zip`: a CMake project that builds as it is, with + minipsd's single header private to it and an example that is its test. ### Fixed +- A crafted file could make `minipsd_read` and the decode calls write past their buffers: a + mask's rectangle was not bounded, so the size of its plane could wrap around 64 bits. A + channel with a side above 300 000 is now `MINIPSD_ERR_UNSUPPORTED`, and `minipsd_read` bounds a + mask's sides as a layer's. +- An RLE row that packs to more than 65 535 bytes (32-bit samples that do not pack, wider than + about 16 380 px) went into a PSD with a wrong row count, and no error: now + `MINIPSD_ERR_SIZE`, the layer's or the composite's. A PSB takes it. +- `minipsd_write` encoded a long row again for each 4 KiB it was cut into: its buffer now holds + the widest row, so each row is cut at most once (30 000 x 400 RGBA noise: 1406 ms to 796 ms, + the same bytes). - A composite's transparency is now named where the layer count cannot say it (a 32-bit document, whose layers go in `Lr32` with a positive count, and a document with no layers): 1006, 1045 and 1053 "Transparency", as Photoshop writes them, unless the document keeps names of its diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 51e9c26..4c99e98 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -114,9 +114,9 @@ cpack --config build/package/CPackConfig.cmake -G DEB Check and install the `.deb`: ```bash -dpkg-deb -I dist/libminipsd-dev_0.3.2_all.deb # metadata -dpkg-deb -c dist/libminipsd-dev_0.3.2_all.deb # files -sudo apt install ./dist/libminipsd-dev_0.3.2_all.deb +dpkg-deb -I dist/libminipsd-dev_0.4.0_all.deb # metadata +dpkg-deb -c dist/libminipsd-dev_0.4.0_all.deb # files +sudo apt install ./dist/libminipsd-dev_0.4.0_all.deb sudo apt remove libminipsd-dev ``` diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index 7b53ff0..140558b 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -223,9 +223,11 @@ int minipsd_writer_fill (minipsd_writer *w, void *buf, size_t cap, size_t * - **No staging buffer.** `fill` produces the current piece again from its start, through a sink that copies only the bytes not yet handed out and only as many as fit. A piece is one row at most (the ICC profile is copied, not re-encoded), so a call that stops inside a row costs one - more encoding of that row: nothing with a buffer of a few rows, quadratic in the row only for - tiny caps. The composite row being produced is blended once and kept. A staging buffer would - remove that cost at the price of workspace; it is left out until a caller needs tiny caps. + more encoding of that row: nothing with a buffer of at least the widest row's + `MINIPSD_ROW_BOUND`, which `minipsd_write` allocates; quadratic in the row for smaller caps + (4 KiB cut a 32-bit PSB row of 300 000 px about 290 times). The composite row being produced + is blended once and kept. A staging buffer would remove that cost at the price of workspace; + it is left out until a caller needs tiny caps. - Any `cap >= 1` works, so a caller with a 1-byte buffer still gets the whole file. - A non-blocking caller writes as much of `buf` as the target accepts and keeps the rest for the next round. The library never needs to know about partial writes. @@ -548,6 +550,12 @@ int composite_matted; /* non-zero: composite is already matted, wri layers, and the writer mattes it. Only `composite_matted` skips it: `minipsd_read` sets it, because un-matting and matting again drift where alpha is low (8-bit matting merges nearby colours), and a file's own composite must come back pixel for pixel. +- **Transparency is the document's to say** (`composite_alpha`, 0.4.0): every composite, + supplied, composed or stored, has transparency only when it is set; without it the composite + is the colour channels alone (composed: matted against white), as Photoshop writes an image + without layers or transparency. A layer's own is `no_alpha`. Before 0.4.0 a supplied or + composed composite always had transparency, which a program saving an opaque image could not + avoid. - **Workspace.** A supplied composite is read row by row like a layer; the composed one keeps its one row in the workspace, as now. - **Kept (`composite_stored`, when `composite == NULL`): a file's own, copied.** The composite as @@ -652,7 +660,8 @@ typedef struct minipsd_read_opts { - **ZIP written** (`compression` `MINIPSD_ZIP`, `_PREDICTED`, with `minipsd_write_opts.deflate`): a channel's size is in its record before its data, and the writer never seeks back, so `mpsd__measure_layers` deflates each channel once and the `MPSD__ZIP` piece once more, as RLE - rows are packed twice. The workspace holds the largest channel's raw rows (gathered big-endian, + rows are packed for their size before they are written (three times a layer row: its size + while measuring, then its count and the row; the composite's sizes are kept, so twice). The workspace holds the largest channel's raw rows (gathered big-endian, predicted in place) and its stream; the stream is kept while a piece of it is handed out in parts, and a second stream of another size is `MINIPSD_ERR_WRITE`. The composite stays RLE. - **Limits:** `minipsd_limits` as in the reader; `max_decoded_bytes` bounds the sum of the call's diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 4b7be6b..be88ca0 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -8,7 +8,7 @@ output is covered by the golden test and the `psd-tools` round trip (see the spe [Verification](SPECIFICATION.md#verification)); this page is about everyone else's files. This page is generated by [tools/compat_report.py](../tools/compat_report.py) with minipsd -0.3.2, over 315 files of psd-tools v1.25.0. +0.4.0, over 315 files of psd-tools v1.25.0. ## Credits diff --git a/docs/GITHUB-RELEASE.md b/docs/GITHUB-RELEASE.md index 222b3eb..18192f9 100644 --- a/docs/GITHUB-RELEASE.md +++ b/docs/GITHUB-RELEASE.md @@ -12,12 +12,16 @@ Pushing the tag starts the [`Release`](../.github/workflows/release.yml) workflo 2. a check that the tag equals the `MINIPSD_VERSION_*` macros in `include/minipsd.h` and that `docs/CHANGELOG.md` has a section for that version; 3. `minipsd.h` as a single file: `include/` joined by `tools/amalgamate.cmake`; -4. a build-provenance attestation for that file; -5. a GitHub Release with `minipsd.h` and `minipsd.h.sha256`, its notes taken from the changelog - section. +4. the libvips plugin as `minipsd-vips-X.Y.Z.zip`, a CMake project that builds as it is + (`tools/package_vips.cmake`), then built from the zip alone against Ubuntu's libvips and its + example run: a zip that does not build is not published; +5. a build-provenance attestation for `minipsd.h` and the zip; +6. a GitHub Release with `minipsd.h`, the zip and a `.sha256` for each, its notes taken from the + changelog section. -There is nothing to build for consumers: the source tree at the tag *is* the release. The two -assets are for users who prefer one file to copying `include/`. +There is nothing to build for consumers: the source tree at the tag *is* the release. `minipsd.h` +is for users who prefer one file to copying `include/`; the zip, for those who want the plugin +without the repository. CI packs and builds the zip on every push too, so it fails there first. ## 0. Pre-flight @@ -47,8 +51,10 @@ notes come from `docs/CHANGELOG.md`, not from the tag. ## 3. Verify - **Actions** → the Release run is green. -- **Releases** → `0.1.0` has `minipsd.h`, `minipsd.h.sha256` and the changelog notes. -- Provenance: `gh attestation verify minipsd.h --repo ShadobaDev/minipsd`. +- **Releases** → `0.1.0` has `minipsd.h`, `minipsd-vips-0.1.0.zip`, a `.sha256` for each, and + the changelog notes. +- Provenance: `gh attestation verify minipsd.h --repo ShadobaDev/minipsd`, and the same for the + zip. - A FetchContent consumer pinned to `GIT_TAG 0.1.0` configures and builds. ## 4. Then the consumers diff --git a/docs/INSTALL.md b/docs/INSTALL.md index ea1768f..6340822 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -13,7 +13,7 @@ attaches the same headers joined into one `minipsd.h`, for those who prefer a si include(FetchContent) FetchContent_Declare(minipsd GIT_REPOSITORY https://github.com/ShadobaDev/minipsd.git - GIT_TAG 0.3.2) + GIT_TAG 0.4.0) FetchContent_MakeAvailable(minipsd) target_link_libraries(my-app PRIVATE minipsd::minipsd) ``` @@ -21,11 +21,11 @@ target_link_libraries(my-app PRIVATE minipsd::minipsd) **[CPM.cmake](https://github.com/cpm-cmake/CPM.cmake)** ```cmake -CPMAddPackage("gh:ShadobaDev/minipsd#0.3.2") +CPMAddPackage("gh:ShadobaDev/minipsd#0.4.0") target_link_libraries(my-app PRIVATE minipsd::minipsd) ``` -Use `#0.3.2`, not `@0.3.2`: the `@` form looks for a tag `v0.3.2`, and minipsd tags have no `v`. +Use `#0.4.0`, not `@0.4.0`: the `@` form looks for a tag `v0.4.0`, and minipsd tags have no `v`. **git submodule** @@ -39,22 +39,8 @@ target_link_libraries(my-app PRIVATE minipsd::minipsd) As a subproject, minipsd builds no tests, fetches nothing and enables no language. -**The libvips plugin.** In any of the three ways above, turn on `MINIPSD_VIPS_PLUGIN` before -adding minipsd and link `minipsd::vips` (then call `minipsd_vips_init()` after `VIPS_INIT`, see -the manual's [PSD in libvips](MANUAL.md#psd-in-libvips-the-plugin)). It builds from source, as -part of your program, against your libvips' headers, since its classes extend libvips' own: - -```cmake -set(MINIPSD_VIPS_PLUGIN ON) -FetchContent_MakeAvailable(minipsd) -target_link_libraries(my-app PRIVATE minipsd::vips) -``` - -It enables C, and finds libvips with pkg-config, or on Windows fetches libvips' prebuilt build -(pinned in [cmake/minipsd_libvips.cmake](../cmake/minipsd_libvips.cmake)). A program with -libvips of its own defines two targets before adding minipsd, and nothing is fetched or found: -`minipsd_libvips` (libvips, glib, gobject) and `minipsd_libvips_zlib` (the zlib that libvips -links, which inflates ZIP layers), each an `INTERFACE` library or `IMPORTED GLOBAL`. +The libvips plugin, minipsd as libvips' loader and saver, comes its own way: +[The libvips plugin](#the-libvips-plugin) below. **Installed package.** minipsd installs as a tree with nothing platform-specific in it: @@ -76,11 +62,11 @@ You can get that tree in any of these ways: - **As an archive**, `minipsd-X.Y.Z.tar.gz` or `.zip`: use it unpacked where it is, or extract it into a prefix. The tree is relocatable, so nothing needs regenerating: ```bash - sudo tar -xzf minipsd-0.3.2.tar.gz --strip-components=1 -C /usr/local + sudo tar -xzf minipsd-0.4.0.tar.gz --strip-components=1 -C /usr/local ``` - **As a Debian/Ubuntu package**, `libminipsd-dev_X.Y.Z_all.deb`: ```bash - sudo apt install ./libminipsd-dev_0.3.2_all.deb + sudo apt install ./libminipsd-dev_0.4.0_all.deb ``` The archive and the `.deb` are built with CPack, see @@ -101,3 +87,64 @@ own SBOM: ```cmake get_target_property(minipsd_sbom minipsd::minipsd MINIPSD_SBOM_FILE) ``` + +## The libvips plugin + +The plugin makes minipsd libvips' loader and saver of PSD and PSB (the manual's +[PSD in libvips](MANUAL.md#psd-in-libvips-the-plugin)). It is built from source, as part of your +program, against your libvips' headers: its classes extend libvips' own, so a compiled one would +hold for one libvips version only. Link `minipsd::vips`, then call `minipsd_vips_init()` after +`VIPS_INIT`. + +**From the release.** Each GitHub Release has `minipsd-vips-X.Y.Z.zip` (and its `.sha256`): the +plugin as a CMake project of its own, with minipsd's single header inside it, private to the +plugin, so a program's own minipsd, of any version, is left alone: + +```cmake +include(FetchContent) +FetchContent_Declare(minipsd_vips + URL https://github.com/ShadobaDev/minipsd/releases/download/0.4.0/minipsd-vips-0.4.0.zip + URL_HASH SHA256=) +FetchContent_MakeAvailable(minipsd_vips) +target_link_libraries(my-app PRIVATE minipsd::vips) +``` + +`add_subdirectory` of the unpacked zip does the same. On its own, the zip builds and tests its +example (`cmake -S`, `cmake --build`, `ctest`); without CMake, its README says what to compile. + +**From minipsd's source.** In any of the ways at the top, turn on `MINIPSD_VIPS_PLUGIN` before +adding minipsd: + +```cmake +set(MINIPSD_VIPS_PLUGIN ON) +FetchContent_MakeAvailable(minipsd) +target_link_libraries(my-app PRIVATE minipsd::vips) +``` + +**Where libvips comes from.** Either way the plugin enables C and, unless told otherwise, finds +libvips itself ([cmake/minipsd_libvips.cmake](../cmake/minipsd_libvips.cmake)): + +- **Linux, macOS:** pkg-config, the system's libvips and zlib. Nothing is fetched. +- **Windows:** libvips' official prebuilt build, `vips-dev-x64-web`, pinned, and zlib's headers + (zlib 1.3.1, pinned, about 1.5 MB), which that build lacks; the plugin links the zlib the + build carries, `libz1`, for ZIP channels. + +A program that fetches libvips' Windows build itself points the plugin at it, before adding the +plugin. Then one libvips is fetched, and the plugin is compiled against the headers of the DLLs +the program ships: + +```cmake +# after the program's own FetchContent_MakeAvailable(my_vips) of vips-dev-x64-*.zip +FetchContent_GetProperties(my_vips SOURCE_DIR my_vips_dir) +set(FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS "${my_vips_dir}") # the plugin takes this libvips +FetchContent_MakeAvailable(minipsd_vips) # or minipsd, with MINIPSD_VIPS_PLUGIN +``` + +The directory is libvips' Windows build unpacked as libvips releases it: the plugin takes +`include/`, `lib/` (the import libraries MSVC links) and `bin/` (the DLLs MinGW links) from it. +zlib's headers are still fetched: `zlib.h` is always there, whichever libvips is used. + +A program whose libvips comes from anywhere else defines two targets before adding the plugin, +and nothing is fetched or found: `minipsd_libvips` (libvips, glib, gobject) and +`minipsd_libvips_zlib` (zlib's headers and the zlib that libvips links), each an `INTERFACE` +library or `IMPORTED GLOBAL`. diff --git a/docs/MANUAL.md b/docs/MANUAL.md index b5ddd88..8d154ed 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -8,7 +8,7 @@ complete programs here are in [examples/](../examples/), which CI builds and run | Chapter | Covers | |---|---| | [Getting started](#getting-started) | adding minipsd, a PSD written and read in a page of code | -| [Writing](#writing) | the order of calls; to memory, to a blocking stream, to a non-blocking one | +| [Writing](#writing) | the order of calls; to memory, to a blocking stream, to a non-blocking one; channels encoded by you | | [Reading](#reading) | the order of calls; a file read in parts; pixels; the composite; untrusted files | | [Round trip](#round-trip) | a file read into a document, changed, written back; what is kept, what is lost | | [Memory and ownership](#memory-and-ownership) | what you allocate, how long it must live | @@ -49,7 +49,8 @@ int main(void) static uint8_t rgba[64 * 64 * 4]; /* straight alpha, rows top down */ /* Fields left out are zero, the defaults: visible, the full canvas, RLE. */ minipsd_layer layer = {.name = "Hello", .pixels = rgba, .opacity = 255}; - minipsd_doc doc = {.width = 64, .height = 64, .dpi = 72.0, .layers = &layer, .layer_count = 1}; + minipsd_doc doc = {.width = 64, .height = 64, .dpi = 72.0, .layers = &layer, .layer_count = 1, + .composite_alpha = 1}; /* composed from the layer, with its transparency */ FILE *f; int i, rc; @@ -140,8 +141,8 @@ In the diagrams, solid is what you write or call; dashed is what minipsd does. **One call, `minipsd_write`.** You write a callback and call `minipsd_write`; minipsd calls the callback with each piece of the file, in order, and returns when the whole file has gone -through it. A piece is at most 4 KiB, the buffer `minipsd_write` keeps on its stack; it has -nothing to do with the target. A callback that returns 0 stops the write with +through it. A piece is at most the buffer `minipsd_write` allocates with its workspace: 4 KiB, +or the widest row's `MINIPSD_ROW_BOUND` when that is more; it has nothing to do with the target. A callback that returns 0 stops the write with `MINIPSD_ERR_WRITE`. ```mermaid @@ -180,11 +181,21 @@ minipsd_layer layers[2] = { { .name = "Template", .pixels = rgba, .opacity = 255 }, /* width * height * 4, straight alpha */ { .name = "Layer", .opacity = 255 }, /* an empty layer above it */ }; -minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = layers, .layer_count = 2 }; +minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = layers, .layer_count = 2, + .composite_alpha = 1 }; /* the composite with the layers' transparency */ ``` Name the fields. The structs only grow at the end, and every field left out is zero, which is -the default: visible, RLE, and a layer that covers the full canvas. A layer of another size sets +the default: visible, RLE, and a layer that covers the full canvas. + +`composite_alpha` says whether the composite has transparency, whichever composite it is: given, +composed from the layers, or stored. With it, as most layered documents want, the composite keeps +the layers' transparency. Without it (0, the default), the composite holds the mode's colour +channels only and a composed one is matted against white, as an image without transparency is. A +layer whose pixels have no transparency sets `no_alpha`: its `pixels` are then the colour channels +alone, and it is written without a transparency channel. A document without layers, its +`composite` given and `composite_alpha` 0, is a flat file, the way Photoshop saves an image that +has neither layers nor transparency. A layer of another size sets its rectangle on the canvas, which may reach past the edges; `pixels` then holds that rectangle: ```c @@ -215,7 +226,7 @@ a `uint16_t` in host order, 0 to 65535; the pointers keep their type, so cast: uint16_t *deep = malloc((size_t)width * height * 4 * sizeof *deep); /* straight alpha */ minipsd_layer layer = { .name = "Deep", .pixels = (const uint8_t *)deep, .opacity = 255 }; minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = &layer, .layer_count = 1, - .depth = 16 }; + .depth = 16, .composite_alpha = 1 }; ``` Another colour mode sets `color_mode` (`MINIPSD_MODE_*`). A pixel is then the mode's colour @@ -227,7 +238,7 @@ uint8_t *cmyka = malloc((size_t)width * height * 5); /* C, M, Y, K, trans uint8_t *flat = malloc((size_t)width * height * 5); /* the same, rendered by you */ minipsd_layer ink = { .name = "Ink", .pixels = cmyka, .opacity = 255 }; minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = &ink, .layer_count = 1, - .composite = flat, .color_mode = MINIPSD_MODE_CMYK }; + .composite = flat, .composite_alpha = 1, .color_mode = MINIPSD_MODE_CMYK }; ``` RGB and grey also come at 32 bits (`depth` 32), every sample a `float` from 0.0 to 1.0, again with @@ -267,8 +278,9 @@ rc = minipsd_write(doc, &as_psb, to_file, f); /* a PSD read from disk, written Going the other way, a PSB that a PSD cannot hold is `MINIPSD_ERR_SIZE`, and the few blocks whose translation is not verified yet are `MINIPSD_ERR_UNSUPPORTED`, both before a byte is written. -Channels are PackBits RLE by default, or raw. ZIP, as Photoshop writes its 16-bit layers, needs a -deflate of yours, zlib's here, since minipsd implements none; the composite stays RLE: +Channels are PackBits RLE by default (`MINIPSD_RLE`, or 0), or raw (`MINIPSD_RAW`). ZIP, as +Photoshop writes its 16-bit layers, needs a deflate of yours, zlib's here, since minipsd +implements none; the composite stays RLE (raw in a raw document): ```c static int deflate_zlib(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t *dst_size) @@ -286,7 +298,12 @@ rc = minipsd_write(&doc, &zip, to_file, f); ``` Each channel is deflated twice, once to size it and once to write it, so the same input must give -the same bytes, as zlib's do. +the same bytes, as zlib's do. A layer may have a compression of its own, `compression` (0: the +document's); one that deflates asks for the deflate as the document's would: + +```c +logo.compression = MINIPSD_RAW; /* this layer raw, the others as doc.compression */ +``` The composite, the flattened image that viewers and thumbnails show, is yours to render when the layers use anything beyond plain "over" (libvips does most of it, see @@ -294,7 +311,8 @@ the layers use anything beyond plain "over" (libvips does most of it, see full canvas; the writer mattes it as the file stores it: ```c -doc.composite = flattened; /* width * height * 4, straight alpha; NULL: composed, a best effort */ +doc.composite = flattened; /* width * height * 4, straight alpha; NULL: composed, a best effort */ +doc.composite_alpha = 1; /* 0: width * height * 3, the colour channels only */ ``` Without one, the writer composes the visible layers with "over" at their opacity: enough for a @@ -329,8 +347,8 @@ is out: the writer sized everything from them at the start. ### To a blocking stream -`minipsd_write` drives the pull writer to the end and hands each piece, up to 4 KiB, to the -callback; a callback that returns 0 stops it with `MINIPSD_ERR_WRITE`. A file is +`minipsd_write` drives the pull writer to the end and hands each piece, up to 4 KiB or the +widest row, to the callback; a callback that returns 0 stops it with `MINIPSD_ERR_WRITE`. A file is [Write a PSD](#write-a-psd) above. A blocking descriptor, such as a pipe or a socket, may take less than it is given, so its callback loops: @@ -412,9 +430,43 @@ else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { /* the target faile ``` `minipsd_writer_total` gives the size up front, for a `Content-Length` header or a progress bar. -The whole program, an `O_NONBLOCK` pipe driven by `poll` with its reading end in the same loop: +A call that stops inside a row encodes it again from its start in the next call, so a buffer of +at least `MINIPSD_ROW_BOUND(width, depth)` for the widest row cuts each row at most once. The +whole program, an `O_NONBLOCK` pipe driven by `poll` with its reading end in the same loop: [examples/write_nonblocking.c](../examples/write_nonblocking.c). +### Channels encoded by you + +`minipsd_write` takes the pixels whole and encodes each row twice, once to size it and once to +write it. A program that reads its image once, top to bottom (a decoder, a libvips pipeline), or +that would rather hold it compressed, encodes each channel itself, row by row as the rows come, +and gives minipsd the channel as the file stores it: `layer.stored` for a layer's channels, +`doc.composite_stored` for the composite. minipsd copies those as they are. Three calls do to a +row what the writer does: + +| Call | What it makes of a row | +|---|---| +| `minipsd_encode_row` | PackBits (`MINIPSD_RLE`), or raw (`MINIPSD_RAW`), big-endian | +| `minipsd_predict_row` | the row that `MINIPSD_ZIP_PREDICTED` deflates; plain ZIP deflates the raw rows | +| `minipsd_matte_row` | a composite's row matted against white, as the writer mattes a given composite (RGB) | + +```c +uint8_t row[MINIPSD_ROW_BOUND(WIDTH, 8)]; /* the most one row encodes to */ +size_t n; +for (y = 0; y < height; ++y) { /* red, out of RGBA rows as they come */ + minipsd_encode_row(MINIPSD_RLE, 8, rgba + (size_t)y * WIDTH * 4, 4, WIDTH, row, sizeof row, &n); + /* keep row[0 .. n) and n: the row, and its size for the table */ +} +``` + +A stored channel is its compression marker (two bytes, big-endian: 0 raw, 1 RLE, 2 ZIP, 3 ZIP +with prediction: the file's numbers, not the `MINIPSD_*` constants), then, for RLE, every row's +size (two bytes in a PSD, four in a PSB), then the rows; for ZIP, the zlib stream of every row. +The composite's is one block: one marker, every channel's row sizes, then every channel's rows. A +PSD's row sizes are 16-bit, so a row longer than 65 535 bytes needs a PSB. The libvips plugin's +`psdsave` saves this way, one top-to-bottom pass in bands: +[libvips-plugin/minipsd_vips_psdsave.c](../libvips-plugin/minipsd_vips_psdsave.c). + ## Reading The reader describes a file and decodes its pixels, layers and composite. It works on bytes you @@ -655,6 +707,12 @@ malformed file: The byte limit is per call, so a layer larger than it can still be decoded in bands of rows. +`minipsd_read` takes the same limits, its byte limit over the whole document. Without them it +allocates what the file asks for, and a small file can ask for a lot: PackBits packs up to 64 to +1, deflate about a thousand to 1. For a file you do not trust, set `max_decoded_bytes`, or call +`minipsd_read_size` first, compare its answer with your budget, then `minipsd_read_into` into +memory of yours. + ## Round trip `minipsd_read` is the mirror of `minipsd_write`: a whole file in, a `minipsd_doc` out, which @@ -673,7 +731,10 @@ minipsd_doc_free(doc); Every colour mode and depth the writer takes comes through, with its colour mode data (a palette, a duotone's inks); a file it could not take back, such as 32-bit CMYK, is -`MINIPSD_ERR_UNSUPPORTED`. +`MINIPSD_ERR_UNSUPPORTED`. Transparency comes as the file has it: a layer without a transparency +channel has `no_alpha` set and its pixels are the colour channels only, and the composite has the +channels the file stores, `composite_alpha` saying whether one of them is transparency, so +writing the document back keeps both as they were. The document lives in one block: from `MINIPSD_MALLOC`, freed by `minipsd_doc_free`, or in memory of yours with `minipsd_read_size` and `minipsd_read_into`. Nothing in it points into the @@ -770,8 +831,8 @@ alpha, rows top down), so nothing is converted. (`vips_linear` on the alpha band), hidden layers and group markers left out, blend keys mapped by the table in [Working with libvips](SPECIFICATION.md#working-with-libvips). Modes without a counterpart, masks, clipping and effects are yours to draw. -3. **Written by minipsd.** The render goes into `doc.composite`, `composite_matted` 0: the writer - mattes it as the format wants. +3. **Written by minipsd.** The render goes into `doc.composite`, `composite_alpha` 1 (it has + transparency), `composite_matted` 0: the writer mattes it as the format wants. 4. **Read back** (the example's check): `minipsd_decode_composite` into a buffer that `vips_image_new_from_memory` wraps without a copy gives the render again, exactly where alpha is full and within the un-matting bound elsewhere. @@ -786,7 +847,8 @@ doc.composite = vips_image_write_to_memory(out, &size); /* width * height * 4, ``` libvips computes lazily, in strips; minipsd takes a whole layer and a whole composite, so each is -materialised before writing. Reading, bands of rows (`minipsd_decode_opts`) fit libvips' strips. +materialised before writing, unless you encode the channels yourself as the strips come +([Channels encoded by you](#channels-encoded-by-you)), as the plugin's `psdsave` does. Reading, bands of rows (`minipsd_decode_opts`) fit libvips' strips. Build the example with `-DMINIPSD_VIPS_EXAMPLE=ON`: on Windows CMake fetches libvips' prebuilt build, elsewhere pkg-config finds yours. minipsd itself never depends on libvips. @@ -815,16 +877,26 @@ vips_image_write_to_file(page, "copy.psd", "layer_name", "Page", NULL); `psd-*` fields such as `psd-layer-count` and a layer's name, blend mode and opacity (the list: the spec's [Working with libvips](SPECIFICATION.md#working-with-libvips)). Bands of rows are decoded from libvips' threads at once; with `access=sequential` memory stays at a few bands - of rows per thread, so it grows with the width and the threads, not the height. -- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): one layer that is also the - composite; grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats; alpha kept, or the layer - made opaque. Options: `psb` (also a file named `.psb`), `zip`, `layer_name`. The image is - held whole while it is written. + of rows per thread, so it grows with the width and the threads, not the height. A layer + without transparency comes without an alpha band. +- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): an image with alpha as one layer + and its composite. One without alpha as a flat file, the composite only and without + transparency, as Photoshop saves an image without layers; with `layered`, one layer without + transparency as well. Grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats. Options: + `compression` (`rle`, the default; `zip`; `zip-predicted`; `none`, the composite raw too), + `psb`, `layer_name`, `layered`. The image is read once, top to bottom, in libvips' bands, each + channel encoded as its rows come: memory about the file's size, not the image's. A PSB when + asked, for a file named `.psb`, or when a PSD cannot hold the image: a side above 30 000, a + row longer than a PSD's two-byte count, or a file above 2 GB, which Adobe documents as + Photoshop's limit for a PSD. - **Building:** `-DMINIPSD_VIPS_PLUGIN=ON` gives the static library `minipsd::vips`; link it. minipsd is compiled into it privately (`MINIPSD_STATIC`), so your own use of minipsd keeps its own. libvips comes as for the example, or from your project: define `minipsd_libvips` and `minipsd_libvips_zlib` (libvips, and the zlib it links, which inflates ZIP layers) before - adding minipsd. + adding minipsd, or point `FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS` at the libvips you fetched + ([INSTALL.md](INSTALL.md#the-libvips-plugin)). +- **Released** as `minipsd-vips-X.Y.Z.zip` with each GitHub Release: the plugin as a CMake project + of its own, minipsd inside it ([INSTALL.md](INSTALL.md#the-libvips-plugin)). ## Memory and ownership diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6e46526..1560580 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -38,7 +38,7 @@ The order of work from 0.1.0 to the design in [CODE-ARCH.md](CODE-ARCH.md). Case | [4. Reader](#4-reader) | buffer to structure, for minipsd's own subset | 0.3.0, at S4.10 | | [5. Round trip and conveniences](#5-round-trip-and-conveniences) | layer rectangles, full import, cheap re-save, kept unknown content | batched, when decided | | [6. Format features](#6-format-features) | blend modes, groups, masks, 16-bit, PSB | batched, when decided | -| [7. Interoperability](#7-interoperability) | libvips | 0.3.0 (S7.1), 0.3.2 (S7.2 to S7.4, at S4.12) | +| [7. Interoperability](#7-interoperability) | libvips | 0.3.0 (S7.1), 0.4.0 (S7.2 to S7.5, at S4.12) | | [8. Test quality](#8-test-quality) | branch coverage of compound conditions | with any release | | [Not scheduled](#not-scheduled) | large cases, only on real demand | — | @@ -195,10 +195,14 @@ Additive. See [Reader](CODE-ARCH.md#reader). workflow checks CI's run on the tagged commit instead of running it again. Before it: [COMPATIBILITY.md](COMPATIBILITY.md) regenerated with the version (done), the PR merged and CI green on `master`. -- [ ] **S4.12** Release **0.3.2** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): an addition and a - fix, so a PATCH. The libvips plugin (S7.2 to S7.4); the writer names a composite's - transparency where the layer count cannot. Before it: [COMPATIBILITY.md](COMPATIBILITY.md) - regenerated with the version (done), the PR merged and CI green on `master`. +- [ ] **S4.12** Release **0.4.0** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): breaking, so a + MINOR (`composite_alpha` for every composite, the compression constants renumbered, + `minipsd_read` giving transparency as the file has it). The libvips plugin (S7.2 to S7.5) + and its zip; layers without transparency, a layer's own compression, the row functions; + the fixes the pre-release review found (a mask's unbounded rectangle, an RLE count a PSD + cannot hold, `minipsd_write`'s cut rows). Prepared as 0.3.2, never tagged, then broken. + Before it: [COMPATIBILITY.md](COMPATIBILITY.md) regenerated with the version, the PR merged + and CI green on `master`; Platemaker sets `composite_alpha = 1` when it moves to 0.4.0. ## 5. Round trip and conveniences @@ -567,18 +571,33 @@ once Platemaker does. 950 empty layers); a 1024 x 30000 PSD read with `access=sequential` leaves libvips holding under 2 MB of its 60 MB (all of it when the loader is not sequential). A PSD, not a PSB: the same bands. - [x] **S7.4** Saver: an image as a one-layer PSD or PSB with its composite, the image whole in - memory; RLE, or ZIP through zlib. + memory; RLE, or ZIP through zlib. (S7.5 changed what it writes and how; see there.) *Done when:* psd-tools and GIMP open what it writes, and the loader reads it back the same. As done: `psdsave`, `_buffer`, `_target`; options `psb`, `zip`, `layer_name`. psd-tools opens every file the test writes, its composite the layer (but where alpha is partial, by the matte's rounding), full cyan as (255, 0, 0, 0); GIMP opens them, by hand. GIMP showed a 32-bit file's transparency as an extra channel: the writer now names it as Photoshop does where the layer count cannot (see the changelog). -- [ ] **S7.5** Row provider (`W6`): a layer's or the composite's rows asked from a callback instead +- [x] **S7.5** Row provider (`W6`): a layer's or the composite's rows asked from a callback instead of an `pixels` pointer, twice with RLE or ZIP (sizing, then writing), so it must be deterministic, as libvips pipelines are. The saver then streams. *Done when:* the saver writes an image larger than the memory it is given, the bytes the same as from memory. + As done: not a row provider. libvips hands a saver an image it may read once, top to + bottom, and a provider would be asked for every channel's rows twice, so the saver encodes + each channel once, as its rows come, and gives minipsd the channels as stored: the row + functions made public for it (`minipsd_encode_row`, `minipsd_predict_row`, + `minipsd_matte_row`), ZIP through a zlib stream a channel. Memory is about the file's size, + not the image's: 20000 x 20000 RGB, an 813 MB file, 1994 MB to 1215 MB, 2.1 s to 1.5 s; + not below the file, so a file larger than memory still fails (temporary files were + declined). Found on the way, and done with it: an image without alpha is written flat (no + layer, no transparency, as Photoshop does; `layered` keeps a layer without it), so + `composite_alpha` now governs every composite and a layer may be `no_alpha` (breaking); + `compression` (`rle`, `zip`, `zip-predicted`, `none`) in place of `zip`, and a layer's own + compression in minipsd (the constants renumbered, breaking); a PSB chosen by itself when a + PSD cannot hold the file; the plugin released as a zip. psdsave is now as fast as + ImageMagick's `magicksave` or faster, its files 4 to 6 times smaller, with the right + pixels where magicksave's are wrong (alpha) or it fails (20000 x 20000). ## 8. Test quality @@ -604,7 +623,11 @@ Large cases. Each would start with a *design* step, and only when a real consume - The composed composite learning blend modes, masks, clipping or groups: a supplied composite is the way to get them right. -- Layer-at-a-time producer (`W8`); the row provider (`W6`) is S7.5. +- Layer-at-a-time producer (`W8`); the row provider (`W6`), which S7.5 did without. +- A streaming deflate (init, feed, finish) beside `minipsd_deflate_fn`, so the writer holds no + ZIP channel whole; and the writer keeping each layer row's size, so RLE encodes a row twice, + not three times. Both only when a program writing with minipsd itself needs them: the plugin + encodes its channels itself. - Parallel encoding split API (`W9`). - Segment output for zero-copy saves (`E3`). - In-place patching of fixed-size fields. diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index ec81af0..51a8b22 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -111,6 +111,9 @@ The declarations, with every parameter and return code, are in | `minipsd_writer_total(w)` | the file size, known before the first byte | | `minipsd_writer_fill(w, buf, cap, &produced)` | the next bytes, into the caller's buffer: `MINIPSD_MORE` until `MINIPSD_OK` | | `minipsd_write(doc, opts, fn, user)` | the whole file through a callback, on a workspace from `MINIPSD_MALLOC`; absent under `MINIPSD_NO_MALLOC` | +| `minipsd_encode_row(compression, depth, samples, step, width, out, cap, &size)` | one row of one channel as the writer encodes it, PackBits or raw, into the caller's buffer (`MINIPSD_ROW_BOUND` always enough): with the marker and the row sizes, a channel for `stored` and `composite_stored` | +| `minipsd_predict_row(depth, samples, step, width, out, cap)` | one row as the writer hands it to the deflate for `MINIPSD_ZIP_PREDICTED` (`MINIPSD_ROW_BYTES`) | +| `minipsd_matte_row(color_mode, depth, pixels, width)` | a composite's row matted against white in place, as the writer mattes a given one (RGB; other modes as given) | | `minipsd_reader_workspace_size(&size)` | the reader's workspace: fixed, known before the file | | `minipsd_reader_init(&r, data, size, workspace, workspace_size, limits, &info)` | the file's description and its resources, from the whole file or a prefix of it | | `minipsd_reader_needed(r)` | after `MINIPSD_ERR_SHORT`: the least buffer size with which the call goes further | @@ -169,7 +172,7 @@ its layers and composite as stored: no colour is converted. | Type | What it is | |---|---| -| `minipsd_doc`, `minipsd_layer` | the document: canvas, dpi, layers bottom first, ICC profile, compression | +| `minipsd_doc`, `minipsd_layer` | the document: canvas, dpi, layers bottom first, ICC profile, compression (the document's, and a layer's own), whether the composite and each layer have transparency | | `minipsd_workspace` | `void`: caller memory lent to the writer or the reader, sized by its `*_workspace_size` function | | `minipsd_writer` | opaque; built by `minipsd_writer_init` inside the workspace | | `minipsd_write_fn` | the callback of `minipsd_write`: non-zero to go on, 0 to abort | @@ -184,7 +187,12 @@ its layers and composite as stored: no colour is converted. | `minipsd_decode_opts` | the rows to decode, the target's `depth` and, for the composite, `keep_matte`; `NULL` for all rows, 8-bit, un-matted | **Defaults and field meanings** -- A zero-initialised document selects RLE (`MINIPSD_RLE = 0`). +- A zero-initialised document selects RLE: `MINIPSD_COMPRESSION_DEFAULT` (0) is RLE for the + document and the document's compression for a layer. The other constants (`MINIPSD_RLE` 1, + `MINIPSD_RAW` 2, `MINIPSD_ZIP` 3, `MINIPSD_ZIP_PREDICTED` 4) are not the markers the file + stores (0 raw, 1 RLE, 2 ZIP, 3 ZIP with prediction). +- `composite_alpha = 0` is a composite without transparency, its colour channels only; + `no_alpha = 0` a layer with transparency. - `opacity = 0` means a fully transparent layer; it is **not** "default opaque". - `pixels = NULL` is an empty layer; `name = NULL` is an empty name. - Layers are listed bottom first. @@ -245,12 +253,13 @@ the caller frees with `minipsd_doc_free`. Everything else is the caller's memory | Name | Owner | Size | Must stay valid | May live in | |---|---|---|---|---| | `minipsd_doc`, `minipsd_layer[]` | caller; borrowed | `sizeof`; `layer_count` layers | the call; for the pull writer, from init for as long as the writer is used | stack, static, heap | -| raw document data: layer pixels (`pixels`) or stored channels (`stored`), names, ICC profile, the supplied `composite`, kept `additional_info`, `resources` and `layer_tail` | caller; borrowed, read in place, never copied | a layer's rectangle times 4 samples, of 1 or 2 bytes (or its channels' stored size); `width * height * 4` samples for the composite | as `minipsd_doc` | wherever the application keeps it | +| raw document data: layer pixels (`pixels`) or stored channels (`stored`), names, ICC profile, the supplied `composite`, kept `additional_info`, `resources` and `layer_tail` | caller; borrowed, read in place, never copied | a layer's rectangle times its pixel's samples, the mode's colour channels and transparency unless `no_alpha` (or its channels' stored size); `width * height` times the colour channels, and transparency with `composite_alpha`, for the composite | as `minipsd_doc` | wherever the application keeps it | | `minipsd_workspace` of the writer | caller; lent to the writer | `minipsd_writer_workspace_size()` | for as long as the writer is used | heap; stack or static sized for the largest document | | `minipsd_writer` | inside `minipsd_workspace`; opaque | part of the workspace | as the workspace | the caller keeps only the pointer | -| raw PSD bytes | passed to the callback of `minipsd_write()` | pieces of up to 4 KiB, in order | the callback copies what it keeps | the callback decides | -| output buffer, `buf` of `minipsd_writer_fill()` | caller | any `cap`, 1 included; a few rows (64 KiB) keeps re-encoding negligible | the call | stack, static or heap | -| `minipsd_write()` internals | minipsd: the workspace through `MINIPSD_MALLOC` / `MINIPSD_FREE`, a 4 KiB chunk on the stack | `minipsd_writer_workspace_size()` | the call | heap and stack | +| raw PSD bytes | passed to the callback of `minipsd_write()` | pieces of up to 4 KiB, or of the widest row's `MINIPSD_ROW_BOUND` when that is more, in order | the callback copies what it keeps | the callback decides | +| output buffer, `buf` of `minipsd_writer_fill()` | caller | any `cap`, 1 included; one of at least the widest row's `MINIPSD_ROW_BOUND` encodes each row at most twice | the call | stack, static or heap | +| `minipsd_write()` internals | minipsd: the workspace and its buffer through `MINIPSD_MALLOC` / `MINIPSD_FREE` | `minipsd_writer_workspace_size()`, and the larger of 4 KiB and the widest row's bound | the call | heap | +| `out` of `minipsd_encode_row()`, `minipsd_predict_row()`; `pixels` of `minipsd_matte_row()` | caller; written (`pixels` read and written in place) | `MINIPSD_ROW_BOUND`, `MINIPSD_ROW_BYTES`; `width` pixels | the call | stack, static or heap | | the PSD to read (`data` of `minipsd_reader_init()`) | caller; borrowed, read in place, never copied | the file, or a prefix of it | for as long as the reader or any view from it is used, unchanged | wherever it was read, mapped or received | | `minipsd_workspace` of the reader | caller; lent to the reader | `minipsd_reader_workspace_size()`, the same for every file | for as long as the reader is used | stack, static or heap | | `minipsd_reader` | inside its workspace; opaque | part of the workspace | as the workspace | the caller keeps only the pointer | @@ -289,6 +298,7 @@ interact, whatever the table says. | `minipsd_writer_init` | no | fills the writer's workspace; several writers of one document at once are fine | | `minipsd_writer_fill` | no | advances the writer | | `minipsd_writer_total` | yes | reads what `minipsd_writer_init` set; safe beside `minipsd_writer_fill` | +| `minipsd_encode_row`, `minipsd_predict_row`, `minipsd_matte_row` | yes | read the caller's samples; write only the caller's buffer (`minipsd_matte_row` its row) | | `minipsd_reader_init` | no | fills the reader's workspace; several readers of one PSD at once are fine | | `minipsd_reader_needed` | yes | reads the reader; after a decode returned `MINIPSD_ERR_SHORT`, call it in that thread | | `minipsd_reader_next_resource` | yes | each thread its own `pos`; only reads the reader | @@ -335,7 +345,8 @@ several threads from one reader into one image. bits, RGB and grey also at 32, indexed at 8, bitmap at 1. Another depth of a mode is `MINIPSD_ERR_UNSUPPORTED`; a mode or depth the format does not have, `MINIPSD_ERR_ARG`. - **Samples** are the mode's colour channels then transparency (`pixels`, `composite`), as the - file stores them: no colour is converted (CMYK inverted, 255 no ink; Lab's L, a, b). A sample + file stores them, transparency only where there is some (a layer's unless `no_alpha`, the + composite's with `composite_alpha`): no colour is converted (CMYK inverted, 255 no ink; Lab's L, a, b). A sample is a byte at 8 bits, a `uint16_t` at 16, a `float` (0.0 to 1.0) at 32, in host order, written big-endian; a mask is one channel of the same depth. - **The colour mode data** (`color_mode_data`) is written as given: an indexed document's palette @@ -358,7 +369,8 @@ several threads from one reader into one image. `opts->stored`; when the composite has transparency (not in a flat mode), an empty `Mtrn`, `Mt16` or `Mt32` by its depth after the tail, unless the tail has one (Adobe's "merged transparency"; no file in psd-tools' set is flat with transparency, since Photoshop keeps it on - a layer). + a layer). Without transparency (`composite_alpha` 0) it is the colour channels alone, the count + positive: how Photoshop saves an image that has neither layers nor transparency. - **The format is what the document is.** A PSB is written with its wide fields (eight-byte section and channel lengths, four-byte RLE row counts), and everything kept from a file is written as it is, so it must be in that format's layout: `minipsd_read` sets `format` to the @@ -393,9 +405,11 @@ several threads from one reader into one image. - the writer's own blocks first, `luni` (a kept one as it is while it says the name) and the group marker, then the blocks kept from a file (`additional_info`), as they are and in their order: the name wins, and kept `luni` blocks go; - - channel data encoded from `pixels`, or copied as it is from `stored` (any compression the - file had, ZIP included), so a layer that was not changed is not encoded again; a stored - layer without transparency is written without one (three colour channels); + - channel data encoded from `pixels` in the layer's compression (`compression`, 0 the + document's), or copied as it is from `stored` (any compression the file had, ZIP included), + so a layer that was not changed is not encoded again; a layer without transparency + (`no_alpha`, or stored with a transparency channel of size 0) is written without one, its + colour channels only; - groups (`section_divider_type`), bottom first: a divider (`lsct` type 3), the group's layers, then the group's top record (type 1 open, 2 closed) with its name, visibility and mode in its `lsct` block (`blend_mode`, Pass Through by default; the record keeps `norm`). A kept `lsct` of the same @@ -410,16 +424,20 @@ several threads from one reader into one image. writes a length of 0, as GIMP does; a file's are written back as they were (Photoshop writes five: the composite grey, then four channels). The composed composite ignores them. - **Composite image:** - - the caller's `composite` when set (the full canvas, straight alpha), whatever the layers - hold: flattening layers is rendering, the caller's domain; + - the caller's `composite` when set (the full canvas: the colour channels, then, with + `composite_alpha`, transparency, straight), whatever the layers hold: flattening layers is + rendering, the caller's domain; - otherwise a file's composite kept as it stores it (`composite_stored`, with `composite_channels`: up to 56, the mode's colour channels, then transparency when `composite_alpha` says so, then alpha and spot channels), copied byte for byte, never decoded nor encoded; the header's channel count follows it, and the layer count is negative with `composite_alpha`; - otherwise composed as a **best effort, with no guarantee**: the visible layers combined with - straight-alpha "over", each at its rectangle, clipped to the canvas. Blend modes, masks, - clipping, groups and effects are not drawn; - - transparency (of a supplied or composed composite, always, but in a flat mode) is the first extra channel, + straight-alpha "over", each at its rectangle, clipped to the canvas, a layer without + transparency as opaque. Blend modes, masks, clipping, groups and effects are not drawn. + Without `composite_alpha` the result is matted against white and its colour channels alone + are written; + - transparency, whichever the composite (supplied, composed or stored), only when + `composite_alpha` says it has some (never in a flat mode: `MINIPSD_ERR_ARG`), is the first extra channel, signalled by writing the layer count as a **negative** number, as the format specifies; where the count cannot say it (the writer's `Lr32`, whose count is positive, and a document with no layers), by the `Mt32` (`Mtrn`, `Mt16`) block and by naming the channel as @@ -470,19 +488,24 @@ several threads from one reader into one image. - **After the layer info:** the kept `layer_tail` (global layer mask, global blocks) as it is, or an empty global layer mask, also for a file read that ends the section with its layer info (GIMP's). -- **Compression:** PackBits RLE (runs from 3 bytes, packets up to 128 bytes) or raw. - Row byte counts are computed in a separate sizing pass, so output streams forward through the - callback and never seeks back. +- **Compression:** PackBits RLE (runs from 3 bytes, packets up to 128 bytes) or raw, the + document's (`compression`) or a layer's own (`minipsd_layer.compression`, its mask's channels + too). Row byte counts are computed in a separate sizing pass, so output streams forward through + the callback and never seeks back. A PSD counts an RLE row's bytes in 16 bits: a row that packs + to more than 65 535 bytes (32-bit samples that do not pack, wider than about 16 380 px) is + `MINIPSD_ERR_SIZE`, the layer's or the composite's; a PSB counts in 32. ZIP (`MINIPSD_ZIP`, and `MINIPSD_ZIP_PREDICTED` with each sample stored as its difference from its left neighbour) compresses the layers' and masks' channels through the caller's deflate, - `minipsd_write_opts.deflate` (`MINIPSD_ERR_ARG` without one): minipsd implements none. The - composite is then RLE, as Photoshop writes it. The sizing pass deflates each channel once to + `minipsd_write_opts.deflate` (`MINIPSD_ERR_ARG` without one, whenever any layer deflates): + minipsd implements none. The composite is RLE, as Photoshop writes it, and raw in a raw + document. The sizing pass deflates each channel once to learn its size and the writing pass once more, so the deflate must give the same bytes for the same input; one that fails, or gives other bytes, is `MINIPSD_ERR_WRITE`. The workspace holds the largest such channel raw and the room for its stream. - **Allocations:** see [Memory and ownership](#memory-and-ownership). - **Limits** (all checked before anything is written): - each side 1 .. 30 000 px in a PSD, 1 .. 300 000 in a PSB, otherwise `MINIPSD_ERR_SIZE`; + - an RLE row of at most 65 535 bytes in a PSD (Compression, above), otherwise `MINIPSD_ERR_SIZE`; - 1 .. 32 767 layers, otherwise `MINIPSD_ERR_ARG`; - dpi must round to 1 .. 2^32 - 1 in 16.16 fixed point, i.e. from about 0.0000076 to just below 65 536 (NaN and infinity excluded), otherwise `MINIPSD_ERR_ARG`; @@ -596,7 +619,11 @@ them; nothing is copied or allocated. Results on real files from other programs: (or, with `opts->stored`, its channels as stored, ZIP ones too) and blocks, the composite as stored (`composite_matted`; with `opts->stored`, every channel's bytes in `composite_stored`), the dpi (72 when none is stored), the ICC profile, every resource and the layer tail, the `Lr16` or `Lr32` its layers came from in it, empty, all copied into one - block. A file without layers gets one, "Background", holding its image; with `opts->stored` it + block. A layer without a transparency channel decodes to its colour channels, `no_alpha` set; + the composite to the channels the file has, `composite_alpha` saying whether one of them is + transparency. A mask whose side is above the format's is `MINIPSD_ERR_UNSUPPORTED`, as a + layer's. A file without layers gets one, "Background", holding its image, without transparency + when the file has none; with `opts->stored` it stays without, its image the composite, copied, and so does a flat mode's (bitmap, indexed, multichannel), which has no layers: decoded, but multichannel's always copied. Anything else is reported in `lost` (`MINIPSD_LOST_*`), never dropped silently. @@ -611,7 +638,8 @@ them; nothing is copied or allocated. Results on real files from other programs: `minipsd_reader_needed` says how much more a call needs. - **Limits:** every length is checked against the buffer and the section it lies in; the caller's `minipsd_limits` add the canvas's and the layers' side, the layer count and the bytes - one decode call writes, all checked before anything is decoded. + one decode call writes, all checked before anything is decoded. A channel whose side is above + either format's (300 000) is `MINIPSD_ERR_UNSUPPORTED` before its size is computed. ## Blend modes @@ -709,7 +737,7 @@ libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSo - **Loading** (`psdload`, `psdload_buffer`, `psdload_source`). The file is mapped (`vips_source_map`) and read in place. The image is the composite, or with `layer` (bottom first; -1, the default, the composite) that layer: its rectangle, placed by `xoffset` and - `yoffset`, always with transparency (opaque when the layer has none). An empty layer (a group's + `yoffset`, with transparency when the layer has it (no alpha band when it has none). An empty layer (a group's marker, an adjustment) is refused. Bands are the mode's colour channels and transparency, up to `MINIPSD_PLANES` (a multichannel file's sixth channel on is not decoded): @@ -741,17 +769,39 @@ libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSo | `psd-layer-count` | the file's layers | | `psd-layer-name`, `-blend-mode`, `-opacity`, `-hidden` | the layer, with `layer` | -- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`). One layer that is also the - composite: grey, RGB or CMYK (by `interpretation` and bands, each with or without alpha), 8 or - 16 bits, grey and RGB as floats; other band counts are refused. Without alpha the layer is - made opaque. CMYK goes back to PSD's inverted ink. Kept: the resolution, the ICC profile, EXIF, - XMP, IPTC and `psd-pixel-aspect-ratio`. Options: `psb` (PSB also for a file named `.psb`, read - from the target, since libvips writes files through `psdsave_target`, and for a side past - 30000), `zip` (the layer's channels; the composite stays RLE), `layer_name` ("Layer 1"). The - image is held whole while it is written; streaming waits for the row provider (S7.5). +- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`). Grey, RGB or CMYK (by + `interpretation` and bands, each with or without alpha), 8 or 16 bits, grey and RGB as floats; + other band counts are refused. With alpha: one layer and the composite, each with + transparency. Without: a flat file, the composite only, its colour channels (as Photoshop's + `history.psd`, `0layers_tblocks.psd` and `slices.psd` in psd-tools' set, and psd-tools' + `frompil`); with `layered`, one layer without transparency as well, which psdload reads back + without an alpha band. Nothing is added: an image's bands are what is written. CMYK goes back + to PSD's inverted ink. Kept: the resolution, the ICC profile, EXIF, XMP, IPTC and + `psd-pixel-aspect-ratio`. Options: `compression` (the layer's channels: `rle`, the default, + `zip`, `zip-predicted`, or `none`, which writes the composite raw too; otherwise the composite + is RLE; `MinipsdVipsCompression` in `minipsd_vips.h`), `psb`, `layer_name` ("Layer 1"), + `layered`. + + The image is read once, top to bottom, in the bands libvips hands a saver + (`vips_sink_disc`), and each channel's rows are encoded as they come with + `minipsd_encode_row`, `minipsd_predict_row` and `minipsd_matte_row`, ZIP through a zlib stream + a channel, then handed to minipsd as `stored` and `composite_stored`: memory about the file's + size, not the image's (an 8000 x 8000 RGBA image, 247 MB as a file, peaks at 326 MB). Each + channel keeps room ahead of its rows for its marker and a PSB's row sizes, so its block is + written in place for either format. zlib's stream depends on how its input is cut, so a ZIP + channel's bytes are those of a deflate fed one row at a time. + + PSB by the option, for a file named `.psb` (read from the target, since libvips writes files + through `psdsave_target`), or a side past 30 000; then, the channels encoded, when a PSD + cannot hold the file: an RLE row past 65 535 bytes, a layer section past 4 GiB, or a file + past 2 GB, which Adobe documents as Photoshop's limit for a PSD. - **Builds.** The plugin compiles minipsd in privately (`MINIPSD_STATIC`). On Windows it uses the zip's `libz1` (zlib-ng, built zlib-compatible) with zlib 1.3.1's headers, fetched and pinned, - since the zip has none; elsewhere pkg-config's `vips` and `zlib`. libvips 8.17 replaced + since the zip has none; elsewhere pkg-config's `vips` and `zlib`. A program's own libvips + replaces the fetched one by `FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS`, or by the targets + `minipsd_libvips` and `minipsd_libvips_zlib`. Released as `minipsd-vips-X.Y.Z.zip` + (`tools/package_vips.cmake`): the plugin as a CMake project, minipsd's single header private to + it, built from the zip and its example run by CI and by the release. libvips 8.17 replaced `VipsSaveable` with `VipsForeignSaveable`; both are handled. CI's `msvc` job runs its test, which compares every pixel with minipsd's own decode, on psd-tools' set too, and checks that a 30000-row image read sequentially leaves libvips holding a few rows' worth. diff --git a/docs/USE-CASES.md b/docs/USE-CASES.md index 34685de..dceaf0c 100644 --- a/docs/USE-CASES.md +++ b/docs/USE-CASES.md @@ -172,7 +172,9 @@ This is not the roadmap, just where each case fits against the current plan in - **Large, only on real demand:** - R9 (streaming parser): PSD's planar layout means a whole compressed layer held when read forward only (ROADMAP S6.15); - - W6 and W8 (row provider, layer-at-a-time); + - W6 and W8 (row provider, layer-at-a-time); W6's own case, a saver that reads its image once, + is served without it: the channels encoded by the program, row by row, and given as stored + (`minipsd_encode_row`, the libvips plugin's `psdsave`); - W9 (parallel split API); - E3 (segment output); - in-place patching. diff --git a/include/minipsd.h b/include/minipsd.h index e070f71..791e9b5 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -81,8 +81,9 @@ * Bytes come out in order, never seeking back, so a pipe or a socket works as well as a file: * pulled by the caller into its own buffer (minipsd_writer_fill), or pushed to a callback * (minipsd_write). The pull writer allocates nothing; its workspace, from the caller, holds a - * size table (layer_count * 6 numbers), the composite's row sizes and one composite row. - * minipsd_write allocates that workspace once. Layer pixels and a supplied composite are read + * size table (layer_count * 7 numbers), the composite's row sizes and one composite row, and, + * when a layer is ZIP, its largest channel twice (raw, then deflated). minipsd_write allocates + * that workspace once, with a buffer for the widest row. Layer pixels and a supplied composite are read * in place, never copied whole. * * \par Project @@ -110,12 +111,12 @@ * The single source of the project version: CMakeLists.txt reads these three lines. */ #define MINIPSD_VERSION_MAJOR 0 -#define MINIPSD_VERSION_MINOR 3 -#define MINIPSD_VERSION_PATCH 2 +#define MINIPSD_VERSION_MINOR 4 +#define MINIPSD_VERSION_PATCH 0 #define MPSD__STR(x) #x #define MPSD__XSTR(x) MPSD__STR(x) -/** \brief The version as a string literal, e.g. "0.3.2". */ +/** \brief The version as a string literal, e.g. "0.4.0". */ #define MINIPSD_VERSION_STRING \ MPSD__XSTR(MINIPSD_VERSION_MAJOR) "." MPSD__XSTR(MINIPSD_VERSION_MINOR) "." MPSD__XSTR(MINIPSD_VERSION_PATCH) @@ -730,8 +731,8 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *doc, const minipsd_write_opt /** * \brief Writes a document as PSD (or PSB) through a callback. * - * The callback receives the whole file front to back, in pieces of up to 4 KiB, and is never - * asked to seek. After it returns 0 it is not called again. The same document always produces + * The callback receives the whole file front to back, in pieces of up to 4 KiB (or of the + * widest row's MINIPSD_ROW_BOUND, when that is more), and is never asked to seek. After it returns 0 it is not called again. The same document always produces * the same bytes. This is the pull writer driven to the end on a workspace from MINIPSD_MALLOC. * * \param doc The document to write. @@ -811,8 +812,10 @@ MINIPSDDEF uint64_t minipsd_writer_total(const minipsd_writer *w); * order, and returns at once: the writer never waits on anything, so it suits a non-blocking * target as well as a blocking one. A target that took only part of the bytes is the caller's * business: keep the rest and send it before calling again. Any cap works, 1 included; a call - * re-encodes the row it stops in, so a buffer of a few rows (64 KiB is plenty) keeps that cost - * negligible. The bytes are those minipsd_write produces for the same document. + * re-encodes the row it stops in from its start, so a buffer of at least the widest row + * (MINIPSD_ROW_BOUND of its width and the depth: a row is one channel's, so 64 KiB holds every + * PSD row at 8 and 16 bits, and rows to about 16 000 px at 32) cuts each row at most once. + * The bytes are those minipsd_write produces for the same document. * * \param w A writer from minipsd_writer_init. * \param buf Receives up to cap bytes. @@ -1221,8 +1224,9 @@ typedef struct minipsd_decode_opts { int keep_matte; /*!< Composite only: non-zero keeps the white matte, as stored. */ int depth; /*!< The target's bits a sample, which must be the file's (minipsd_info.depth): 1, 0 or 8, 16, 32 (see minipsd_plane). */ - minipsd_inflate_fn inflate; /*!< Layers and masks: decodes ZIP channels (compression 2, and - 3 with prediction); NULL leaves them MINIPSD_ERR_UNSUPPORTED. */ + minipsd_inflate_fn inflate; /*!< Layers and masks: decodes ZIP channels (the file's marker 2, + and 3 with prediction: MINIPSD_ZIP and MINIPSD_ZIP_PREDICTED + when written); NULL leaves them MINIPSD_ERR_UNSUPPORTED. */ void *inflate_user; /*!< Passed to inflate. */ uint8_t *scratch; /*!< With inflate: where a ZIP channel is inflated whole, even for a band; at least the layer's (or mask's) height times rows of diff --git a/libvips-plugin/release/README.md b/libvips-plugin/release/README.md index 463bcd4..abd1bb2 100644 --- a/libvips-plugin/release/README.md +++ b/libvips-plugin/release/README.md @@ -24,9 +24,31 @@ add_subdirectory(minipsd-vips-) target_link_libraries(my-app PRIVATE minipsd::vips) ``` -A project with libvips of its own defines two targets before `add_subdirectory`, and nothing is -fetched or found: `minipsd_libvips` (libvips, glib, gobject) and `minipsd_libvips_zlib` (the zlib -libvips links), each an `INTERFACE` library or `IMPORTED GLOBAL`. +Or, with FetchContent, straight from the release: + +```cmake +include(FetchContent) +FetchContent_Declare(minipsd_vips + URL https://github.com/ShadobaDev/minipsd/releases/download//minipsd-vips-.zip + URL_HASH SHA256=.zip.sha256>) +FetchContent_MakeAvailable(minipsd_vips) +``` + +**One libvips.** Linux and macOS take the system's libvips and zlib through pkg-config. Windows +fetches libvips' prebuilt build, and zlib's headers, which that build lacks. A program that +fetches libvips' Windows build itself points the plugin at it first, so that one libvips is +fetched and the plugin is compiled against the headers of the DLLs the program ships: + +```cmake +FetchContent_GetProperties(my_vips SOURCE_DIR my_vips_dir) # vips-dev-x64-*.zip, unpacked +set(FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS "${my_vips_dir}") # the plugin takes this libvips +FetchContent_MakeAvailable(minipsd_vips) +``` + +zlib's headers are still fetched, so `zlib.h` is always there. A program whose libvips comes from +anywhere else defines two targets first, and nothing is fetched or found: `minipsd_libvips` +(libvips, glib, gobject) and `minipsd_libvips_zlib` (zlib's headers and the zlib libvips links), +each an `INTERFACE` library or `IMPORTED GLOBAL`. On its own, with its example as the test: diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index d71d0b5..e7bd9de 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -1417,7 +1417,7 @@ TEST(MinipsdWriterTest, ZipChannelsGoThroughTheCallersDeflate) ASSERT_EQ(minipsd_reader_next_layer(pr, &pv), MINIPSD_MORE); Bytes rows(row * h), stream(rows.size() * 2 + 64); for (std::uint32_t y = 0; y < h; ++y) // red: the pixel's first sample - ASSERT_EQ(minipsd_predict_row(depth, pixels.data() + y * w * pixel, pixel, w, rows.data() + y * row, row), MINIPSD_OK); + ASSERT_EQ(minipsd_predict_row(depth, pixels.data() + std::size_t(y) * w * pixel, pixel, w, rows.data() + y * row, row), MINIPSD_OK); std::size_t n = stream.size(); ASSERT_EQ(zlib_stored_compress(nullptr, rows.data(), rows.size(), stream.data(), &n), 0); Bytes want{0, ZIP_PREDICTED_MARKER}; From 185f05d826fe7a90fa5b86c212d79fbb7db4c0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 21:24:56 +0100 Subject: [PATCH 16/22] refactor(vips): laid out and named as libvips' foreign/ The plugin's formats move to libvips-plugin/foreign/, as libvips keeps its own: psdload.c, psdsave.c, and psd.c and psd.h for what both share (as foreign/tiff.c and tiff.h; the three internal headers become one). minipsd_vips.h and minipsd_vips.c, the plugin's API and registration, stay beside. The zip mirrors it, minipsd's single header in foreign/minipsd/, as libvips carries libnsgif and qoi, so a pull request to libvips copies foreign/ as it is. Types and functions are named as libvips names its own (VipsForeignSavePsd*, vips_foreign_save_psd_*, VipsForeignLoadPsd*, vips_foreign_load_psd_*; helpers vips__foreign_psd_*); the compression enum is VipsForeignPsdCompression, VIPS_FOREIGN_PSD_COMPRESSION_*. psdsave's encoder is split: VipsForeignSavePsdChannels holds one image's channels (a layer's, or the composite's in one block), and a pass reads one image once, in bands, encoding each row into any number of them. psdsave is one pass into the layer and the composite; the bytes are unchanged. ROADMAP: S7.6, a layered document saved through libvips. --- docs/CODE-ARCH.md | 9 +- docs/MANUAL.md | 2 +- docs/ROADMAP.md | 15 +- docs/SPECIFICATION.md | 2 +- libvips-plugin/CMakeLists.txt | 6 +- .../{minipsd_vips_util.c => foreign/psd.c} | 10 +- .../{minipsd_vips_util.h => foreign/psd.h} | 29 +- .../psdload.c} | 232 +++--- libvips-plugin/foreign/psdsave.c | 731 ++++++++++++++++++ libvips-plugin/minipsd_vips.c | 15 +- libvips-plugin/minipsd_vips.h | 10 +- libvips-plugin/minipsd_vips_psdload.h | 20 - libvips-plugin/minipsd_vips_psdsave.c | 678 ---------------- libvips-plugin/minipsd_vips_psdsave.h | 20 - libvips-plugin/release/CMakeLists.txt | 10 +- libvips-plugin/release/README.md | 10 +- libvips-plugin/release/example.c | 2 +- libvips-plugin/test_vips.c | 18 +- tools/package_vips.cmake | 13 +- 19 files changed, 935 insertions(+), 897 deletions(-) rename libvips-plugin/{minipsd_vips_util.c => foreign/psd.c} (75%) rename libvips-plugin/{minipsd_vips_util.h => foreign/psd.h} (60%) rename libvips-plugin/{minipsd_vips_psdload.c => foreign/psdload.c} (67%) create mode 100644 libvips-plugin/foreign/psdsave.c delete mode 100644 libvips-plugin/minipsd_vips_psdload.h delete mode 100644 libvips-plugin/minipsd_vips_psdsave.c delete mode 100644 libvips-plugin/minipsd_vips_psdsave.h diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index 140558b..8771f98 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -116,11 +116,12 @@ examples/ the I/O implementations; built and run in CI, nev read_file.c whole file into memory, described, every layer and the composite decoded resave.c minipsd_read with stored, one layer changed, written back: the round trip libvips-plugin/ minipsd as libvips' loader and saver (psdload, psdsave), registered by the program - minipsd_vips.h the one public call, minipsd_vips_init + minipsd_vips.h the public header: minipsd_vips_init, VipsForeignPsdCompression minipsd_vips.c minipsd_vips_init: registers both sides with libvips - minipsd_vips_psdload.c/.h psdload, _buffer, _source; minipsd's reader compiled in, private - minipsd_vips_psdsave.c/.h psdsave, _buffer, _target; minipsd's writer compiled in, private - minipsd_vips_util.c/.h internal: constants and helpers both sides share + foreign/ as libvips/foreign/ would hold it, named as libvips names its own + psdload.c psdload, _buffer, _source; minipsd's reader compiled in, private + psdsave.c psdsave, _buffer, _target; minipsd's writer compiled in, private + psd.c, psd.h internal: constants, helpers and the types both sides share test_vips.c documents and files loaded and saved through libvips, against minipsd's decode cmake/ minipsd_libvips.cmake libvips and its zlib for the example and the plugin: fetched on Windows, else pkg-config diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 8d154ed..fb23629 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -465,7 +465,7 @@ size (two bytes in a PSD, four in a PSB), then the rows; for ZIP, the zlib strea The composite's is one block: one marker, every channel's row sizes, then every channel's rows. A PSD's row sizes are 16-bit, so a row longer than 65 535 bytes needs a PSB. The libvips plugin's `psdsave` saves this way, one top-to-bottom pass in bands: -[libvips-plugin/minipsd_vips_psdsave.c](../libvips-plugin/minipsd_vips_psdsave.c). +[libvips-plugin/foreign/psdsave.c](../libvips-plugin/foreign/psdsave.c). ## Reading diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1560580..24a8a0d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -38,7 +38,7 @@ The order of work from 0.1.0 to the design in [CODE-ARCH.md](CODE-ARCH.md). Case | [4. Reader](#4-reader) | buffer to structure, for minipsd's own subset | 0.3.0, at S4.10 | | [5. Round trip and conveniences](#5-round-trip-and-conveniences) | layer rectangles, full import, cheap re-save, kept unknown content | batched, when decided | | [6. Format features](#6-format-features) | blend modes, groups, masks, 16-bit, PSB | batched, when decided | -| [7. Interoperability](#7-interoperability) | libvips | 0.3.0 (S7.1), 0.4.0 (S7.2 to S7.5, at S4.12) | +| [7. Interoperability](#7-interoperability) | libvips | 0.3.0 (S7.1), 0.4.0 (S7.2 to S7.6, at S4.12) | | [8. Test quality](#8-test-quality) | branch coverage of compound conditions | with any release | | [Not scheduled](#not-scheduled) | large cases, only on real demand | — | @@ -197,8 +197,8 @@ Additive. See [Reader](CODE-ARCH.md#reader). and CI green on `master`. - [ ] **S4.12** Release **0.4.0** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): breaking, so a MINOR (`composite_alpha` for every composite, the compression constants renumbered, - `minipsd_read` giving transparency as the file has it). The libvips plugin (S7.2 to S7.5) - and its zip; layers without transparency, a layer's own compression, the row functions; + `minipsd_read` giving transparency as the file has it). The libvips plugin (S7.2 to S7.6), + layered documents written through libvips included, and its zip; layers without transparency, a layer's own compression, the row functions; the fixes the pre-release review found (a mask's unbounded rectangle, an RLE count a PSD cannot hold, `minipsd_write`'s cut rows). Prepared as 0.3.2, never tagged, then broken. Before it: [COMPATIBILITY.md](COMPATIBILITY.md) regenerated with the version, the PR merged @@ -598,6 +598,15 @@ once Platemaker does. PSD cannot hold the file; the plugin released as a zip. psdsave is now as fast as ImageMagick's `magicksave` or faster, its files 4 to 6 times smaller, with the right pixels where magicksave's are wrong (alpha) or it fails (20000 x 20000). +- [ ] **S7.6** A layered document saved through libvips: several images, each a layer with its + name, position, opacity, visibility and blend mode, and the composite, in one call, so + that a program uses minipsd's writer through libvips as it reads through it (`psdload` + gives every layer, with `layer`, and its `psd-layer-*` fields). A saver takes one image, + so this is an operation over an array of images, as `vips_composite` is. The plan, with + its questions: `temp/S7.6-PLAN.md` while it is worked on. + *Done when:* Platemaker's template (a "Template" layer and an empty "Layer" above it) is + written through it, the bytes those of `minipsd_write` from the same layers; a file's + layers loaded with `psdload` and saved with it come back with their pixels and fields. ## 8. Test quality diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 51a8b22..8510ecf 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -779,7 +779,7 @@ libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSo to PSD's inverted ink. Kept: the resolution, the ICC profile, EXIF, XMP, IPTC and `psd-pixel-aspect-ratio`. Options: `compression` (the layer's channels: `rle`, the default, `zip`, `zip-predicted`, or `none`, which writes the composite raw too; otherwise the composite - is RLE; `MinipsdVipsCompression` in `minipsd_vips.h`), `psb`, `layer_name` ("Layer 1"), + is RLE; `VipsForeignPsdCompression` in `minipsd_vips.h`), `psb`, `layer_name` ("Layer 1"), `layered`. The image is read once, top to bottom, in the bands libvips hands a saver diff --git a/libvips-plugin/CMakeLists.txt b/libvips-plugin/CMakeLists.txt index 0d1e3fa..caa3f85 100644 --- a/libvips-plugin/CMakeLists.txt +++ b/libvips-plugin/CMakeLists.txt @@ -9,11 +9,11 @@ # compared with minipsd's own decode (test_vips.c) # --------------------------------------------------------------------------- +# foreign/ is what libvips/foreign/ would hold; minipsd_vips.* is the plugin's own (registration, +# the public header). add_library(minipsd_vips STATIC minipsd_vips.c minipsd_vips.h - minipsd_vips_psdload.c minipsd_vips_psdload.h - minipsd_vips_psdsave.c minipsd_vips_psdsave.h - minipsd_vips_util.c minipsd_vips_util.h) + foreign/psdload.c foreign/psdsave.c foreign/psd.c foreign/psd.h) add_library(minipsd::vips ALIAS minipsd_vips) target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd::minipsd minipsd_libvips_zlib) diff --git a/libvips-plugin/minipsd_vips_util.c b/libvips-plugin/foreign/psd.c similarity index 75% rename from libvips-plugin/minipsd_vips_util.c rename to libvips-plugin/foreign/psd.c index 5629166..e34d176 100644 --- a/libvips-plugin/minipsd_vips_util.c +++ b/libvips-plugin/foreign/psd.c @@ -1,26 +1,26 @@ /** * \file - * \brief The helpers minipsd_vips_util.h declares. + * \brief The helpers psd.h declares, which psdload.c and psdsave.c share. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha */ -#include "minipsd_vips_util.h" +#include "psd.h" #include #include -const char *minipsd_vips__suffs[] = { ".psd", ".psb", NULL }; +const char *vips__foreign_psd_suffs[] = { ".psd", ".psb", NULL }; -void minipsd_vips__class_properties(void *class) +void vips__foreign_psd_class_properties(void *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); gobject_class->set_property = vips_object_set_property; gobject_class->get_property = vips_object_get_property; } -void minipsd_vips__invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) +void vips__foreign_psd_invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) { size_t i; unsigned c; diff --git a/libvips-plugin/minipsd_vips_util.h b/libvips-plugin/foreign/psd.h similarity index 60% rename from libvips-plugin/minipsd_vips_util.h rename to libvips-plugin/foreign/psd.h index a1a9f30..a2edc0c 100644 --- a/libvips-plugin/minipsd_vips_util.h +++ b/libvips-plugin/foreign/psd.h @@ -2,15 +2,16 @@ * \file * \brief Internal to the plugin: what the loader and the saver share. * - * Constants, the PSD_ARGS_* pair, and the few helpers both sides use, defined once in - * minipsd_vips_util.c (prefixed minipsd_vips__, since a static library's names meet the program's). + * Constants, the PSD_ARGS_* pair, the few helpers both sides use (defined once in psd.c, prefixed + * vips__foreign_psd_, as libvips prefixes what its parts share), and the operations' types, which + * minipsd_vips_init registers. As libvips keeps TIFF's shared code in foreign/tiff.c and tiff.h. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha */ -#ifndef MINIPSD_VIPS_UTIL_H -#define MINIPSD_VIPS_UTIL_H +#ifndef VIPS_FOREIGN_PSD_H +#define VIPS_FOREIGN_PSD_H #include @@ -34,16 +35,26 @@ #endif /* The suffixes libvips matches files by: .psd and .psb. */ -extern const char *minipsd_vips__suffs[]; +extern const char *vips__foreign_psd_suffs[]; /* GObject resets set_property and get_property in every class (g_object_base_class_init), so each class with arguments of its own sets libvips' again. */ -void minipsd_vips__class_properties(void *class); +void vips__foreign_psd_class_properties(void *class); /* CMYK the other way round, in place: PSD stores ink inverted (full: no ink), libvips holds it as TIFF does (0: no ink), as its own jpegload makes Adobe's inverted CMYK and ImageMagick makes PSD's. The colour channels only, not transparency; both ways, loading and saving. count pixels of bands samples, each of depth bits (8, 16, or 32 as floats). */ -void minipsd_vips__invert_cmyk(void *pixels, size_t count, unsigned bands, int depth); - -#endif /* MINIPSD_VIPS_UTIL_H */ +void vips__foreign_psd_invert_cmyk(void *pixels, size_t count, unsigned bands, int depth); + +/* The operations: each abstract base, then the three made from it (psdload.c, psdsave.c). */ +GType vips_foreign_load_psd_get_type(void); +GType vips_foreign_load_psd_source_get_type(void); +GType vips_foreign_load_psd_file_get_type(void); +GType vips_foreign_load_psd_buffer_get_type(void); +GType vips_foreign_save_psd_get_type(void); +GType vips_foreign_save_psd_target_get_type(void); +GType vips_foreign_save_psd_file_get_type(void); +GType vips_foreign_save_psd_buffer_get_type(void); + +#endif /* VIPS_FOREIGN_PSD_H */ diff --git a/libvips-plugin/minipsd_vips_psdload.c b/libvips-plugin/foreign/psdload.c similarity index 67% rename from libvips-plugin/minipsd_vips_psdload.c rename to libvips-plugin/foreign/psdload.c index cb5e7f0..2fd0bd9 100644 --- a/libvips-plugin/minipsd_vips_psdload.c +++ b/libvips-plugin/foreign/psdload.c @@ -3,8 +3,9 @@ * \brief psdload, psdload_buffer, psdload_source: a PSD or PSB as a libvips image. * * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer - * variants make one), so the code can move into libvips/foreign/ (as psdload.c, the minipsd_vips_ - * names back to vips_foreign_). The file is mapped (vips_source_map) and read in place by + * variants make one), named as libvips names its own, in foreign/ as libvips keeps its formats: + * the directory moves into libvips/foreign/ as it is (minipsd.h into its own foreign/minipsd/, + * as libvips keeps the libraries it carries, libnsgif and qoi). The file is mapped (vips_source_map) and read in place by * minipsd's reader; nothing is copied. * Pixels are decoded in bands of rows as libvips asks for them, from its threads at once. * @@ -29,8 +30,7 @@ #define MINIPSD_IMPLEMENTATION #include "minipsd.h" -#include "minipsd_vips_psdload.h" -#include "minipsd_vips_util.h" +#include "psd.h" #include @@ -48,7 +48,7 @@ /* --- The base class: a PSD read from a source --------------------------------------------- */ -typedef struct _MinipsdVipsPsdload { +typedef struct _VipsForeignLoadPsd { VipsForeignLoad parent_object; int layer; /* the option: -1 the composite, else a layer, bottom first */ VipsSource *source; /* set by the subclass's build */ @@ -63,51 +63,51 @@ typedef struct _MinipsdVipsPsdload { int transparent; /* indexed: the transparent index (resource 1047), or -1 */ size_t scratch_size; /* a ZIP layer's channels inflated whole; 0 without ZIP */ double xres, yres; /* pixels per mm */ -} MinipsdVipsPsdload; +} VipsForeignLoadPsd; -typedef VipsForeignLoadClass MinipsdVipsPsdloadClass; +typedef VipsForeignLoadClass VipsForeignLoadPsdClass; -G_DEFINE_ABSTRACT_TYPE(MinipsdVipsPsdload, minipsd_vips_psdload, VIPS_TYPE_FOREIGN_LOAD) +G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadPsd, vips_foreign_load_psd, VIPS_TYPE_FOREIGN_LOAD) /* Do these bytes start a PSD or a PSB? */ -static int minipsd_vips_psdload_is_a_bytes(const unsigned char *data, size_t size) +static int vips_foreign_load_psd_is_a_bytes(const unsigned char *data, size_t size) { return size >= PSD_SNIFF_SIZE && memcmp(data, PSD_SIGNATURE, PSD_SIGNATURE_SIZE) == 0 && data[4] == 0 && (data[5] == MINIPSD_PSD || data[5] == MINIPSD_PSB); } -static gboolean minipsd_vips_psdload_is_a_source(VipsSource *source) +static gboolean vips_foreign_load_psd_is_a_source(VipsSource *source) { const unsigned char *data = vips_source_sniff(source, PSD_SNIFF_SIZE); - return data && minipsd_vips_psdload_is_a_bytes(data, PSD_SNIFF_SIZE); + return data && vips_foreign_load_psd_is_a_bytes(data, PSD_SNIFF_SIZE); } -static void minipsd_vips_psdload_dispose(GObject *gobject) +static void vips_foreign_load_psd_dispose(GObject *gobject) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)gobject; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)gobject; VIPS_UNREF(psd->source); - G_OBJECT_CLASS(minipsd_vips_psdload_parent_class)->dispose(gobject); + G_OBJECT_CLASS(vips_foreign_load_psd_parent_class)->dispose(gobject); } -static VipsForeignFlags minipsd_vips_psdload_get_flags(VipsForeignLoad *load) +static VipsForeignFlags vips_foreign_load_psd_get_flags(VipsForeignLoad *load) { (void)load; return VIPS_FOREIGN_SEQUENTIAL; /* bands of rows; libvips keeps the image for random access */ } -static void minipsd_vips_psdload_error(MinipsdVipsPsdload *psd, const char *message) +static void vips_foreign_load_psd_error(VipsForeignLoadPsd *psd, const char *message) { vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", message); } /* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ -static VipsBandFormat minipsd_vips_psdload_format(const minipsd_info *info) +static VipsBandFormat vips_foreign_load_psd_format(const minipsd_info *info) { return info->depth == 32 ? VIPS_FORMAT_FLOAT : info->depth == 16 ? VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR; } /* What the samples are, as libvips names it; MULTIBAND where it has no name for the mode. */ -static VipsInterpretation minipsd_vips_psdload_interpretation(const minipsd_info *info) +static VipsInterpretation vips_foreign_load_psd_interpretation(const minipsd_info *info) { switch (info->color_mode) { case MINIPSD_MODE_RGB: @@ -127,11 +127,11 @@ static VipsInterpretation minipsd_vips_psdload_interpretation(const minipsd_info } /* The image's fields: on out, and on the image load() generates. */ -static int minipsd_vips_psdload_fields(MinipsdVipsPsdload *psd, VipsImage *image) +static int vips_foreign_load_psd_fields(VipsForeignLoadPsd *psd, VipsImage *image) { vips_image_init_fields(image, (int)psd->width, (int)psd->height, (int)psd->bands, - minipsd_vips_psdload_format(&psd->info), VIPS_CODING_NONE, - minipsd_vips_psdload_interpretation(&psd->info), psd->xres, psd->yres); + vips_foreign_load_psd_format(&psd->info), VIPS_CODING_NONE, + vips_foreign_load_psd_interpretation(&psd->info), psd->xres, psd->yres); if (psd->layer >= 0) { image->Xoffset = psd->view.left; image->Yoffset = psd->view.top; @@ -140,7 +140,7 @@ static int minipsd_vips_psdload_fields(MinipsdVipsPsdload *psd, VipsImage *image } /* A resource's bytes as a blob of the image, when the file has it. */ -static void minipsd_vips_psdload_set_blob(MinipsdVipsPsdload *psd, uint16_t id, const char *name) +static void vips_foreign_load_psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, const char *name) { minipsd_resource res; if (minipsd_reader_find_resource(psd->reader, id, &res) == MINIPSD_OK && res.data.size) @@ -149,7 +149,7 @@ static void minipsd_vips_psdload_set_blob(MinipsdVipsPsdload *psd, uint16_t id, /* Bytes to inflate the layer's ZIP channels whole, each in its own part (scratch_kept); 0 when none of the planes is ZIP. */ -static size_t minipsd_vips_psdload_scratch_size(const MinipsdVipsPsdload *psd) +static size_t vips_foreign_load_psd_scratch_size(const VipsForeignLoadPsd *psd) { const size_t row = ((size_t)psd->width * psd->info.depth + 7) / 8; unsigned i; @@ -162,7 +162,7 @@ static size_t minipsd_vips_psdload_scratch_size(const MinipsdVipsPsdload *psd) } /* The layer the option names: its record, its size, its name and look as metadata. */ -static int minipsd_vips_psdload_find_layer(MinipsdVipsPsdload *psd) +static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) { VipsImage *out = ((VipsForeignLoad *)psd)->out; char *name; @@ -170,11 +170,11 @@ static int minipsd_vips_psdload_find_layer(MinipsdVipsPsdload *psd) int rc, i; for (i = 0; i <= psd->layer; ++i) if ((rc = minipsd_reader_next_layer(psd->reader, &psd->view)) != MINIPSD_MORE) { - minipsd_vips_psdload_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); + vips_foreign_load_psd_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); return -1; } if (psd->view.right <= psd->view.left || psd->view.bottom <= psd->view.top) { - minipsd_vips_psdload_error(psd, "the layer is empty"); + vips_foreign_load_psd_error(psd, "the layer is empty"); return -1; } psd->width = (uint32_t)(psd->view.right - psd->view.left); @@ -194,9 +194,9 @@ static int minipsd_vips_psdload_find_layer(MinipsdVipsPsdload *psd) return 0; } -static int minipsd_vips_psdload_header(VipsForeignLoad *load) +static int vips_foreign_load_psd_header(VipsForeignLoad *load) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)load; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; minipsd_resource res; minipsd_resolution_info resolution; minipsd_version_info version; @@ -214,7 +214,7 @@ static int minipsd_vips_psdload_header(VipsForeignLoad *load) return -1; rc = minipsd_reader_init(&psd->reader, data, size, workspace, workspace_size, NULL, &psd->info); if (rc != MINIPSD_OK) { - minipsd_vips_psdload_error(psd, minipsd_error_string(rc)); + vips_foreign_load_psd_error(psd, minipsd_error_string(rc)); return -1; } @@ -222,8 +222,8 @@ static int minipsd_vips_psdload_header(VipsForeignLoad *load) psd->height = psd->info.height; /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ psd->planes = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); - if (psd->layer >= 0 && minipsd_vips_psdload_find_layer(psd)) return -1; - psd->scratch_size = psd->layer >= 0 ? minipsd_vips_psdload_scratch_size(psd) : 0; + if (psd->layer >= 0 && vips_foreign_load_psd_find_layer(psd)) return -1; + psd->scratch_size = psd->layer >= 0 ? vips_foreign_load_psd_scratch_size(psd) : 0; /* Indexed: the palette's RGB, and transparency where resource 1047 names an index. */ psd->bands = psd->planes; @@ -231,7 +231,7 @@ static int minipsd_vips_psdload_header(VipsForeignLoad *load) psd->indexed = psd->info.color_mode == MINIPSD_MODE_INDEXED; if (psd->indexed) { if (psd->info.color_mode_data.size < PSD_RGB * PSD_PALETTE_ENTRIES || !psd->info.color_mode_data.data) { - minipsd_vips_psdload_error(psd, "an indexed file without its palette"); + vips_foreign_load_psd_error(psd, "an indexed file without its palette"); return -1; } if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_TRANSPARENCY_INDEX, &res) == MINIPSD_OK @@ -250,15 +250,15 @@ static int minipsd_vips_psdload_header(VipsForeignLoad *load) } psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; - if (minipsd_vips_psdload_fields(psd, load->out)) return -1; + if (vips_foreign_load_psd_fields(psd, load->out)) return -1; if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); vips_image_set_int(load->out, VIPS_META_BITS_PER_SAMPLE, psd->info.depth); if (psd->info.icc.size) vips_image_set_blob_copy(load->out, VIPS_META_ICC_NAME, psd->info.icc.data, psd->info.icc.size); - minipsd_vips_psdload_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); - minipsd_vips_psdload_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); - minipsd_vips_psdload_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); + vips_foreign_load_psd_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); + vips_foreign_load_psd_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); + vips_foreign_load_psd_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); if (psd->info.color_mode_data.size) vips_image_set_blob_copy(load->out, "psd-color-mode-data", psd->info.color_mode_data.data, psd->info.color_mode_data.size); @@ -273,7 +273,7 @@ static int minipsd_vips_psdload_header(VipsForeignLoad *load) } /* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ -static int minipsd_vips_psdload_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) +static int vips_foreign_load_psd_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) { uLongf n = (uLongf)dst_size; (void)user; @@ -282,15 +282,15 @@ static int minipsd_vips_psdload_inflate(void *user, const uint8_t *src, size_t s } /* A thread's sequence: the scratch a ZIP layer's channels stay inflated in, between its bands. */ -typedef struct minipsd_vips_psdload_sequence { +typedef struct vips_foreign_load_psd_sequence { uint8_t *scratch; int kept; -} minipsd_vips_psdload_sequence; +} vips_foreign_load_psd_sequence; -static void *minipsd_vips_psdload_start(VipsImage *out, void *a, void *b) +static void *vips_foreign_load_psd_start(VipsImage *out, void *a, void *b) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)a; - minipsd_vips_psdload_sequence *seq = g_new0(minipsd_vips_psdload_sequence, 1); + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; + vips_foreign_load_psd_sequence *seq = g_new0(vips_foreign_load_psd_sequence, 1); (void)out, (void)b; /* ponytail: every thread inflates a ZIP layer whole into its own scratch; one shared scratch under a lock would bound memory at one copy, if layers that large turn up. */ @@ -301,17 +301,17 @@ static void *minipsd_vips_psdload_start(VipsImage *out, void *a, void *b) return seq; } -static int minipsd_vips_psdload_stop(void *seq, void *a, void *b) +static int vips_foreign_load_psd_stop(void *seq, void *a, void *b) { (void)a, (void)b; - g_free(((minipsd_vips_psdload_sequence *)seq)->scratch); + g_free(((vips_foreign_load_psd_sequence *)seq)->scratch); g_free(seq); return 0; } /* Indexed, in place: each pixel's index (in its first band) becomes the palette's RGB, and alpha 0 at the transparent index. */ -static void minipsd_vips_psdload_expand_palette(const MinipsdVipsPsdload *psd, VipsPel *row, int width) +static void vips_foreign_load_psd_expand_palette(const VipsForeignLoadPsd *psd, VipsPel *row, int width) { const uint8_t *palette = psd->info.color_mode_data.data; int x; @@ -326,10 +326,10 @@ static void minipsd_vips_psdload_expand_palette(const MinipsdVipsPsdload *psd, V /* One band of rows, decoded straight into the region. vips_sequential asks for whole rows; calls run at once from libvips' threads on the one reader, which minipsd allows on a whole file. */ -static int minipsd_vips_psdload_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) +static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)a; - minipsd_vips_psdload_sequence *sequence = (minipsd_vips_psdload_sequence *)seq; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; + vips_foreign_load_psd_sequence *sequence = (vips_foreign_load_psd_sequence *)seq; const VipsRect *rect = ®ion->valid; minipsd_decode_opts opts; minipsd_target target; @@ -339,7 +339,7 @@ static int minipsd_vips_psdload_generate(VipsRegion *region, void *seq, void *a, (void)b, (void)stop; if (rect->left != 0 || rect->width != region->im->Xsize) { - minipsd_vips_psdload_error(psd, "only whole rows are decoded"); + vips_foreign_load_psd_error(psd, "only whole rows are decoded"); return -1; } memset(&opts, 0, sizeof opts); @@ -351,7 +351,7 @@ static int minipsd_vips_psdload_generate(VipsRegion *region, void *seq, void *a, if (psd->layer < 0) { rc = minipsd_decode_composite(psd->reader, &target, &opts); } else { - opts.inflate = minipsd_vips_psdload_inflate; + opts.inflate = vips_foreign_load_psd_inflate; opts.scratch = sequence->scratch; opts.scratch_size = psd->scratch_size; opts.scratch_kept = sequence->kept; @@ -359,16 +359,16 @@ static int minipsd_vips_psdload_generate(VipsRegion *region, void *seq, void *a, sequence->kept = rc == MINIPSD_OK && sequence->scratch; } if (rc != MINIPSD_OK) { - minipsd_vips_psdload_error(psd, minipsd_error_string(rc)); + vips_foreign_load_psd_error(psd, minipsd_error_string(rc)); return -1; } for (y = 0; y < rect->height; ++y) { row = top + (size_t)y * skip; if (psd->indexed) { - minipsd_vips_psdload_expand_palette(psd, row, rect->width); + vips_foreign_load_psd_expand_palette(psd, row, rect->width); } else if (psd->info.color_mode == MINIPSD_MODE_CMYK) { - minipsd_vips__invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); + vips__foreign_psd_invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); } else if (psd->info.color_mode == MINIPSD_MODE_BITMAP) { /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ for (x = 0; x < rect->width; ++x) row[x] = row[x] ? 0 : UCHAR_MAX; @@ -377,188 +377,188 @@ static int minipsd_vips_psdload_generate(VipsRegion *region, void *seq, void *a, return 0; } -static int minipsd_vips_psdload_load(VipsForeignLoad *load) +static int vips_foreign_load_psd_load(VipsForeignLoad *load) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)load; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(load), 2); t[0] = vips_image_new(); - if (minipsd_vips_psdload_fields(psd, t[0]) - || vips_image_generate(t[0], minipsd_vips_psdload_start, minipsd_vips_psdload_generate, - minipsd_vips_psdload_stop, psd, NULL) + if (vips_foreign_load_psd_fields(psd, t[0]) + || vips_image_generate(t[0], vips_foreign_load_psd_start, vips_foreign_load_psd_generate, + vips_foreign_load_psd_stop, psd, NULL) || vips_sequential(t[0], &t[1], NULL) || vips_image_write(t[1], load->real)) return -1; return 0; } -static void minipsd_vips_psdload_class_init(MinipsdVipsPsdloadClass *class) +static void vips_foreign_load_psd_class_init(VipsForeignLoadPsdClass *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - gobject_class->dispose = minipsd_vips_psdload_dispose; - minipsd_vips__class_properties(class); + gobject_class->dispose = vips_foreign_load_psd_dispose; + vips__foreign_psd_class_properties(class); object_class->nickname = "psdload_base"; object_class->description = "load PSD and PSB with minipsd"; - load_class->get_flags = minipsd_vips_psdload_get_flags; - load_class->header = minipsd_vips_psdload_header; - load_class->load = minipsd_vips_psdload_load; + load_class->get_flags = vips_foreign_load_psd_get_flags; + load_class->header = vips_foreign_load_psd_header; + load_class->load = vips_foreign_load_psd_load; PSD_ARGS_BEGIN VIPS_ARG_INT(class, "layer", 20, "Layer", "Load this layer (bottom first), -1 the composite", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdload, layer), PSD_COMPOSITE, INT_MAX, + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignLoadPsd, layer), PSD_COMPOSITE, INT_MAX, PSD_COMPOSITE); PSD_ARGS_END } -static void minipsd_vips_psdload_init(MinipsdVipsPsdload *psd) +static void vips_foreign_load_psd_init(VipsForeignLoadPsd *psd) { psd->layer = PSD_COMPOSITE; } /* --- psdload_source ----------------------------------------------------------------------- */ -typedef struct _MinipsdVipsPsdloadSource { - MinipsdVipsPsdload parent_object; +typedef struct _VipsForeignLoadPsdSource { + VipsForeignLoadPsd parent_object; VipsSource *source; -} MinipsdVipsPsdloadSource; +} VipsForeignLoadPsdSource; -typedef MinipsdVipsPsdloadClass MinipsdVipsPsdloadSourceClass; +typedef VipsForeignLoadPsdClass VipsForeignLoadPsdSourceClass; -G_DEFINE_TYPE(MinipsdVipsPsdloadSource, minipsd_vips_psdload_source, minipsd_vips_psdload_get_type()) +G_DEFINE_TYPE(VipsForeignLoadPsdSource, vips_foreign_load_psd_source, vips_foreign_load_psd_get_type()) -static int minipsd_vips_psdload_source_build(VipsObject *object) +static int vips_foreign_load_psd_source_build(VipsObject *object) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)object; - MinipsdVipsPsdloadSource *source = (MinipsdVipsPsdloadSource *)object; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; + VipsForeignLoadPsdSource *source = (VipsForeignLoadPsdSource *)object; if (source->source) { psd->source = source->source; g_object_ref(psd->source); } - return VIPS_OBJECT_CLASS(minipsd_vips_psdload_source_parent_class)->build(object); + return VIPS_OBJECT_CLASS(vips_foreign_load_psd_source_parent_class)->build(object); } -static void minipsd_vips_psdload_source_class_init(MinipsdVipsPsdloadSourceClass *class) +static void vips_foreign_load_psd_source_class_init(VipsForeignLoadPsdSourceClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class); VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - minipsd_vips__class_properties(class); + vips__foreign_psd_class_properties(class); object_class->nickname = "psdload_source"; object_class->description = "load PSD and PSB from a source with minipsd"; - object_class->build = minipsd_vips_psdload_source_build; + object_class->build = vips_foreign_load_psd_source_build; operation_class->flags |= VIPS_OPERATION_NOCACHE; - load_class->is_a_source = minipsd_vips_psdload_is_a_source; + load_class->is_a_source = vips_foreign_load_psd_is_a_source; PSD_ARGS_BEGIN VIPS_ARG_OBJECT(class, "source", 1, "Source", "Source to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(MinipsdVipsPsdloadSource, source), VIPS_TYPE_SOURCE); + G_STRUCT_OFFSET(VipsForeignLoadPsdSource, source), VIPS_TYPE_SOURCE); PSD_ARGS_END } -static void minipsd_vips_psdload_source_init(MinipsdVipsPsdloadSource *source) +static void vips_foreign_load_psd_source_init(VipsForeignLoadPsdSource *source) { (void)source; } /* --- psdload (a file) ---------------------------------------------------------------------- */ -typedef struct _MinipsdVipsPsdloadFile { - MinipsdVipsPsdload parent_object; +typedef struct _VipsForeignLoadPsdFile { + VipsForeignLoadPsd parent_object; char *filename; -} MinipsdVipsPsdloadFile; +} VipsForeignLoadPsdFile; -typedef MinipsdVipsPsdloadClass MinipsdVipsPsdloadFileClass; +typedef VipsForeignLoadPsdClass VipsForeignLoadPsdFileClass; -G_DEFINE_TYPE(MinipsdVipsPsdloadFile, minipsd_vips_psdload_file, minipsd_vips_psdload_get_type()) +G_DEFINE_TYPE(VipsForeignLoadPsdFile, vips_foreign_load_psd_file, vips_foreign_load_psd_get_type()) -static gboolean minipsd_vips_psdload_is_a_file(const char *filename) +static gboolean vips_foreign_load_psd_is_a_file(const char *filename) { VipsSource *source = vips_source_new_from_file(filename); gboolean is_a; if (!source) return FALSE; - is_a = minipsd_vips_psdload_is_a_source(source); + is_a = vips_foreign_load_psd_is_a_source(source); g_object_unref(source); return is_a; } -static int minipsd_vips_psdload_file_build(VipsObject *object) +static int vips_foreign_load_psd_file_build(VipsObject *object) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)object; - MinipsdVipsPsdloadFile *file = (MinipsdVipsPsdloadFile *)object; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; + VipsForeignLoadPsdFile *file = (VipsForeignLoadPsdFile *)object; if (file->filename && !(psd->source = vips_source_new_from_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(minipsd_vips_psdload_file_parent_class)->build(object); + return VIPS_OBJECT_CLASS(vips_foreign_load_psd_file_parent_class)->build(object); } -static void minipsd_vips_psdload_file_class_init(MinipsdVipsPsdloadFileClass *class) +static void vips_foreign_load_psd_file_class_init(VipsForeignLoadPsdFileClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignClass *foreign_class = (VipsForeignClass *)class; VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - minipsd_vips__class_properties(class); + vips__foreign_psd_class_properties(class); object_class->nickname = "psdload"; object_class->description = "load PSD and PSB with minipsd"; - object_class->build = minipsd_vips_psdload_file_build; - foreign_class->suffs = minipsd_vips__suffs; - load_class->is_a = minipsd_vips_psdload_is_a_file; + object_class->build = vips_foreign_load_psd_file_build; + foreign_class->suffs = vips__foreign_psd_suffs; + load_class->is_a = vips_foreign_load_psd_is_a_file; PSD_ARGS_BEGIN VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(MinipsdVipsPsdloadFile, filename), NULL); + G_STRUCT_OFFSET(VipsForeignLoadPsdFile, filename), NULL); PSD_ARGS_END } -static void minipsd_vips_psdload_file_init(MinipsdVipsPsdloadFile *file) +static void vips_foreign_load_psd_file_init(VipsForeignLoadPsdFile *file) { (void)file; } /* --- psdload_buffer ------------------------------------------------------------------------ */ -typedef struct _MinipsdVipsPsdloadBuffer { - MinipsdVipsPsdload parent_object; +typedef struct _VipsForeignLoadPsdBuffer { + VipsForeignLoadPsd parent_object; VipsBlob *blob; -} MinipsdVipsPsdloadBuffer; +} VipsForeignLoadPsdBuffer; -typedef MinipsdVipsPsdloadClass MinipsdVipsPsdloadBufferClass; +typedef VipsForeignLoadPsdClass VipsForeignLoadPsdBufferClass; -G_DEFINE_TYPE(MinipsdVipsPsdloadBuffer, minipsd_vips_psdload_buffer, minipsd_vips_psdload_get_type()) +G_DEFINE_TYPE(VipsForeignLoadPsdBuffer, vips_foreign_load_psd_buffer, vips_foreign_load_psd_get_type()) -static gboolean minipsd_vips_psdload_is_a_buffer(const void *data, size_t size) +static gboolean vips_foreign_load_psd_is_a_buffer(const void *data, size_t size) { - return minipsd_vips_psdload_is_a_bytes((const unsigned char *)data, size); + return vips_foreign_load_psd_is_a_bytes((const unsigned char *)data, size); } -static int minipsd_vips_psdload_buffer_build(VipsObject *object) +static int vips_foreign_load_psd_buffer_build(VipsObject *object) { - MinipsdVipsPsdload *psd = (MinipsdVipsPsdload *)object; - MinipsdVipsPsdloadBuffer *buffer = (MinipsdVipsPsdloadBuffer *)object; + VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; + VipsForeignLoadPsdBuffer *buffer = (VipsForeignLoadPsdBuffer *)object; if (buffer->blob && !(psd->source = vips_source_new_from_blob(buffer->blob))) return -1; - return VIPS_OBJECT_CLASS(minipsd_vips_psdload_buffer_parent_class)->build(object); + return VIPS_OBJECT_CLASS(vips_foreign_load_psd_buffer_parent_class)->build(object); } -static void minipsd_vips_psdload_buffer_class_init(MinipsdVipsPsdloadBufferClass *class) +static void vips_foreign_load_psd_buffer_class_init(VipsForeignLoadPsdBufferClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - minipsd_vips__class_properties(class); + vips__foreign_psd_class_properties(class); object_class->nickname = "psdload_buffer"; object_class->description = "load PSD and PSB from a buffer with minipsd"; - object_class->build = minipsd_vips_psdload_buffer_build; - load_class->is_a_buffer = minipsd_vips_psdload_is_a_buffer; + object_class->build = vips_foreign_load_psd_buffer_build; + load_class->is_a_buffer = vips_foreign_load_psd_is_a_buffer; PSD_ARGS_BEGIN VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(MinipsdVipsPsdloadBuffer, blob), VIPS_TYPE_BLOB); + G_STRUCT_OFFSET(VipsForeignLoadPsdBuffer, blob), VIPS_TYPE_BLOB); PSD_ARGS_END } -static void minipsd_vips_psdload_buffer_init(MinipsdVipsPsdloadBuffer *buffer) +static void vips_foreign_load_psd_buffer_init(VipsForeignLoadPsdBuffer *buffer) { (void)buffer; } diff --git a/libvips-plugin/foreign/psdsave.c b/libvips-plugin/foreign/psdsave.c new file mode 100644 index 0000000..46f49e6 --- /dev/null +++ b/libvips-plugin/foreign/psdsave.c @@ -0,0 +1,731 @@ +/** + * \file + * \brief psdsave, psdsave_buffer, psdsave_target: an image as a PSD or PSB, one layer or flat. + * + * Laid out as libvips' own savers are (a base class over a VipsTarget; the file and buffer + * variants make one), named as libvips names its own, in foreign/ as libvips keeps its formats: + * the directory moves into libvips/foreign/ as it is (minipsd.h into its own foreign/minipsd/, + * as libvips keeps the libraries it carries, libnsgif and qoi). An image with alpha is written as one layer that is also the + * composite; one without, flat, as Photoshop saves an image without layers: the composite only, + * without transparency (with `layered`, one layer without transparency too). Grey, RGB or CMYK, + * 8 or 16 bits, grey and RGB also 32, CMYK back to PSD's inverted ink; the resolution, the ICC + * profile, EXIF, XMP and IPTC kept. The image is read once, top to bottom, in bands; each + * channel is encoded as its rows come, as minipsd encodes it, kept compressed and handed to + * minipsd as stored: memory about the file's size, not the image's. PSB when asked, for a .psb + * file, a side past PSD's, or when a PSD cannot hold the file (a row its counts cannot hold, a + * layer section past 4 GiB, a file past the 2 GB Photoshop opens as a PSD). + * + * minipsd's writer is compiled in here, private (MINIPSD_STATIC; the reader left out), so a + * program that uses minipsd itself keeps its own. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#define MINIPSD_STATIC +#define MINIPSD_NO_READER +#define MINIPSD_IMPLEMENTATION +#include "minipsd.h" + +#include "minipsd_vips.h" +#include "psd.h" + +#define ZLIB_CONST /* next_in const: rows are only read */ +#include + +#include +#include + +/* --- The saver's base class: an image as a one-layer PSD into a target --------------------- */ + +#define PSD_ZIP_LEVEL 6 /* zlib's default */ +#define PSD_DEFAULT_DPI 72.0 /* when the image's resolution is none PSD can hold */ +#define PSD_MAX_DPI 65536.0 /* PSD's resolution is a 16.16 fixed-point number */ +#define PSD_LAYER_NAME "Layer 1" +#define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ +#define PSD_MAX_PLANES 5 /* the most bands it takes: CMYK and alpha */ + +typedef struct _VipsForeignSavePsd { + VipsForeignSave parent_object; + VipsTarget *target; /* set by the subclass's build */ + gboolean psb; /* the option; also a .psb file, or a side past PSD's */ + VipsForeignPsdCompression compression; /* the option: the layer's channels; none: the composite's too */ + char *layer_name; /* the option */ + gboolean layered; /* the option: a layer also for an image without alpha */ +} VipsForeignSavePsd; + +typedef VipsForeignSaveClass VipsForeignSavePsdClass; + +G_DEFINE_ABSTRACT_TYPE(VipsForeignSavePsd, vips_foreign_save_psd, VIPS_TYPE_FOREIGN_SAVE) + +/* VipsForeignPsdCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none", + "zip-predicted"). */ +static GType vips_foreign_psd_compression_get_type(void) +{ + static gsize type = 0; + if (g_once_init_enter(&type)) { + static const GEnumValue values[] = { + { VIPS_FOREIGN_PSD_COMPRESSION_RLE, "VIPS_FOREIGN_PSD_COMPRESSION_RLE", "rle" }, + { VIPS_FOREIGN_PSD_COMPRESSION_ZIP, "VIPS_FOREIGN_PSD_COMPRESSION_ZIP", "zip" }, + { VIPS_FOREIGN_PSD_COMPRESSION_NONE, "VIPS_FOREIGN_PSD_COMPRESSION_NONE", "none" }, + { VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED, "VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED", "zip-predicted" }, + { 0, NULL, NULL }, + }; + g_once_init_leave(&type, g_enum_register_static("VipsForeignPsdCompression", values)); + } + return (GType)type; +} + +/* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ +static VipsBandFormat vips_foreign_save_psd_save_formats[] = { + /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ + VIPS_FORMAT_UCHAR, VIPS_FORMAT_UCHAR, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, + VIPS_FORMAT_USHORT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT +}; + +static void vips_foreign_save_psd_dispose(GObject *gobject) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)gobject; + VIPS_UNREF(psd->target); + G_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->dispose(gobject); +} + +static int vips_foreign_save_psd_to_target(void *user, const void *data, size_t size) +{ + return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ +} + +/* A blob of the image as a resource to keep, when it has one. */ +static void vips_foreign_save_psd_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, + size_t *count) +{ + const void *data; + size_t size; + if (!vips_image_get_typeof(image, name) || vips_image_get_blob(image, name, &data, &size) || !size) return; + memset(&res[*count], 0, sizeof res[*count]); + res[*count].id = id; + res[*count].data.data = (const uint8_t *)data; + res[*count].data.size = size; + ++*count; +} + +/* --- The channels, encoded once: rows as they come, kept compressed, written as stored ------- */ + +#define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ +#define PSD_STORED_RAW 0 /* ... raw */ +#define PSD_STORED_RLE 1 /* ... PackBits, its rows' sizes ahead of them */ +#define PSD_STORED_ZIP 2 /* ... zlib, without prediction */ +#define PSD_STORED_ZIP_PREDICTED 3 /* ... zlib of each sample's difference from its left neighbour */ +#define PSD_COUNT_PSD 2 /* an RLE row's size: two bytes in a PSD */ +#define PSD_COUNT_PSB 4 /* ... four in a PSB */ +#define PSD_MAX_FILE INT32_MAX /* a PSD Photoshop opens: 2 GB at most, as Adobe documents it */ +#define PSD_ZIP_ROOM 65536 /* deflate's output grows by at least this */ + +/* Bytes that grow, past what guint can count. */ +typedef struct _VipsForeignSavePsdBytes { + uint8_t *data; + size_t size, room; +} VipsForeignSavePsdBytes; + +/* Room for extra bytes more: exactly, or at least doubled. */ +static int vips_foreign_save_psd_grow(VipsForeignSavePsdBytes *b, size_t extra, int exact) +{ + if (extra > b->room - b->size) { + const size_t room = exact ? b->size + extra : VIPS_MAX(b->room * 2, b->size + extra); + uint8_t *grown; + if (room < b->size || !(grown = (uint8_t *)g_try_realloc(b->data, room))) return -1; + b->data = grown; + b->room = room; + } + return 0; +} + +static int vips_foreign_save_psd_append(VipsForeignSavePsdBytes *b, const void *data, size_t size) +{ + if (vips_foreign_save_psd_grow(b, size, 0)) return -1; + memcpy(b->data + b->size, data, size); + b->size += size; + return 0; +} + +/* One channel: room ahead (head) for its marker and its rows' sizes, the most a PSB needs, then + its rows encoded one after another; each one's size (RLE), or its deflate (ZIP). The block a + file stores is then written in front of the rows, where they lie: never copied. */ +typedef struct _VipsForeignSavePsdChannel { + VipsForeignSavePsdBytes rows; + size_t head; + size_t *sizes; + z_stream zip; + int zipping; +} VipsForeignSavePsdChannel; + +/* One image's channels as a file stores them: a layer's, each channel its own block, or the + composite's, one block, its channels joined behind the first's head, its rows matted when the + image has alpha (as minipsd mattes a composite it is given). */ +typedef struct _VipsForeignSavePsdChannels { + VipsForeignSavePsdChannel channel[PSD_MAX_PLANES]; + unsigned planes; /* the image's bands: colour channels, then alpha */ + int compression; /* MINIPSD_RLE, _RAW, _ZIP, _ZIP_PREDICTED */ + int composite, matte; + uint32_t height; + size_t longest; /* the longest RLE row: a PSD counts to 65535 */ +} VipsForeignSavePsdChannels; + +/* One image read once, top to bottom, each row encoded into every target's channels. */ +typedef struct _VipsForeignSavePsdPass { + VipsForeignSavePsdChannels **targets; + unsigned count, planes; + int depth, mode; + uint32_t width; + uint8_t *row; /* one encoded row, MINIPSD_ROW_BOUND */ + size_t row_size; + uint8_t *ink; /* one row of pixels, CMYK inverted to PSD's ink */ + uint8_t *matted; /* one row of pixels, matted for a composite */ + int failed; /* memory ran out, or deflate failed, while the bands came */ +} VipsForeignSavePsdPass; + +static void vips_foreign_save_psd_channel_free(VipsForeignSavePsdChannel *c) +{ + g_free(c->rows.data); + g_free(c->sizes); + if (c->zipping) (void)deflateEnd(&c->zip); + memset(c, 0, sizeof *c); +} + +static void vips_foreign_save_psd_channels_free(VipsForeignSavePsdChannels *c) +{ + unsigned k; + for (k = 0; k < PSD_MAX_PLANES; ++k) vips_foreign_save_psd_channel_free(&c->channel[k]); + memset(c, 0, sizeof *c); +} + +/* A channel ready for rows: the head for the tables of channels channels (the composite's first + holds them all, its others none), raw rows' room known ahead, ZIP's deflate started. */ +static int vips_foreign_save_psd_channel_init(VipsForeignSavePsdChannel *c, int compression, unsigned channels, uint32_t height, + size_t row_bytes) +{ + const int rle = compression == MINIPSD_RLE, raw = compression == MINIPSD_RAW; + c->head = channels ? PSD_MARKER_SIZE + (rle ? (size_t)channels * height * PSD_COUNT_PSB : 0) : 0; + if (rle && !(c->sizes = g_try_new(size_t, height))) return -1; + if (!rle && !raw) { + if (deflateInit(&c->zip, PSD_ZIP_LEVEL) != Z_OK) return -1; + c->zipping = 1; + } + if (vips_foreign_save_psd_grow(&c->rows, c->head + (raw ? (size_t)(channels ? channels : 1u) * height * row_bytes : 0), 1)) + return -1; + c->rows.size = c->head; + return 0; +} + +/* An image's channels ready for its rows: a layer's (composite 0) or the composite's. */ +static int vips_foreign_save_psd_channels_init(VipsForeignSavePsdChannels *c, VipsImage *in, int depth, int compression, + int composite, int matte) +{ + const size_t row_bytes = (size_t)MINIPSD_ROW_BYTES((uint32_t)in->Xsize, depth); + unsigned k; + memset(c, 0, sizeof *c); + c->planes = (unsigned)in->Bands; + c->compression = compression; + c->composite = composite; + c->matte = matte; + c->height = (uint32_t)in->Ysize; + for (k = 0; k < c->planes; ++k) + if (vips_foreign_save_psd_channel_init(&c->channel[k], compression, composite ? (k ? 0 : c->planes) : 1, c->height, row_bytes)) + return -1; + return 0; +} + +/* data deflated onto the channel's rows; with Z_FINISH, the stream ended. */ +static int vips_foreign_save_psd_deflate(VipsForeignSavePsdChannel *c, const uint8_t *data, size_t size, int flush) +{ + int rc; + c->zip.next_in = data; + c->zip.avail_in = (uInt)size; /* a row: within uInt */ + do { + if (vips_foreign_save_psd_grow(&c->rows, PSD_ZIP_ROOM, 0)) return -1; + c->zip.next_out = c->rows.data + c->rows.size; + c->zip.avail_out = (uInt)VIPS_MIN(c->rows.room - c->rows.size, (size_t)UINT_MAX); + rc = deflate(&c->zip, flush); + c->rows.size = (size_t)(c->zip.next_out - c->rows.data); + if (rc != Z_OK && rc != Z_STREAM_END && rc != Z_BUF_ERROR) return -1; + } while (flush == Z_FINISH ? rc != Z_STREAM_END : c->zip.avail_out == 0); + return 0; +} + +/* Row y of channel k out of interleaved pixels, encoded into t's channel: PackBits, raw, or + deflated (raw or predicted rows into the channel's stream). */ +static int vips_foreign_save_psd_encode(VipsForeignSavePsdPass *p, VipsForeignSavePsdChannels *t, const uint8_t *pixels, unsigned k, + uint32_t y) +{ + VipsForeignSavePsdChannel *c = &t->channel[k]; + const size_t sample = (size_t)(p->depth / CHAR_BIT); + size_t n = (size_t)MINIPSD_ROW_BYTES(p->width, p->depth); + const int rc = t->compression == MINIPSD_ZIP_PREDICTED + ? minipsd_predict_row(p->depth, pixels + k * sample, p->planes * sample, p->width, p->row, + p->row_size) + : minipsd_encode_row(t->compression == MINIPSD_RLE ? MINIPSD_RLE : MINIPSD_RAW, p->depth, + pixels + k * sample, p->planes * sample, p->width, p->row, + p->row_size, &n); + if (rc != MINIPSD_OK) return -1; + if (c->zipping) return vips_foreign_save_psd_deflate(c, p->row, n, Z_NO_FLUSH); + if (c->sizes) { + c->sizes[y] = n; + if (n > t->longest) t->longest = n; + } + return vips_foreign_save_psd_append(&c->rows, p->row, n); +} + +/* count rows of interleaved pixels, from row y on, stride bytes apart: CMYK inverted to PSD's ink, + then each target's channels encoded, a composite's rows matted when it asks (RGB with alpha). */ +static int vips_foreign_save_psd_encode_rows(VipsForeignSavePsdPass *p, const uint8_t *rows, uint32_t y, uint32_t count, + size_t stride) +{ + const size_t bytes = (size_t)p->width * p->planes * (size_t)(p->depth / CHAR_BIT); + uint32_t i; + unsigned t, k; + for (i = 0; i < count; ++i, rows += stride) { + const uint8_t *pixels = rows; + int matted = 0; + if (p->mode == MINIPSD_MODE_CMYK) { + memcpy(p->ink, rows, bytes); + vips__foreign_psd_invert_cmyk(p->ink, p->width, p->planes, p->depth); + pixels = p->ink; + } + for (t = 0; t < p->count; ++t) { + VipsForeignSavePsdChannels *target = p->targets[t]; + if (target->matte && !matted) { /* as minipsd mattes a composite it is given (RGB; the others as they are) */ + memcpy(p->matted, pixels, bytes); + (void)minipsd_matte_row(p->mode, p->depth, p->matted, p->width); + matted = 1; + } + for (k = 0; k < p->planes; ++k) + if (vips_foreign_save_psd_encode(p, target, target->matte ? p->matted : pixels, k, y + i)) return -1; + } + } + return 0; +} + +/* A band of the image, as libvips hands it: once, top to bottom; the region is the band. */ +static int vips_foreign_save_psd_band(VipsRegion *region, VipsRect *area, void *user) +{ + VipsForeignSavePsdPass *p = (VipsForeignSavePsdPass *)user; + if (vips_foreign_save_psd_encode_rows(p, VIPS_REGION_ADDR_TOPLEFT(region), (uint32_t)area->top, (uint32_t)area->height, + (size_t)VIPS_REGION_LSKIP(region))) { + p->failed = 1; + return -1; + } + return 0; +} + +/* in read once, top to bottom, in the bands libvips hands a saver, its rows encoded into count + targets' channels (each with in's bands). -1 on failure, *memory set when memory ran out or a + deflate failed; otherwise the error is the image's own, reported where it arose. */ +static int vips_foreign_save_psd_pass(VipsImage *in, int depth, int mode, VipsForeignSavePsdChannels **targets, unsigned count, + int *memory) +{ + VipsForeignSavePsdPass p; + const size_t pixels = (size_t)in->Xsize * (size_t)in->Bands * (size_t)(depth / CHAR_BIT); + int rc; + memset(&p, 0, sizeof p); + p.targets = targets; + p.count = count; + p.planes = (unsigned)in->Bands; + p.depth = depth; + p.mode = mode; + p.width = (uint32_t)in->Xsize; + p.row_size = (size_t)MINIPSD_ROW_BOUND(p.width, depth); + p.row = (uint8_t *)g_try_malloc(p.row_size); + p.ink = (uint8_t *)g_try_malloc(pixels); + p.matted = (uint8_t *)g_try_malloc(pixels); + rc = !p.row || !p.ink || !p.matted ? -1 : vips_sink_disc(in, vips_foreign_save_psd_band, &p); + *memory = rc && (!p.row || !p.ink || !p.matted || p.failed); + g_free(p.row); + g_free(p.ink); + g_free(p.matted); + return rc ? -1 : 0; +} + +/* Every row in: the deflates ended, a composite's channels joined behind the first's head (the + file stores them as one block). */ +static int vips_foreign_save_psd_finish(VipsForeignSavePsdChannels *c) +{ + VipsForeignSavePsdBytes *first = &c->channel[0].rows; + size_t total = first->size; + unsigned k; + for (k = 0; k < c->planes; ++k) + if (c->channel[k].zipping) { + if (vips_foreign_save_psd_deflate(&c->channel[k], NULL, 0, Z_FINISH)) return -1; + (void)deflateEnd(&c->channel[k].zip); + c->channel[k].zipping = 0; + } + if (!c->composite) return 0; + for (k = 1; k < c->planes; ++k) total += c->channel[k].rows.size; + if (vips_foreign_save_psd_grow(first, total - first->size, 1)) return -1; + for (k = 1; k < c->planes; ++k) { + memcpy(first->data + first->size, c->channel[k].rows.data, c->channel[k].rows.size); + first->size += c->channel[k].rows.size; + g_free(c->channel[k].rows.data); /* copied: the first holds it now */ + memset(&c->channel[k].rows, 0, sizeof c->channel[k].rows); + } + return 0; +} + +/* Puts n, big-endian, in size bytes. */ +static uint8_t *vips_foreign_save_psd_put(uint8_t *p, uint64_t n, unsigned size) +{ + unsigned i; + for (i = 0; i < size; ++i) p[i] = (uint8_t)(n >> (CHAR_BIT * (size - 1 - i))); + return p + size; +} + +/* The block a file stores for channels c[0] .. c[channels - 1], whose rows c[0] holds: the + marker, an RLE one's row sizes in count bytes each, every channel's in turn, then the rows. + Written into c[0]'s head, just ahead of the rows. */ +static minipsd_bytes vips_foreign_save_psd_block(VipsForeignSavePsdChannel *c, unsigned channels, uint32_t height, unsigned count, + int compression) +{ + const int rle = compression == MINIPSD_RLE; + const size_t table = rle ? (size_t)channels * height * count : 0; + const size_t start = c->head - PSD_MARKER_SIZE - table; + const uint8_t marker = rle ? PSD_STORED_RLE : compression == MINIPSD_RAW ? PSD_STORED_RAW + : compression == MINIPSD_ZIP ? PSD_STORED_ZIP : PSD_STORED_ZIP_PREDICTED; + minipsd_bytes block; + uint8_t *p = vips_foreign_save_psd_put(c->rows.data + start, marker, PSD_MARKER_SIZE); + unsigned k; + uint32_t y; + for (k = 0; rle && k < channels; ++k) + for (y = 0; y < height; ++y) p = vips_foreign_save_psd_put(p, c[k].sizes[y], count); + block.data = c->rows.data + start; + block.size = c->rows.size - start; + return block; +} + +/* A layer's channels as a PSD (count 2) or a PSB (4) stores them, into stored: the colour + channels, then transparency. */ +static void vips_foreign_save_psd_layer_blocks(VipsForeignSavePsdChannels *c, unsigned count, minipsd_bytes *stored) +{ + unsigned k; + for (k = 0; k < c->planes; ++k) stored[k] = vips_foreign_save_psd_block(&c->channel[k], 1, c->height, count, c->compression); +} + +/* The composite's channels as a PSD or a PSB stores them: one block. */ +static minipsd_bytes vips_foreign_save_psd_composite_block(VipsForeignSavePsdChannels *c, unsigned count) +{ + return vips_foreign_save_psd_block(c->channel, c->planes, c->height, count, c->compression); +} + +/* Whether a PSD holds the document: its layer section within the 4 GiB its length counts + (minipsd refuses one past it, MINIPSD_ERR_SIZE), the file within PSD_MAX_FILE. Any other + refusal is minipsd_write's to report. */ +static int vips_foreign_save_psd_fits_psd(const minipsd_doc *doc) +{ + minipsd_writer *w; + void *workspace; + size_t size = 0; + int rc = minipsd_writer_workspace_size(doc, NULL, &size), fits; + if (rc != MINIPSD_OK) return rc != MINIPSD_ERR_SIZE; + if (!(workspace = g_try_malloc(size))) return 1; /* minipsd_write will not find it either */ + rc = minipsd_writer_init(&w, doc, NULL, workspace, size); + fits = rc == MINIPSD_OK ? minipsd_writer_total(w) <= PSD_MAX_FILE : rc != MINIPSD_ERR_SIZE; + g_free(workspace); + return fits; +} + +/* MINIPSD_* compression by the option: the layers'; the composite's is RLE, raw with none. */ +static int vips_foreign_save_psd_layer_compression(VipsForeignPsdCompression compression) +{ + static const int by_option[] = { MINIPSD_RLE, MINIPSD_ZIP, MINIPSD_RAW, MINIPSD_ZIP_PREDICTED }; + return by_option[compression]; +} + +static int vips_foreign_save_psd_build(VipsObject *object) +{ + VipsForeignSave *save = (VipsForeignSave *)object; + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsImage *in; + minipsd_resource resources[PSD_MAX_RESOURCES]; + minipsd_bytes stored[PSD_MAX_PLANES]; + minipsd_layer layer; + minipsd_doc doc; + VipsForeignSavePsdChannels layer_channels, composite_channels, *targets[2]; + const void *icc; + const char *filename; + size_t icc_size, colors, count = 0; + double ratio; + unsigned n = 0, table; + int rc, alpha, layered, psb, memory = 0; + + if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->build(object)) return -1; + in = save->ready; + + /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ + memset(&doc, 0, sizeof doc); + if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == 4 || in->Bands == 5)) { + doc.color_mode = MINIPSD_MODE_CMYK; + colors = 4; + } else if (in->Bands == 3 || in->Bands == 4) { + doc.color_mode = MINIPSD_MODE_RGB; + colors = PSD_RGB; + } else if (in->Bands == 1 || in->Bands == 2) { + doc.color_mode = MINIPSD_MODE_GREY; + colors = 1; + } else { + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); + return -1; + } + doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; + alpha = (size_t)in->Bands > colors; /* transparency as the image has it: none, none added */ + layered = alpha || psd->layered; /* without alpha: flat, unless asked */ + doc.width = (uint32_t)in->Xsize; + doc.height = (uint32_t)in->Ysize; + doc.dpi = in->Xres * MM_PER_INCH; + if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; + + /* Every channel encoded once, as minipsd encodes it, while libvips hands the image over in + bands, top to bottom: memory about the file's size, never the whole image's. One pass + feeds the layer and the composite. */ + memset(&layer_channels, 0, sizeof layer_channels); + if (layered) { + memory = vips_foreign_save_psd_channels_init(&layer_channels, in, doc.depth, + vips_foreign_save_psd_layer_compression(psd->compression), 0, 0); + targets[n++] = &layer_channels; + } + if (!memory) + memory = vips_foreign_save_psd_channels_init(&composite_channels, in, doc.depth, + psd->compression == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, + 1, alpha); + else + memset(&composite_channels, 0, sizeof composite_channels); + targets[n++] = &composite_channels; + if (!memory && vips_foreign_save_psd_pass(in, doc.depth, doc.color_mode, targets, n, &memory) && !memory) { + vips_foreign_save_psd_channels_free(&layer_channels); /* the image's own error, reported where it arose */ + vips_foreign_save_psd_channels_free(&composite_channels); + return -1; + } + if (!memory) memory = vips_foreign_save_psd_finish(&layer_channels) || vips_foreign_save_psd_finish(&composite_channels); + if (memory) { + vips_foreign_save_psd_channels_free(&layer_channels); + vips_foreign_save_psd_channels_free(&composite_channels); + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(MINIPSD_ERR_ALLOC)); + return -1; + } + + memset(stored, 0, sizeof stored); /* past the image's bands: none (transparency of size 0) */ + memset(&layer, 0, sizeof layer); + layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; + layer.opacity = UCHAR_MAX; + layer.no_alpha = (uint8_t)!alpha; + layer.stored = stored; /* its own rectangle, the canvas: a stored layer gives it */ + layer.bottom = (int32_t)doc.height; + layer.right = (int32_t)doc.width; + doc.layers = layered ? &layer : NULL; + doc.layer_count = layered ? 1 : 0; + doc.composite_channels = (uint16_t)in->Bands; + doc.composite_alpha = (uint8_t)alpha; + doc.compression = MINIPSD_RLE; + if (vips_image_get_typeof(in, VIPS_META_ICC_NAME) && !vips_image_get_blob(in, VIPS_META_ICC_NAME, &icc, &icc_size)) { + doc.icc = (const uint8_t *)icc; + doc.icc_size = icc_size; + } + if (vips_image_get_typeof(in, "psd-pixel-aspect-ratio") && !vips_image_get_double(in, "psd-pixel-aspect-ratio", &ratio) + && ratio > 0.0) + doc.pixel_aspect_ratio = ratio; + vips_foreign_save_psd_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); + vips_foreign_save_psd_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); + vips_foreign_save_psd_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); + doc.resources = resources; + doc.resource_count = count; + + /* PSB by the option, a file named .psb (libvips writes files through a target too), a side + past PSD's; then, the channels known, a row a PSD cannot count, a layer section past its + 4 GiB, or a file past what Photoshop opens as a PSD. */ + filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); + psb = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE + || in->Ysize > MINIPSD_MAX_SIDE || layer_channels.longest > UINT16_MAX || composite_channels.longest > UINT16_MAX; + for (;;) { /* the blocks for the format, again for a PSB when a PSD cannot hold them */ + table = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; + doc.format = psb ? MINIPSD_PSB : MINIPSD_PSD; + if (layered) vips_foreign_save_psd_layer_blocks(&layer_channels, table, stored); + doc.composite_stored = vips_foreign_save_psd_composite_block(&composite_channels, table); + if (psb || vips_foreign_save_psd_fits_psd(&doc)) break; + psb = 1; + } + + rc = minipsd_write(&doc, NULL, vips_foreign_save_psd_to_target, psd->target); + vips_foreign_save_psd_channels_free(&layer_channels); + vips_foreign_save_psd_channels_free(&composite_channels); + if (rc != MINIPSD_OK) { + vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); + return -1; + } + return vips_target_end(psd->target); +} + +static void vips_foreign_save_psd_class_init(VipsForeignSavePsdClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignSaveClass *save_class = (VipsForeignSaveClass *)class; + + gobject_class->dispose = vips_foreign_save_psd_dispose; + vips__foreign_psd_class_properties(class); + + object_class->nickname = "psdsave_base"; + object_class->description = "save PSD and PSB with minipsd"; + object_class->build = vips_foreign_save_psd_build; + + /* Any bands: the build takes grey, RGB and CMYK, with or without alpha, and refuses the rest. */ +#if VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 17) + save_class->saveable = VIPS_FOREIGN_SAVEABLE_ANY; +#else + save_class->saveable = VIPS_SAVEABLE_ANY; +#endif + save_class->format_table = vips_foreign_save_psd_save_formats; + + PSD_ARGS_BEGIN + VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, psb), FALSE); + VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layer's compression: rle, zip, zip-predicted, or none (the composite too)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, compression), + vips_foreign_psd_compression_get_type(), VIPS_FOREIGN_PSD_COMPRESSION_RLE); + VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, layer_name), PSD_LAYER_NAME); + VIPS_ARG_BOOL(class, "layered", 23, "Layered", "Without alpha, a layer too, without transparency (else flat)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, layered), FALSE); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_init(VipsForeignSavePsd *psd) +{ + (void)psd; +} + +/* --- psdsave_target ------------------------------------------------------------------------ */ + +typedef struct _VipsForeignSavePsdTarget { + VipsForeignSavePsd parent_object; + VipsTarget *target; +} VipsForeignSavePsdTarget; + +typedef VipsForeignSavePsdClass VipsForeignSavePsdTargetClass; + +G_DEFINE_TYPE(VipsForeignSavePsdTarget, vips_foreign_save_psd_target, vips_foreign_save_psd_get_type()) + +static int vips_foreign_save_psd_target_build(VipsObject *object) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsForeignSavePsdTarget *target = (VipsForeignSavePsdTarget *)object; + if (target->target) { + psd->target = target->target; + g_object_ref(psd->target); + } + return VIPS_OBJECT_CLASS(vips_foreign_save_psd_target_parent_class)->build(object); +} + +static void vips_foreign_save_psd_target_class_init(VipsForeignSavePsdTargetClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + vips__foreign_psd_class_properties(class); + object_class->nickname = "psdsave_target"; + object_class->description = "save PSD and PSB to a target with minipsd"; + object_class->build = vips_foreign_save_psd_target_build; + foreign_class->suffs = vips__foreign_psd_suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdTarget, target), VIPS_TYPE_TARGET); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_target_init(VipsForeignSavePsdTarget *target) +{ + (void)target; +} + +/* --- psdsave (a file) ---------------------------------------------------------------------- */ + +typedef struct _VipsForeignSavePsdFile { + VipsForeignSavePsd parent_object; + char *filename; +} VipsForeignSavePsdFile; + +typedef VipsForeignSavePsdClass VipsForeignSavePsdFileClass; + +G_DEFINE_TYPE(VipsForeignSavePsdFile, vips_foreign_save_psd_file, vips_foreign_save_psd_get_type()) + +static int vips_foreign_save_psd_file_build(VipsObject *object) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsForeignSavePsdFile *file = (VipsForeignSavePsdFile *)object; + if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; + return VIPS_OBJECT_CLASS(vips_foreign_save_psd_file_parent_class)->build(object); +} + +static void vips_foreign_save_psd_file_class_init(VipsForeignSavePsdFileClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + vips__foreign_psd_class_properties(class); + object_class->nickname = "psdsave"; + object_class->description = "save PSD and PSB to a file with minipsd"; + object_class->build = vips_foreign_save_psd_file_build; + foreign_class->suffs = vips__foreign_psd_suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdFile, filename), NULL); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_file_init(VipsForeignSavePsdFile *file) +{ + (void)file; +} + +/* --- psdsave_buffer ------------------------------------------------------------------------ */ + +typedef struct _VipsForeignSavePsdBuffer { + VipsForeignSavePsd parent_object; + VipsArea *buf; +} VipsForeignSavePsdBuffer; + +typedef VipsForeignSavePsdClass VipsForeignSavePsdBufferClass; + +G_DEFINE_TYPE(VipsForeignSavePsdBuffer, vips_foreign_save_psd_buffer, vips_foreign_save_psd_get_type()) + +static int vips_foreign_save_psd_buffer_build(VipsObject *object) +{ + VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + VipsBlob *blob; + if (!(psd->target = vips_target_new_to_memory())) return -1; + if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_buffer_parent_class)->build(object)) return -1; + g_object_get(psd->target, "blob", &blob, NULL); + g_object_set(object, "buffer", blob, NULL); + vips_area_unref(VIPS_AREA(blob)); + return 0; +} + +static void vips_foreign_save_psd_buffer_class_init(VipsForeignSavePsdBufferClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsForeignClass *foreign_class = (VipsForeignClass *)class; + + vips__foreign_psd_class_properties(class); + object_class->nickname = "psdsave_buffer"; + object_class->description = "save PSD and PSB to a buffer with minipsd"; + object_class->build = vips_foreign_save_psd_buffer_build; + foreign_class->suffs = vips__foreign_psd_suffs; + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdBuffer, buf), VIPS_TYPE_BLOB); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) +{ + (void)buffer; +} diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index eab7561..e311aaa 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -8,8 +8,7 @@ #include "minipsd_vips.h" -#include "minipsd_vips_psdload.h" -#include "minipsd_vips_psdsave.h" +#include "foreign/psd.h" /* --- Registration -------------------------------------------------------------------------- */ @@ -17,11 +16,11 @@ int minipsd_vips_init(void) { /* Registering a type is enough: libvips finds loaders and savers among VipsForeignLoad's and VipsForeignSave's subclasses. */ - (void)minipsd_vips_psdload_file_get_type(); - (void)minipsd_vips_psdload_buffer_get_type(); - (void)minipsd_vips_psdload_source_get_type(); - (void)minipsd_vips_psdsave_file_get_type(); - (void)minipsd_vips_psdsave_buffer_get_type(); - (void)minipsd_vips_psdsave_target_get_type(); + (void)vips_foreign_load_psd_file_get_type(); + (void)vips_foreign_load_psd_buffer_get_type(); + (void)vips_foreign_load_psd_source_get_type(); + (void)vips_foreign_save_psd_file_get_type(); + (void)vips_foreign_save_psd_buffer_get_type(); + (void)vips_foreign_save_psd_target_get_type(); return 0; } diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index fa196eb..bf88a18 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -25,13 +25,13 @@ extern "C" { * From the command line or an option string, by its nick: `compression=zip`. */ typedef enum { - MINIPSD_VIPS_COMPRESSION_RLE = 0, /*!< PackBits, as Photoshop writes 8-bit layers: the default. */ - MINIPSD_VIPS_COMPRESSION_ZIP = 1, /*!< zlib, without prediction. */ - MINIPSD_VIPS_COMPRESSION_NONE = 2, /*!< Raw: the largest file. */ - MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED = 3 /*!< zlib of each sample's difference from its left + VIPS_FOREIGN_PSD_COMPRESSION_RLE = 0, /*!< PackBits, as Photoshop writes 8-bit layers: the default. */ + VIPS_FOREIGN_PSD_COMPRESSION_ZIP = 1, /*!< zlib, without prediction. */ + VIPS_FOREIGN_PSD_COMPRESSION_NONE = 2, /*!< Raw: the largest file. */ + VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED = 3 /*!< zlib of each sample's difference from its left neighbour, as Photoshop writes 16-bit layers: the smaller file at 16 and 32 bits. */ -} MinipsdVipsCompression; +} VipsForeignPsdCompression; /** * \brief Registers minipsd's operations with libvips. Safe to call more than once. diff --git a/libvips-plugin/minipsd_vips_psdload.h b/libvips-plugin/minipsd_vips_psdload.h deleted file mode 100644 index 5b2a475..0000000 --- a/libvips-plugin/minipsd_vips_psdload.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * \file - * \brief Internal to the plugin: the psdload operations' types (minipsd_vips_psdload.c). - * - * SPDX-License-Identifier: MIT - * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha - */ - -#ifndef MINIPSD_VIPS_PSDLOAD_H -#define MINIPSD_VIPS_PSDLOAD_H - -#include - -/* The abstract base, then the three made from it; minipsd_vips_init registers the three. */ -GType minipsd_vips_psdload_get_type(void); -GType minipsd_vips_psdload_source_get_type(void); -GType minipsd_vips_psdload_file_get_type(void); -GType minipsd_vips_psdload_buffer_get_type(void); - -#endif /* MINIPSD_VIPS_PSDLOAD_H */ diff --git a/libvips-plugin/minipsd_vips_psdsave.c b/libvips-plugin/minipsd_vips_psdsave.c deleted file mode 100644 index 9f9ac5e..0000000 --- a/libvips-plugin/minipsd_vips_psdsave.c +++ /dev/null @@ -1,678 +0,0 @@ -/** - * \file - * \brief psdsave, psdsave_buffer, psdsave_target: an image as a PSD or PSB, one layer or flat. - * - * Laid out as libvips' own savers are (a base class over a VipsTarget; the file and buffer - * variants make one), so the code can move into libvips/foreign/ (as psdsave.c, the minipsd_vips_ - * names back to vips_foreign_). An image with alpha is written as one layer that is also the - * composite; one without, flat, as Photoshop saves an image without layers: the composite only, - * without transparency (with `layered`, one layer without transparency too). Grey, RGB or CMYK, - * 8 or 16 bits, grey and RGB also 32, CMYK back to PSD's inverted ink; the resolution, the ICC - * profile, EXIF, XMP and IPTC kept. The image is read once, top to bottom, in bands; each - * channel is encoded as its rows come, as minipsd encodes it, kept compressed and handed to - * minipsd as stored: memory about the file's size, not the image's. PSB when asked, for a .psb - * file, a side past PSD's, or when a PSD cannot hold the file (a row its counts cannot hold, a - * layer section past 4 GiB, a file past the 2 GB Photoshop opens as a PSD). - * - * minipsd's writer is compiled in here, private (MINIPSD_STATIC; the reader left out), so a - * program that uses minipsd itself keeps its own. - * - * SPDX-License-Identifier: MIT - * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha - */ - -#define MINIPSD_STATIC -#define MINIPSD_NO_READER -#define MINIPSD_IMPLEMENTATION -#include "minipsd.h" - -#include "minipsd_vips.h" -#include "minipsd_vips_psdsave.h" -#include "minipsd_vips_util.h" - -#define ZLIB_CONST /* next_in const: rows are only read */ -#include - -#include -#include - -/* --- The saver's base class: an image as a one-layer PSD into a target --------------------- */ - -#define PSD_ZIP_LEVEL 6 /* zlib's default */ -#define PSD_DEFAULT_DPI 72.0 /* when the image's resolution is none PSD can hold */ -#define PSD_MAX_DPI 65536.0 /* PSD's resolution is a 16.16 fixed-point number */ -#define PSD_LAYER_NAME "Layer 1" -#define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ -#define PSD_MAX_PLANES 5 /* the most bands it takes: CMYK and alpha */ - -typedef struct _MinipsdVipsPsdsave { - VipsForeignSave parent_object; - VipsTarget *target; /* set by the subclass's build */ - gboolean psb; /* the option; also a .psb file, or a side past PSD's */ - MinipsdVipsCompression compression; /* the option: the layer's channels; none: the composite's too */ - char *layer_name; /* the option */ - gboolean layered; /* the option: a layer also for an image without alpha */ -} MinipsdVipsPsdsave; - -typedef VipsForeignSaveClass MinipsdVipsPsdsaveClass; - -G_DEFINE_ABSTRACT_TYPE(MinipsdVipsPsdsave, minipsd_vips_psdsave, VIPS_TYPE_FOREIGN_SAVE) - -/* MinipsdVipsCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none", - "zip-predicted"). */ -static GType minipsd_vips_psdsave_compression_get_type(void) -{ - static gsize type = 0; - if (g_once_init_enter(&type)) { - static const GEnumValue values[] = { - { MINIPSD_VIPS_COMPRESSION_RLE, "MINIPSD_VIPS_COMPRESSION_RLE", "rle" }, - { MINIPSD_VIPS_COMPRESSION_ZIP, "MINIPSD_VIPS_COMPRESSION_ZIP", "zip" }, - { MINIPSD_VIPS_COMPRESSION_NONE, "MINIPSD_VIPS_COMPRESSION_NONE", "none" }, - { MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED, "MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED", "zip-predicted" }, - { 0, NULL, NULL }, - }; - g_once_init_leave(&type, g_enum_register_static("MinipsdVipsCompression", values)); - } - return (GType)type; -} - -/* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ -static VipsBandFormat minipsd_vips_psdsave_save_formats[] = { - /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ - VIPS_FORMAT_UCHAR, VIPS_FORMAT_UCHAR, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, - VIPS_FORMAT_USHORT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT -}; - -static void minipsd_vips_psdsave_dispose(GObject *gobject) -{ - MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)gobject; - VIPS_UNREF(psd->target); - G_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->dispose(gobject); -} - -static int minipsd_vips_psdsave_to_target(void *user, const void *data, size_t size) -{ - return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ -} - -/* A blob of the image as a resource to keep, when it has one. */ -static void minipsd_vips_psdsave_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, - size_t *count) -{ - const void *data; - size_t size; - if (!vips_image_get_typeof(image, name) || vips_image_get_blob(image, name, &data, &size) || !size) return; - memset(&res[*count], 0, sizeof res[*count]); - res[*count].id = id; - res[*count].data.data = (const uint8_t *)data; - res[*count].data.size = size; - ++*count; -} - -/* --- The channels, encoded once: rows as they come, kept compressed, written as stored ------- */ - -#define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ -#define PSD_STORED_RAW 0 /* ... raw */ -#define PSD_STORED_RLE 1 /* ... PackBits, its rows' sizes ahead of them */ -#define PSD_STORED_ZIP 2 /* ... zlib, without prediction */ -#define PSD_STORED_ZIP_PREDICTED 3 /* ... zlib of each sample's difference from its left neighbour */ -#define PSD_COUNT_PSD 2 /* an RLE row's size: two bytes in a PSD */ -#define PSD_COUNT_PSB 4 /* ... four in a PSB */ -#define PSD_MAX_FILE INT32_MAX /* a PSD Photoshop opens: 2 GB at most, as Adobe documents it */ -#define PSD_ZIP_ROOM 65536 /* deflate's output grows by at least this */ - -/* Bytes that grow, past what guint can count. */ -typedef struct _MinipsdVipsPsdsaveBytes { - uint8_t *data; - size_t size, room; -} MinipsdVipsPsdsaveBytes; - -/* Room for extra bytes more: exactly, or at least doubled. */ -static int minipsd_vips_psdsave_grow(MinipsdVipsPsdsaveBytes *b, size_t extra, int exact) -{ - if (extra > b->room - b->size) { - const size_t room = exact ? b->size + extra : VIPS_MAX(b->room * 2, b->size + extra); - uint8_t *grown; - if (room < b->size || !(grown = (uint8_t *)g_try_realloc(b->data, room))) return -1; - b->data = grown; - b->room = room; - } - return 0; -} - -static int minipsd_vips_psdsave_append(MinipsdVipsPsdsaveBytes *b, const void *data, size_t size) -{ - if (minipsd_vips_psdsave_grow(b, size, 0)) return -1; - memcpy(b->data + b->size, data, size); - b->size += size; - return 0; -} - -/* One channel: room ahead (head) for its marker and its rows' sizes, the most a PSB needs, then - its rows encoded one after another; each one's size (RLE), or its deflate (ZIP). The block a - file stores is then written in front of the rows, where they lie: never copied. */ -typedef struct _MinipsdVipsPsdsaveChannel { - MinipsdVipsPsdsaveBytes rows; - size_t head; - size_t *sizes; - z_stream zip; - int zipping; -} MinipsdVipsPsdsaveChannel; - -/* What the build encodes: the layer's channels (the colour ones, then transparency) when there is - a layer, the composite's always (the colour ones, then transparency when the image has it). */ -typedef struct _MinipsdVipsPsdsaveEncoding { - MinipsdVipsPsdsaveChannel layer[PSD_MAX_PLANES], composite[PSD_MAX_PLANES]; - unsigned planes; /* the image's bands: colour channels, then alpha */ - int layered, alpha, depth, mode; - int layer_compression, composite_compression; /* MINIPSD_RLE, _RAW, _ZIP, _ZIP_PREDICTED */ - uint32_t width, height; - size_t longest; /* the longest RLE row: a PSD counts to 65535 */ - uint8_t *row; /* one encoded row, MINIPSD_ROW_BOUND */ - size_t row_size; - uint8_t *ink; /* one row of pixels, CMYK inverted to PSD's ink */ - uint8_t *matted; /* one row of pixels, matted for the composite */ - int failed; /* memory ran out, or deflate failed, while the bands came */ -} MinipsdVipsPsdsaveEncoding; - -static void minipsd_vips_psdsave_channel_free(MinipsdVipsPsdsaveChannel *c) -{ - g_free(c->rows.data); - g_free(c->sizes); - if (c->zipping) (void)deflateEnd(&c->zip); - memset(c, 0, sizeof *c); -} - -static void minipsd_vips_psdsave_encoding_free(MinipsdVipsPsdsaveEncoding *e) -{ - unsigned k; - for (k = 0; k < PSD_MAX_PLANES; ++k) { - minipsd_vips_psdsave_channel_free(&e->layer[k]); - minipsd_vips_psdsave_channel_free(&e->composite[k]); - } - g_free(e->row); - g_free(e->ink); - g_free(e->matted); - memset(e, 0, sizeof *e); -} - -/* A channel ready for rows: the head for the tables of channels channels (the composite's first - holds them all, the others none), raw rows' room known ahead, ZIP's deflate started. */ -static int minipsd_vips_psdsave_channel_init(MinipsdVipsPsdsaveChannel *c, int compression, unsigned channels, uint32_t height, - size_t row_bytes) -{ - const int rle = compression == MINIPSD_RLE, raw = compression == MINIPSD_RAW; - c->head = channels ? PSD_MARKER_SIZE + (rle ? (size_t)channels * height * PSD_COUNT_PSB : 0) : 0; - if (rle && !(c->sizes = g_try_new(size_t, height))) return -1; - if (!rle && !raw) { - if (deflateInit(&c->zip, PSD_ZIP_LEVEL) != Z_OK) return -1; - c->zipping = 1; - } - if (minipsd_vips_psdsave_grow(&c->rows, c->head + (raw ? (size_t)(channels ? channels : 1u) * height * row_bytes : 0), 1)) return -1; - c->rows.size = c->head; - return 0; -} - -static int minipsd_vips_psdsave_encoding_init(MinipsdVipsPsdsaveEncoding *e, VipsImage *in, int depth, int mode, int alpha, - int layered, MinipsdVipsCompression compression) -{ - static const int layer_compression[] = { MINIPSD_RLE, MINIPSD_ZIP, MINIPSD_RAW, MINIPSD_ZIP_PREDICTED }; /* by option */ - const size_t sample = (size_t)(depth / CHAR_BIT); - size_t row_bytes, pixels; - unsigned k; - memset(e, 0, sizeof *e); - e->planes = (unsigned)in->Bands; - e->layered = layered; - e->alpha = alpha; - e->depth = depth; - e->mode = mode; - e->width = (uint32_t)in->Xsize; - e->height = (uint32_t)in->Ysize; - e->layer_compression = layer_compression[compression]; - e->composite_compression = compression == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE; - row_bytes = (size_t)MINIPSD_ROW_BYTES(e->width, depth); - pixels = (size_t)e->width * e->planes * sample; - e->row_size = (size_t)MINIPSD_ROW_BOUND(e->width, depth); - e->row = (uint8_t *)g_try_malloc(e->row_size); - e->ink = (uint8_t *)g_try_malloc(pixels); - e->matted = (uint8_t *)g_try_malloc(pixels); - if (!e->row || !e->ink || !e->matted) return -1; - for (k = 0; k < e->planes; ++k) { - if (minipsd_vips_psdsave_channel_init(&e->composite[k], e->composite_compression, k ? 0 : e->planes, e->height, row_bytes)) - return -1; - if (layered && minipsd_vips_psdsave_channel_init(&e->layer[k], e->layer_compression, 1, e->height, row_bytes)) return -1; - } - return 0; -} - -/* data deflated onto the channel's rows; with Z_FINISH, the stream ended. */ -static int minipsd_vips_psdsave_deflate(MinipsdVipsPsdsaveChannel *c, const uint8_t *data, size_t size, int flush) -{ - int rc; - c->zip.next_in = data; - c->zip.avail_in = (uInt)size; /* a row: within uInt */ - do { - if (minipsd_vips_psdsave_grow(&c->rows, PSD_ZIP_ROOM, 0)) return -1; - c->zip.next_out = c->rows.data + c->rows.size; - c->zip.avail_out = (uInt)VIPS_MIN(c->rows.room - c->rows.size, (size_t)UINT_MAX); - rc = deflate(&c->zip, flush); - c->rows.size = (size_t)(c->zip.next_out - c->rows.data); - if (rc != Z_OK && rc != Z_STREAM_END && rc != Z_BUF_ERROR) return -1; - } while (flush == Z_FINISH ? rc != Z_STREAM_END : c->zip.avail_out == 0); - return 0; -} - -/* Row y of channel k out of interleaved pixels, encoded into c: PackBits, raw, or deflated (raw - or predicted rows into the channel's stream). */ -static int minipsd_vips_psdsave_encode(MinipsdVipsPsdsaveEncoding *e, MinipsdVipsPsdsaveChannel *c, const uint8_t *pixels, - unsigned k, uint32_t y, int compression) -{ - const size_t sample = (size_t)(e->depth / CHAR_BIT); - size_t n = (size_t)MINIPSD_ROW_BYTES(e->width, e->depth); - const int rc = compression == MINIPSD_ZIP_PREDICTED - ? minipsd_predict_row(e->depth, pixels + k * sample, e->planes * sample, e->width, e->row, e->row_size) - : minipsd_encode_row(compression == MINIPSD_RLE ? MINIPSD_RLE : MINIPSD_RAW, e->depth, pixels + k * sample, - e->planes * sample, e->width, e->row, e->row_size, &n); - if (rc != MINIPSD_OK) return -1; - if (c->zipping) return minipsd_vips_psdsave_deflate(c, e->row, n, Z_NO_FLUSH); - if (c->sizes) { - c->sizes[y] = n; - if (n > e->longest) e->longest = n; - } - return minipsd_vips_psdsave_append(&c->rows, e->row, n); -} - -/* count rows of interleaved pixels, from row y on, stride bytes apart: CMYK inverted to PSD's ink, - each channel's rows encoded, the layer's as they are, the composite's matted (RGB with alpha, as - minipsd mattes a composite it is given). */ -static int minipsd_vips_psdsave_encode_rows(MinipsdVipsPsdsaveEncoding *e, const uint8_t *rows, uint32_t y, uint32_t count, - size_t stride) -{ - const size_t bytes = (size_t)e->width * e->planes * (size_t)(e->depth / CHAR_BIT); - uint32_t i; - unsigned k; - for (i = 0; i < count; ++i, rows += stride) { - const uint8_t *pixels = rows, *composite; - if (e->mode == MINIPSD_MODE_CMYK) { - memcpy(e->ink, rows, bytes); - minipsd_vips__invert_cmyk(e->ink, e->width, e->planes, e->depth); - pixels = e->ink; - } - for (k = 0; e->layered && k < e->planes; ++k) - if (minipsd_vips_psdsave_encode(e, &e->layer[k], pixels, k, y + i, e->layer_compression)) return -1; - composite = pixels; - if (e->alpha) { /* matted as minipsd mattes a composite it is given (RGB; the others as they are) */ - memcpy(e->matted, pixels, bytes); - (void)minipsd_matte_row(e->mode, e->depth, e->matted, e->width); - composite = e->matted; - } - for (k = 0; k < e->planes; ++k) - if (minipsd_vips_psdsave_encode(e, &e->composite[k], composite, k, y + i, e->composite_compression)) return -1; - } - return 0; -} - -/* A band of the image, as libvips hands it: once, top to bottom; the region is the band. */ -static int minipsd_vips_psdsave_band(VipsRegion *region, VipsRect *area, void *user) -{ - MinipsdVipsPsdsaveEncoding *e = (MinipsdVipsPsdsaveEncoding *)user; - if (minipsd_vips_psdsave_encode_rows(e, VIPS_REGION_ADDR_TOPLEFT(region), (uint32_t)area->top, (uint32_t)area->height, - (size_t)VIPS_REGION_LSKIP(region))) { - e->failed = 1; - return -1; - } - return 0; -} - -/* Every row in: the deflates ended, the composite's channels joined behind the first's head (the - file stores them as one block). */ -static int minipsd_vips_psdsave_finish(MinipsdVipsPsdsaveEncoding *e) -{ - MinipsdVipsPsdsaveBytes *first = &e->composite[0].rows; - size_t total = first->size; - unsigned k; - for (k = 0; k < e->planes; ++k) - if (e->layer[k].zipping) { - if (minipsd_vips_psdsave_deflate(&e->layer[k], NULL, 0, Z_FINISH)) return -1; - (void)deflateEnd(&e->layer[k].zip); - e->layer[k].zipping = 0; - } - for (k = 1; k < e->planes; ++k) total += e->composite[k].rows.size; - if (minipsd_vips_psdsave_grow(first, total - first->size, 1)) return -1; - for (k = 1; k < e->planes; ++k) { - memcpy(first->data + first->size, e->composite[k].rows.data, e->composite[k].rows.size); - first->size += e->composite[k].rows.size; - g_free(e->composite[k].rows.data); /* copied: the first holds it now */ - memset(&e->composite[k].rows, 0, sizeof e->composite[k].rows); - } - return 0; -} - -/* Puts n, big-endian, in size bytes. */ -static uint8_t *minipsd_vips_psdsave_put(uint8_t *p, uint64_t n, unsigned size) -{ - unsigned i; - for (i = 0; i < size; ++i) p[i] = (uint8_t)(n >> (CHAR_BIT * (size - 1 - i))); - return p + size; -} - -/* The block a file stores for channels c[0] .. c[channels - 1], whose rows c[0] holds: the - marker, an RLE one's row sizes in count bytes each, every channel's in turn, then the rows. - Written into c[0]'s head, just ahead of the rows. */ -static minipsd_bytes minipsd_vips_psdsave_block(MinipsdVipsPsdsaveChannel *c, unsigned channels, uint32_t height, unsigned count, - int compression) -{ - const int rle = compression == MINIPSD_RLE; - const size_t table = rle ? (size_t)channels * height * count : 0; - const size_t start = c->head - PSD_MARKER_SIZE - table; - const uint8_t marker = rle ? PSD_STORED_RLE : compression == MINIPSD_RAW ? PSD_STORED_RAW - : compression == MINIPSD_ZIP ? PSD_STORED_ZIP : PSD_STORED_ZIP_PREDICTED; - minipsd_bytes block; - uint8_t *p = minipsd_vips_psdsave_put(c->rows.data + start, marker, PSD_MARKER_SIZE); - unsigned k; - uint32_t y; - for (k = 0; rle && k < channels; ++k) - for (y = 0; y < height; ++y) p = minipsd_vips_psdsave_put(p, c[k].sizes[y], count); - block.data = c->rows.data + start; - block.size = c->rows.size - start; - return block; -} - -/* The document's channels as a PSD or a PSB stores them. */ -static void minipsd_vips_psdsave_stored(MinipsdVipsPsdsaveEncoding *e, minipsd_doc *doc, minipsd_bytes *stored, int psb) -{ - const unsigned count = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; - unsigned k; - doc->format = psb ? MINIPSD_PSB : MINIPSD_PSD; - for (k = 0; e->layered && k < e->planes; ++k) /* the colour channels, then transparency */ - stored[k] = minipsd_vips_psdsave_block(&e->layer[k], 1, e->height, count, e->layer_compression); - doc->composite_stored = minipsd_vips_psdsave_block(e->composite, e->planes, e->height, count, e->composite_compression); -} - -/* Whether a PSD holds the document: its layer section within the 4 GiB its length counts - (minipsd refuses one past it, MINIPSD_ERR_SIZE), the file within PSD_MAX_FILE. Any other - refusal is minipsd_write's to report. */ -static int minipsd_vips_psdsave_fits_psd(const minipsd_doc *doc) -{ - minipsd_writer *w; - void *workspace; - size_t size = 0; - int rc = minipsd_writer_workspace_size(doc, NULL, &size), fits; - if (rc != MINIPSD_OK) return rc != MINIPSD_ERR_SIZE; - if (!(workspace = g_try_malloc(size))) return 1; /* minipsd_write will not find it either */ - rc = minipsd_writer_init(&w, doc, NULL, workspace, size); - fits = rc == MINIPSD_OK ? minipsd_writer_total(w) <= PSD_MAX_FILE : rc != MINIPSD_ERR_SIZE; - g_free(workspace); - return fits; -} - -static int minipsd_vips_psdsave_build(VipsObject *object) -{ - VipsForeignSave *save = (VipsForeignSave *)object; - MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; - VipsImage *in; - minipsd_resource resources[PSD_MAX_RESOURCES]; - minipsd_bytes stored[PSD_MAX_PLANES]; - minipsd_layer layer; - minipsd_doc doc; - MinipsdVipsPsdsaveEncoding e; - const void *icc; - const char *filename; - size_t icc_size, colors, count = 0; - double ratio; - int rc, alpha, psb, memory; - - if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_parent_class)->build(object)) return -1; - in = save->ready; - - /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ - memset(&doc, 0, sizeof doc); - if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == 4 || in->Bands == 5)) { - doc.color_mode = MINIPSD_MODE_CMYK; - colors = 4; - } else if (in->Bands == 3 || in->Bands == 4) { - doc.color_mode = MINIPSD_MODE_RGB; - colors = PSD_RGB; - } else if (in->Bands == 1 || in->Bands == 2) { - doc.color_mode = MINIPSD_MODE_GREY; - colors = 1; - } else { - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); - return -1; - } - doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; - alpha = (size_t)in->Bands > colors; /* transparency as the image has it: none, none added */ - doc.width = (uint32_t)in->Xsize; - doc.height = (uint32_t)in->Ysize; - doc.dpi = in->Xres * MM_PER_INCH; - if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; - - /* Every channel encoded once, as minipsd encodes it, while libvips hands the image over in - bands, top to bottom: memory about the file's size, never the whole image's. */ - memory = minipsd_vips_psdsave_encoding_init(&e, in, doc.depth, doc.color_mode, alpha, alpha || psd->layered, psd->compression); - if (!memory && vips_sink_disc(in, minipsd_vips_psdsave_band, &e)) { - memory = e.failed; - if (!memory) { /* the image's own error, reported where it arose */ - minipsd_vips_psdsave_encoding_free(&e); - return -1; - } - } - if (!memory) memory = minipsd_vips_psdsave_finish(&e); - if (memory) { - minipsd_vips_psdsave_encoding_free(&e); - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(MINIPSD_ERR_ALLOC)); - return -1; - } - - memset(stored, 0, sizeof stored); /* past the image's bands: none (transparency of size 0) */ - memset(&layer, 0, sizeof layer); - layer.name = psd->layer_name ? psd->layer_name : PSD_LAYER_NAME; - layer.opacity = UCHAR_MAX; - layer.no_alpha = (uint8_t)!alpha; - layer.stored = stored; /* its own rectangle, the canvas: a stored layer gives it */ - layer.bottom = (int32_t)doc.height; - layer.right = (int32_t)doc.width; - doc.layers = e.layered ? &layer : NULL; /* without alpha: flat, unless asked */ - doc.layer_count = doc.layers ? 1 : 0; - doc.composite_channels = (uint16_t)in->Bands; - doc.composite_alpha = (uint8_t)alpha; - doc.compression = MINIPSD_RLE; - if (vips_image_get_typeof(in, VIPS_META_ICC_NAME) && !vips_image_get_blob(in, VIPS_META_ICC_NAME, &icc, &icc_size)) { - doc.icc = (const uint8_t *)icc; - doc.icc_size = icc_size; - } - if (vips_image_get_typeof(in, "psd-pixel-aspect-ratio") && !vips_image_get_double(in, "psd-pixel-aspect-ratio", &ratio) - && ratio > 0.0) - doc.pixel_aspect_ratio = ratio; - minipsd_vips_psdsave_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); - minipsd_vips_psdsave_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); - minipsd_vips_psdsave_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); - doc.resources = resources; - doc.resource_count = count; - - /* PSB by the option, a file named .psb (libvips writes files through a target too), a side - past PSD's; then, the channels known, a row a PSD cannot count, a layer section past its - 4 GiB, or a file past what Photoshop opens as a PSD. */ - filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); - psb = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || in->Xsize > MINIPSD_MAX_SIDE - || in->Ysize > MINIPSD_MAX_SIDE || e.longest > UINT16_MAX; - minipsd_vips_psdsave_stored(&e, &doc, stored, psb); - if (!psb && !minipsd_vips_psdsave_fits_psd(&doc)) minipsd_vips_psdsave_stored(&e, &doc, stored, 1); - - rc = minipsd_write(&doc, NULL, minipsd_vips_psdsave_to_target, psd->target); - minipsd_vips_psdsave_encoding_free(&e); - if (rc != MINIPSD_OK) { - vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); - return -1; - } - return vips_target_end(psd->target); -} - -static void minipsd_vips_psdsave_class_init(MinipsdVipsPsdsaveClass *class) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS(class); - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignSaveClass *save_class = (VipsForeignSaveClass *)class; - - gobject_class->dispose = minipsd_vips_psdsave_dispose; - minipsd_vips__class_properties(class); - - object_class->nickname = "psdsave_base"; - object_class->description = "save PSD and PSB with minipsd"; - object_class->build = minipsd_vips_psdsave_build; - - /* Any bands: the build takes grey, RGB and CMYK, with or without alpha, and refuses the rest. */ -#if VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 17) - save_class->saveable = VIPS_FOREIGN_SAVEABLE_ANY; -#else - save_class->saveable = VIPS_SAVEABLE_ANY; -#endif - save_class->format_table = minipsd_vips_psdsave_save_formats; - - PSD_ARGS_BEGIN - VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, psb), FALSE); - VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layer's compression: rle, zip, zip-predicted, or none (the composite too)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, compression), - minipsd_vips_psdsave_compression_get_type(), MINIPSD_VIPS_COMPRESSION_RLE); - VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, layer_name), PSD_LAYER_NAME); - VIPS_ARG_BOOL(class, "layered", 23, "Layered", "Without alpha, a layer too, without transparency (else flat)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsPsdsave, layered), FALSE); - PSD_ARGS_END -} - -static void minipsd_vips_psdsave_init(MinipsdVipsPsdsave *psd) -{ - (void)psd; -} - -/* --- psdsave_target ------------------------------------------------------------------------ */ - -typedef struct _MinipsdVipsPsdsaveTarget { - MinipsdVipsPsdsave parent_object; - VipsTarget *target; -} MinipsdVipsPsdsaveTarget; - -typedef MinipsdVipsPsdsaveClass MinipsdVipsPsdsaveTargetClass; - -G_DEFINE_TYPE(MinipsdVipsPsdsaveTarget, minipsd_vips_psdsave_target, minipsd_vips_psdsave_get_type()) - -static int minipsd_vips_psdsave_target_build(VipsObject *object) -{ - MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; - MinipsdVipsPsdsaveTarget *target = (MinipsdVipsPsdsaveTarget *)object; - if (target->target) { - psd->target = target->target; - g_object_ref(psd->target); - } - return VIPS_OBJECT_CLASS(minipsd_vips_psdsave_target_parent_class)->build(object); -} - -static void minipsd_vips_psdsave_target_class_init(MinipsdVipsPsdsaveTargetClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - - minipsd_vips__class_properties(class); - object_class->nickname = "psdsave_target"; - object_class->description = "save PSD and PSB to a target with minipsd"; - object_class->build = minipsd_vips_psdsave_target_build; - foreign_class->suffs = minipsd_vips__suffs; - - PSD_ARGS_BEGIN - VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(MinipsdVipsPsdsaveTarget, target), VIPS_TYPE_TARGET); - PSD_ARGS_END -} - -static void minipsd_vips_psdsave_target_init(MinipsdVipsPsdsaveTarget *target) -{ - (void)target; -} - -/* --- psdsave (a file) ---------------------------------------------------------------------- */ - -typedef struct _MinipsdVipsPsdsaveFile { - MinipsdVipsPsdsave parent_object; - char *filename; -} MinipsdVipsPsdsaveFile; - -typedef MinipsdVipsPsdsaveClass MinipsdVipsPsdsaveFileClass; - -G_DEFINE_TYPE(MinipsdVipsPsdsaveFile, minipsd_vips_psdsave_file, minipsd_vips_psdsave_get_type()) - -static int minipsd_vips_psdsave_file_build(VipsObject *object) -{ - MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; - MinipsdVipsPsdsaveFile *file = (MinipsdVipsPsdsaveFile *)object; - if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(minipsd_vips_psdsave_file_parent_class)->build(object); -} - -static void minipsd_vips_psdsave_file_class_init(MinipsdVipsPsdsaveFileClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - - minipsd_vips__class_properties(class); - object_class->nickname = "psdsave"; - object_class->description = "save PSD and PSB to a file with minipsd"; - object_class->build = minipsd_vips_psdsave_file_build; - foreign_class->suffs = minipsd_vips__suffs; - - PSD_ARGS_BEGIN - VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(MinipsdVipsPsdsaveFile, filename), NULL); - PSD_ARGS_END -} - -static void minipsd_vips_psdsave_file_init(MinipsdVipsPsdsaveFile *file) -{ - (void)file; -} - -/* --- psdsave_buffer ------------------------------------------------------------------------ */ - -typedef struct _MinipsdVipsPsdsaveBuffer { - MinipsdVipsPsdsave parent_object; - VipsArea *buf; -} MinipsdVipsPsdsaveBuffer; - -typedef MinipsdVipsPsdsaveClass MinipsdVipsPsdsaveBufferClass; - -G_DEFINE_TYPE(MinipsdVipsPsdsaveBuffer, minipsd_vips_psdsave_buffer, minipsd_vips_psdsave_get_type()) - -static int minipsd_vips_psdsave_buffer_build(VipsObject *object) -{ - MinipsdVipsPsdsave *psd = (MinipsdVipsPsdsave *)object; - VipsBlob *blob; - if (!(psd->target = vips_target_new_to_memory())) return -1; - if (VIPS_OBJECT_CLASS(minipsd_vips_psdsave_buffer_parent_class)->build(object)) return -1; - g_object_get(psd->target, "blob", &blob, NULL); - g_object_set(object, "buffer", blob, NULL); - vips_area_unref(VIPS_AREA(blob)); - return 0; -} - -static void minipsd_vips_psdsave_buffer_class_init(MinipsdVipsPsdsaveBufferClass *class) -{ - VipsObjectClass *object_class = (VipsObjectClass *)class; - VipsForeignClass *foreign_class = (VipsForeignClass *)class; - - minipsd_vips__class_properties(class); - object_class->nickname = "psdsave_buffer"; - object_class->description = "save PSD and PSB to a buffer with minipsd"; - object_class->build = minipsd_vips_psdsave_buffer_build; - foreign_class->suffs = minipsd_vips__suffs; - - PSD_ARGS_BEGIN - VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, - G_STRUCT_OFFSET(MinipsdVipsPsdsaveBuffer, buf), VIPS_TYPE_BLOB); - PSD_ARGS_END -} - -static void minipsd_vips_psdsave_buffer_init(MinipsdVipsPsdsaveBuffer *buffer) -{ - (void)buffer; -} diff --git a/libvips-plugin/minipsd_vips_psdsave.h b/libvips-plugin/minipsd_vips_psdsave.h deleted file mode 100644 index c4c7161..0000000 --- a/libvips-plugin/minipsd_vips_psdsave.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * \file - * \brief Internal to the plugin: the psdsave operations' types (minipsd_vips_psdsave.c). - * - * SPDX-License-Identifier: MIT - * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha - */ - -#ifndef MINIPSD_VIPS_PSDSAVE_H -#define MINIPSD_VIPS_PSDSAVE_H - -#include - -/* The abstract base, then the three made from it; minipsd_vips_init registers the three. */ -GType minipsd_vips_psdsave_get_type(void); -GType minipsd_vips_psdsave_target_get_type(void); -GType minipsd_vips_psdsave_file_get_type(void); -GType minipsd_vips_psdsave_buffer_get_type(void); - -#endif /* MINIPSD_VIPS_PSDSAVE_H */ diff --git a/libvips-plugin/release/CMakeLists.txt b/libvips-plugin/release/CMakeLists.txt index 9d267c4..1a7f49b 100644 --- a/libvips-plugin/release/CMakeLists.txt +++ b/libvips-plugin/release/CMakeLists.txt @@ -7,8 +7,8 @@ # then minipsd_vips_init() after VIPS_INIT() (README.md). libvips: fetched on Windows (libvips' # official prebuilt build, pinned), found with pkg-config elsewhere; or the program's own, as the # targets minipsd_libvips and minipsd_libvips_zlib, defined before this (cmake/minipsd_libvips.cmake). -# minipsd is compiled into the plugin from include/minipsd.h, private to it: a program's own -# minipsd, of any version, is left alone. +# minipsd is compiled into the plugin from foreign/minipsd/minipsd.h, private to it: a program's +# own minipsd, of any version, is left alone. foreign/ is laid out as libvips/foreign/. # # minipsd::vips the plugin, a static library # minipsd-vips-example example.c: an image saved and read back through the plugin (its test) @@ -20,10 +20,10 @@ option(MINIPSD_VIPS_EXAMPLE "Build example.c, and its test" ${PROJECT_IS_TOP_LEV include(cmake/minipsd_libvips.cmake) -file(GLOB _minipsd_vips_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h") -add_library(minipsd_vips STATIC ${_minipsd_vips_sources}) +file(GLOB _minipsd_vips_foreign CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/foreign/*.c" "${CMAKE_CURRENT_SOURCE_DIR}/foreign/*.h") +add_library(minipsd_vips STATIC minipsd_vips.c minipsd_vips.h ${_minipsd_vips_foreign}) add_library(minipsd::vips ALIAS minipsd_vips) -target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/foreign/minipsd") target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd_libvips_zlib) set_target_properties(minipsd_vips PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) diff --git a/libvips-plugin/release/README.md b/libvips-plugin/release/README.md index abd1bb2..ff0a2ab 100644 --- a/libvips-plugin/release/README.md +++ b/libvips-plugin/release/README.md @@ -9,8 +9,9 @@ manual's "PSD in libvips: the plugin". | | | |---|---| -| `src/` | the plugin; `minipsd_vips.h` is its API | -| `include/minipsd.h` | minipsd, one file, compiled into the plugin and private to it | +| `minipsd_vips.h`, `minipsd_vips.c` | the plugin's API (what a program includes) and its registration | +| `foreign/` | the loader and the saver, laid out as `libvips/foreign/` | +| `foreign/minipsd/minipsd.h` | minipsd, one file, compiled into the plugin and private to it | | `cmake/minipsd_libvips.cmake` | how libvips is found: Windows fetches libvips' official prebuilt build (pinned), elsewhere pkg-config | | `CMakeLists.txt` | builds `minipsd::vips`, a static library | | `example.c` | an image saved and read back through the plugin: its test | @@ -58,8 +59,9 @@ cmake --build build ctest --test-dir build # Windows: add -C Debug (or the configuration built) ``` -Without CMake, compile `src/*.c` with `include/` on the include path and libvips' and zlib's -flags (`pkg-config --cflags vips zlib`), and link the objects into your program. +Without CMake, compile `minipsd_vips.c` and `foreign/*.c` with the zip's root and +`foreign/minipsd/` on the include path and libvips' and zlib's flags +(`pkg-config --cflags vips zlib`), and link the objects into your program. ## Use diff --git a/libvips-plugin/release/example.c b/libvips-plugin/release/example.c index 6206e59..803df47 100644 --- a/libvips-plugin/release/example.c +++ b/libvips-plugin/release/example.c @@ -35,7 +35,7 @@ int main(int argc, char **argv) || !(back = vips_image_new_from_file(argv[1], NULL))) vips_error_exit(NULL); loader = vips_foreign_find_load(argv[1]); - ok = loader && strcmp(loader, "MinipsdVipsPsdloadFile") == 0 && back->Xsize == SIDE && back->Ysize == SIDE + ok = loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0 && back->Xsize == SIDE && back->Ysize == SIDE && back->Bands == BANDS; printf("%s: %dx%d, %d bands, read by %s: %s\n", argv[1], back->Xsize, back->Ysize, back->Bands, loader ? loader : "nothing", ok ? "ok" : "FAILED"); diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index ca0530e..a452d5a 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -380,7 +380,7 @@ static void check_document(const minipsd_doc *doc, const minipsd_write_opts *opt if (f) fclose(f); loader = vips_foreign_find_load(path); - check(loader && strcmp(loader, "MinipsdVipsPsdloadFile") == 0, "found as psdload", e->name); + check(loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0, "found as psdload", e->name); vips_error_clear(); check_image(vips_image_new_from_file(path, NULL), doc, e, "file", &b); check_image(vips_image_new_from_file(path, "access", VIPS_ACCESS_SEQUENTIAL, NULL), doc, e, "file, sequential", &b); @@ -481,9 +481,9 @@ static int from_pixels(VipsImage *image, unsigned colors, int layered, const cha doc.composite = pixels; doc.composite_alpha = (uint8_t)alpha; doc.compression = !option || strcmp(option, "compression") != 0 ? MINIPSD_RLE - : value == MINIPSD_VIPS_COMPRESSION_ZIP ? MINIPSD_ZIP - : value == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW - : value == MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED ? MINIPSD_ZIP_PREDICTED + : value == VIPS_FOREIGN_PSD_COMPRESSION_ZIP ? MINIPSD_ZIP + : value == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW + : value == VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED ? MINIPSD_ZIP_PREDICTED : MINIPSD_RLE; doc.format = file_size > PSD_VERSION_AT && file[PSD_VERSION_AT] == MINIPSD_PSB ? MINIPSD_PSB : MINIPSD_PSD; opts.deflate = deflate_rows; @@ -534,7 +534,7 @@ static void check_saved(VipsImage *image, const char *name, const char *dir, int return; } saver = vips_foreign_find_save(path); - check(saver && strcmp(saver, "MinipsdVipsPsdsaveFile") == 0, "found as psdsave", name); + check(saver && strcmp(saver, "VipsForeignSavePsdFile") == 0, "found as psdsave", name); /* The same bytes three ways. */ file = read_all(path, &file_size); @@ -905,11 +905,11 @@ int main(int argc, char **argv) check_saved(make_image(rgbf, 3, VIPS_FORMAT_FLOAT, VIPS_INTERPRETATION_scRGB), "save-float.psd", argv[1], 1, NULL, 0); check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-layered.psd", argv[1], 1, "layered", TRUE); check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 0, "compression", - MINIPSD_VIPS_COMPRESSION_ZIP); + VIPS_FOREIGN_PSD_COMPRESSION_ZIP); check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-raw.psd", argv[1], 0, "compression", - MINIPSD_VIPS_COMPRESSION_NONE); + VIPS_FOREIGN_PSD_COMPRESSION_NONE); check_saved(make_image(grey16, 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16), "save-predicted.psd", argv[1], 0, - "compression", MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED); + "compression", VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED); snprintf(path, sizeof path, "%s/save-predicted.psd", argv[1]); check(first_layer_compression(path) == STORED_ZIP_PREDICTED, "ZIP with prediction by option", path); { /* 32-bit noise, rows a PSD cannot count: a PSB, unasked */ @@ -990,7 +990,7 @@ int main(int argc, char **argv) /* Not a PSD: psdload does not claim it. */ loader = vips_foreign_find_load_buffer(junk, sizeof junk); - check(!loader || strcmp(loader, "MinipsdVipsPsdloadBuffer") != 0, "junk not claimed", "junk"); + check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); vips_error_clear(); if (argc == 3) check_list(argv[2]); diff --git a/tools/package_vips.cmake b/tools/package_vips.cmake index 1453eb3..99a6804 100644 --- a/tools/package_vips.cmake +++ b/tools/package_vips.cmake @@ -2,8 +2,10 @@ # that builds as it is (libvips-plugin/release/README.md). Its tree: # # CMakeLists.txt, README.md, example.c libvips-plugin/release/ -# src/ libvips-plugin/minipsd_vips*.c and .h (not its test) -# include/minipsd.h include/ joined by tools/amalgamate.cmake +# minipsd_vips.h, minipsd_vips.c the plugin's own: its API, its registration +# foreign/ libvips-plugin/foreign/: what libvips/foreign/ would hold +# foreign/minipsd/minipsd.h include/ joined by tools/amalgamate.cmake, carried as +# libvips carries libnsgif and qoi # cmake/minipsd_libvips.cmake as the repository finds libvips # LICENSE.MIT # @@ -32,12 +34,13 @@ set(_stage "${_out}/${_name}") file(REMOVE_RECURSE "${_stage}") file(REMOVE "${_out}/${_name}.zip") -file(GLOB _sources "${_root}/libvips-plugin/minipsd_vips*.c" "${_root}/libvips-plugin/minipsd_vips*.h") -file(COPY ${_sources} DESTINATION "${_stage}/src") +file(GLOB _foreign "${_root}/libvips-plugin/foreign/*.c" "${_root}/libvips-plugin/foreign/*.h") +file(COPY ${_foreign} DESTINATION "${_stage}/foreign") +file(COPY "${_root}/libvips-plugin/minipsd_vips.c" "${_root}/libvips-plugin/minipsd_vips.h" DESTINATION "${_stage}") file(COPY "${_root}/libvips-plugin/release/CMakeLists.txt" "${_root}/libvips-plugin/release/README.md" "${_root}/libvips-plugin/release/example.c" "${_root}/LICENSE.MIT" DESTINATION "${_stage}") file(COPY "${_root}/cmake/minipsd_libvips.cmake" DESTINATION "${_stage}/cmake") -execute_process(COMMAND "${CMAKE_COMMAND}" "-DOUTPUT=${_stage}/include/minipsd.h" -P "${_root}/tools/amalgamate.cmake" +execute_process(COMMAND "${CMAKE_COMMAND}" "-DOUTPUT=${_stage}/foreign/minipsd/minipsd.h" -P "${_root}/tools/amalgamate.cmake" RESULT_VARIABLE _rc) if(_rc) message(FATAL_ERROR "tools/amalgamate.cmake failed") From e96832bd85684211fdbe1e901e23e2502b56406c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 22:32:05 +0100 Subject: [PATCH 17/22] feat(vips): psdsave_layers, a layered document saved through libvips A saver takes one image; psdsave_layers (and _buffer, _target, with vips_psdsave_layers in minipsd_vips.h) takes an array of images, one a layer, bottom first, as vips_composite does. Each layer's name, opacity, visibility, blend mode and clipping come from the image's psd-layer-* fields, the ones psdload sets, its position from xoffset and yoffset; psd-layer-empty makes an empty layer, as Photoshop writes one. Each image is read once, in bands, its channels encoded as its rows come. The composite is given, or made by vips_composite from the visible layers (blend modes by the spec's table, opacity in the alpha), flattened against white when no layer has alpha. The canvas is width and height, else the composite's size, else every layer's from (left, top). psdload loads an empty layer as one transparent pixel with psd-layer-empty, and sets psd-layer-clipping, so a file's layers loaded and saved come back the same bytes: Platemaker's template, and a document with a placed, half-opaque, multiplied, clipped layer, are byte for byte minipsd_write's. All 2280 layers of psd-tools' set now load as minipsd decodes them. --- libvips-plugin/foreign/psd.h | 4 + libvips-plugin/foreign/psdload.c | 21 +- libvips-plugin/foreign/psdsave.c | 583 ++++++++++++++++++++++++++++++- libvips-plugin/minipsd_vips.c | 3 + libvips-plugin/minipsd_vips.h | 34 +- libvips-plugin/test_vips.c | 221 +++++++++++- 6 files changed, 835 insertions(+), 31 deletions(-) diff --git a/libvips-plugin/foreign/psd.h b/libvips-plugin/foreign/psd.h index a2edc0c..f95d9e3 100644 --- a/libvips-plugin/foreign/psd.h +++ b/libvips-plugin/foreign/psd.h @@ -56,5 +56,9 @@ GType vips_foreign_save_psd_get_type(void); GType vips_foreign_save_psd_target_get_type(void); GType vips_foreign_save_psd_file_get_type(void); GType vips_foreign_save_psd_buffer_get_type(void); +GType vips_foreign_save_psd_layers_get_type(void); +GType vips_foreign_save_psd_layers_target_get_type(void); +GType vips_foreign_save_psd_layers_file_get_type(void); +GType vips_foreign_save_psd_layers_buffer_get_type(void); #endif /* VIPS_FOREIGN_PSD_H */ diff --git a/libvips-plugin/foreign/psdload.c b/libvips-plugin/foreign/psdload.c index 2fd0bd9..8389deb 100644 --- a/libvips-plugin/foreign/psdload.c +++ b/libvips-plugin/foreign/psdload.c @@ -61,6 +61,7 @@ typedef struct _VipsForeignLoadPsd { unsigned bands; /* what libvips gets: planes, or indexed's RGB(A) */ int indexed; /* the palette makes the colour */ int transparent; /* indexed: the transparent index (resource 1047), or -1 */ + int empty; /* the layer has no pixels: one transparent pixel stands for it */ size_t scratch_size; /* a ZIP layer's channels inflated whole; 0 without ZIP */ double xres, yres; /* pixels per mm */ } VipsForeignLoadPsd; @@ -173,12 +174,12 @@ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) vips_foreign_load_psd_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); return -1; } - if (psd->view.right <= psd->view.left || psd->view.bottom <= psd->view.top) { - vips_foreign_load_psd_error(psd, "the layer is empty"); - return -1; - } - psd->width = (uint32_t)(psd->view.right - psd->view.left); - psd->height = (uint32_t)(psd->view.bottom - psd->view.top); + /* An empty layer (no area: Photoshop writes its new, unpainted layers so) has no pixels, and a + libvips image has one at least: one transparent pixel, with psd-layer-empty, which + psdsave_layers writes back as an empty layer. */ + psd->empty = psd->view.right <= psd->view.left || psd->view.bottom <= psd->view.top; + psd->width = psd->empty ? 1 : (uint32_t)(psd->view.right - psd->view.left); + psd->height = psd->empty ? 1 : (uint32_t)(psd->view.bottom - psd->view.top); /* Transparency when the layer has its channel: a layer without one comes without alpha, as it was saved. */ psd->planes = psd->info.color_channels; @@ -191,6 +192,8 @@ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); vips_image_set_int(out, "psd-layer-opacity", psd->view.opacity); vips_image_set_int(out, "psd-layer-hidden", psd->view.hidden); + vips_image_set_int(out, "psd-layer-clipping", psd->view.clipping); + vips_image_set_int(out, "psd-layer-empty", psd->empty); return 0; } @@ -223,7 +226,7 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ psd->planes = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); if (psd->layer >= 0 && vips_foreign_load_psd_find_layer(psd)) return -1; - psd->scratch_size = psd->layer >= 0 ? vips_foreign_load_psd_scratch_size(psd) : 0; + psd->scratch_size = psd->layer >= 0 && !psd->empty ? vips_foreign_load_psd_scratch_size(psd) : 0; /* Indexed: the palette's RGB, and transparency where resource 1047 names an index. */ psd->bands = psd->planes; @@ -342,6 +345,10 @@ static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a vips_foreign_load_psd_error(psd, "only whole rows are decoded"); return -1; } + if (psd->empty) { /* no pixels: transparent, and colour 0 */ + for (y = 0; y < rect->height; ++y) memset(top + (size_t)y * skip, 0, (size_t)rect->width * psd->bands * (size_t)vips_format_sizeof(region->im->BandFmt)); + return 0; + } memset(&opts, 0, sizeof opts); opts.y_first = (uint32_t)rect->top; opts.y_count = (uint32_t)rect->height; diff --git a/libvips-plugin/foreign/psdsave.c b/libvips-plugin/foreign/psdsave.c index 46f49e6..566bcda 100644 --- a/libvips-plugin/foreign/psdsave.c +++ b/libvips-plugin/foreign/psdsave.c @@ -34,6 +34,7 @@ #include #include +#include #include /* --- The saver's base class: an image as a one-layer PSD into a target --------------------- */ @@ -109,6 +110,31 @@ static void vips_foreign_save_psd_keep_blob(VipsImage *image, const char *name, ++*count; } +/* The PSD mode of an image's bands: grey, RGB or CMYK (by its interpretation), each with or + without alpha, its colour channels in *colors; -1 for any other band count. */ +static int vips_foreign_save_psd_mode(const VipsImage *in, size_t *colors) +{ + if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == PSD_CMYK || in->Bands == PSD_CMYK + 1)) { + *colors = PSD_CMYK; + return MINIPSD_MODE_CMYK; + } + if (in->Bands == PSD_RGB || in->Bands == PSD_RGB + 1) { + *colors = PSD_RGB; + return MINIPSD_MODE_RGB; + } + if (in->Bands == 1 || in->Bands == 2) { + *colors = 1; + return MINIPSD_MODE_GREY; + } + return -1; +} + +/* The PSD depth of an image's samples: 8, 16, 32 (floats); 0 for another format. */ +static int vips_foreign_save_psd_depth(const VipsImage *in) +{ + return in->BandFmt == VIPS_FORMAT_UCHAR ? 8 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : 0; +} + /* --- The channels, encoded once: rows as they come, kept compressed, written as stored ------- */ #define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ @@ -450,30 +476,22 @@ static int vips_foreign_save_psd_build(VipsObject *object) VipsForeignSavePsdChannels layer_channels, composite_channels, *targets[2]; const void *icc; const char *filename; - size_t icc_size, colors, count = 0; + size_t icc_size, colors = 0, count = 0; double ratio; unsigned n = 0, table; - int rc, alpha, layered, psb, memory = 0; + int rc, mode, alpha, layered, psb, memory = 0; if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->build(object)) return -1; in = save->ready; /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ memset(&doc, 0, sizeof doc); - if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == 4 || in->Bands == 5)) { - doc.color_mode = MINIPSD_MODE_CMYK; - colors = 4; - } else if (in->Bands == 3 || in->Bands == 4) { - doc.color_mode = MINIPSD_MODE_RGB; - colors = PSD_RGB; - } else if (in->Bands == 1 || in->Bands == 2) { - doc.color_mode = MINIPSD_MODE_GREY; - colors = 1; - } else { + if ((mode = vips_foreign_save_psd_mode(in, &colors)) < 0) { vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); return -1; } - doc.depth = in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : 8; + doc.color_mode = (uint16_t)mode; + doc.depth = (uint16_t)vips_foreign_save_psd_depth(in); /* format_table made it one of the three */ alpha = (size_t)in->Bands > colors; /* transparency as the image has it: none, none added */ layered = alpha || psd->layered; /* without alpha: flat, unless asked */ doc.width = (uint32_t)in->Xsize; @@ -729,3 +747,542 @@ static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) { (void)buffer; } + +/* --- psdsave_layers: a layered document from an array of images ----------------------------- */ + +/* A saver takes one image; a layered document is several, so this is an operation over an array + of them, as vips_composite is. Each image is a layer, bottom first, its name, opacity, + visibility, blend mode and clipping in the psd-layer-* fields psdload sets, its position in + xoffset and yoffset; psd-layer-empty makes it an empty layer (no pixels), as Photoshop writes + one. The composite is given, or made by vips_composite from the visible layers. */ + +#define PSD_BLEND_KEY_SIZE 4 /* a blend mode's key: four characters, such as "mul " */ + +typedef struct _VipsForeignSavePsdLayers { + VipsOperation parent_object; + VipsTarget *target; /* set by the subclass's build */ + VipsArrayImage *in; /* the layers, bottom first */ + VipsImage *composite; /* the option: the composite, as given */ + VipsForeignPsdCompression compression; + gboolean psb; + int width, height; /* the canvas: 0, from the composite or the layers */ + int left, top; /* where the canvas lies in the layers' coordinates */ +} VipsForeignSavePsdLayers; + +typedef VipsOperationClass VipsForeignSavePsdLayersClass; + +G_DEFINE_ABSTRACT_TYPE(VipsForeignSavePsdLayers, vips_foreign_save_psd_layers, VIPS_TYPE_OPERATION) + +static void vips_foreign_save_psd_layers_dispose(GObject *gobject) +{ + VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)gobject; + VIPS_UNREF(psd->target); + G_OBJECT_CLASS(vips_foreign_save_psd_layers_parent_class)->dispose(gobject); +} + +/* An int field of an image, or fallback without it. */ +static int vips_foreign_save_psd_field(VipsImage *image, const char *name, int fallback) +{ + int value; + return vips_image_get_typeof(image, name) && !vips_image_get_int(image, name, &value) ? value : fallback; +} + +static int vips_foreign_save_psd_empty(VipsImage *image) +{ + return vips_foreign_save_psd_field(image, "psd-layer-empty", 0) != 0; +} + +/* A PSD blend key as a libvips blend mode, as the spec's table maps them (Working with libvips); + a key libvips has no counterpart for is drawn "over", as minipsd's own composer draws it. */ +static VipsBlendMode vips_foreign_save_psd_blend_mode(const char key[PSD_BLEND_KEY_SIZE]) +{ + static const struct { + char key[PSD_BLEND_KEY_SIZE + 1]; + VipsBlendMode mode; + } table[] = { + { "norm", VIPS_BLEND_MODE_OVER }, { "mul ", VIPS_BLEND_MODE_MULTIPLY }, + { "scrn", VIPS_BLEND_MODE_SCREEN }, { "over", VIPS_BLEND_MODE_OVERLAY }, + { "dark", VIPS_BLEND_MODE_DARKEN }, { "lite", VIPS_BLEND_MODE_LIGHTEN }, + { "lddg", VIPS_BLEND_MODE_ADD }, { "div ", VIPS_BLEND_MODE_COLOUR_DODGE }, + { "idiv", VIPS_BLEND_MODE_COLOUR_BURN }, { "hLit", VIPS_BLEND_MODE_HARD_LIGHT }, + { "sLit", VIPS_BLEND_MODE_SOFT_LIGHT }, { "diff", VIPS_BLEND_MODE_DIFFERENCE }, + { "smud", VIPS_BLEND_MODE_EXCLUSION }, + }; + size_t k; + for (k = 0; k < sizeof table / sizeof table[0]; ++k) + if (memcmp(key, table[k].key, PSD_BLEND_KEY_SIZE) == 0) return table[k].mode; + return VIPS_BLEND_MODE_OVER; +} + +/* The visible layers composed by vips_composite onto a transparent canvas of width x height, at + their positions less (left, top), their opacity in their alpha, their blend modes mapped; with + no layer's transparency, flattened against white (as minipsd mattes a composite without it). + Its bands and format are the layers'. Clipping, and a blend mode libvips lacks, are drawn as + normal. NULL with an error. */ +static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, VipsImage **in, int n, const VipsImage *ref, + size_t colors, int mode, int depth, int width, int height, int alpha) +{ + const double max = depth == 32 ? 1.0 : depth == 16 ? (double)UINT16_MAX : (double)UCHAR_MAX; + const int bands = (int)colors + 1; + VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(psd), 3 * n + 4); + VipsImage **layers = (VipsImage **)vips_object_local_array(VIPS_OBJECT(psd), n + 1); /* the canvas, then each */ + int *modes = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)(n + 1) * sizeof(int)); + int *xs = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)(n + 1) * sizeof(int)); + int *ys = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)(n + 1) * sizeof(int)); + double white[PSD_MAX_PLANES], scale[PSD_MAX_PLANES], offset[PSD_MAX_PLANES]; + VipsImage *out, *composed = NULL; + int count = 0, i, k, rc; + + if (!modes || !xs || !ys) return NULL; + if (mode == MINIPSD_MODE_CMYK && depth != 8) { /* libvips takes CMYK's alpha as 8-bit */ + vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", "a 16-bit CMYK document needs its composite given"); + return NULL; + } + /* The canvas: transparent, the document's size, as the layers are. */ + if (vips_black(&t[0], width, height, "bands", bands, NULL) || vips_cast(t[0], &t[1], ref->BandFmt, NULL) + || vips_copy(t[1], &layers[0], "interpretation", ref->Type, NULL)) + return NULL; + ++count; + for (i = 0; i < n; ++i) { /* bottom first, as minipsd and vips_composite both list them */ + VipsImage *layer = in[i]; + char key[PSD_BLEND_KEY_SIZE] = { 'n', 'o', 'r', 'm' }; + const char *given; + const int opacity = VIPS_CLIP(0, vips_foreign_save_psd_field(layer, "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX); + if (vips_foreign_save_psd_empty(layer) || vips_foreign_save_psd_field(layer, "psd-layer-hidden", 0)) continue; + if (layer->Bands == (int)colors) { /* without transparency: opaque */ + if (vips_bandjoin_const1(layer, &t[2 + 3 * i], max, NULL)) return NULL; + layer = t[2 + 3 * i]; + } + if (opacity < UCHAR_MAX) { /* the opacity in the alpha */ + for (k = 0; k < bands; ++k) scale[k] = 1.0, offset[k] = 0.0; + scale[bands - 1] = opacity / (double)UCHAR_MAX; + if (vips_linear(layer, &t[3 + 3 * i], scale, offset, bands, NULL)) return NULL; + layer = t[3 + 3 * i]; + } + if (vips_copy(layer, &t[4 + 3 * i], "interpretation", ref->Type, NULL)) return NULL; + layers[count] = t[4 + 3 * i]; + g_object_ref(layers[count]); /* both arrays drop theirs */ + if (vips_image_get_typeof(in[i], "psd-layer-blend-mode") && !vips_image_get_string(in[i], "psd-layer-blend-mode", &given) + && strlen(given) == PSD_BLEND_KEY_SIZE) + memcpy(key, given, PSD_BLEND_KEY_SIZE); + modes[count - 1] = (int)vips_foreign_save_psd_blend_mode(key); + xs[count - 1] = in[i]->Xoffset - psd->left; /* where the layer goes; it may reach past the canvas */ + ys[count - 1] = in[i]->Yoffset - psd->top; + ++count; + } + if (count == 1) { /* nothing visible: the transparent canvas */ + composed = layers[0]; + g_object_ref(composed); + } else { + VipsArrayInt *x = vips_array_int_new(xs, count - 1), *y = vips_array_int_new(ys, count - 1); + rc = vips_composite(layers, &composed, count, modes, "x", x, "y", y, "compositing_space", ref->Type, NULL); + vips_area_unref(VIPS_AREA(x)); + vips_area_unref(VIPS_AREA(y)); + if (rc) return NULL; + } + if (!alpha) { /* no transparency anywhere: against white, as minipsd mattes; CMYK's white is no ink */ + VipsArrayDouble *background; + for (k = 0; k < (int)colors; ++k) white[k] = mode == MINIPSD_MODE_CMYK ? 0.0 : max; + background = vips_array_double_new(white, (int)colors); + rc = vips_flatten(composed, &out, "background", background, "max_alpha", max, NULL); + vips_area_unref(VIPS_AREA(background)); + g_object_unref(composed); + if (rc) return NULL; + composed = out; + } + rc = vips_cast(composed, &t[2 + 3 * n], ref->BandFmt, NULL); /* vips_composite may give a wider format */ + g_object_unref(composed); + if (rc || vips_copy(t[2 + 3 * n], &out, "interpretation", ref->Type, NULL)) return NULL; + return out; +} + +static int vips_foreign_save_psd_layers_build(VipsObject *object) +{ + VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; + const char *nickname = VIPS_OBJECT_GET_CLASS(object)->nickname; + VipsImage **in, *ref = NULL, *composite = NULL; + VipsForeignSavePsdChannels *channels = NULL, composite_channels, *target; + minipsd_layer *layers = NULL; + minipsd_bytes *stored = NULL; + char **names = NULL; + minipsd_resource resources[PSD_MAX_RESOURCES]; + minipsd_doc doc; + const void *icc; + const char *filename, *error = NULL, *text; + size_t colors = 0, icc_size, count = 0, longest = 0; + unsigned table; + int n, i, mode, depth, width, height, alpha = 0, psb, memory = 0, rc = MINIPSD_OK, failed = 0; + + if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_parent_class)->build(object)) return -1; + in = vips_array_image_get(psd->in, &n); + if (n < 1) { + vips_error(nickname, "%s", "no layers"); + return -1; + } + + /* The mode and depth: the first layer with pixels decides, else the composite, else the first + layer; every other image must agree. */ + for (i = 0; i < n && !ref; ++i) + if (!vips_foreign_save_psd_empty(in[i])) ref = in[i]; + if (!ref) ref = psd->composite ? psd->composite : in[0]; + mode = vips_foreign_save_psd_mode(ref, &colors); + depth = vips_foreign_save_psd_depth(ref); + if (mode < 0 || !depth) { + vips_error(nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha, as uchar, ushort or float"); + return -1; + } + for (i = 0; i < n; ++i) { + if (vips_foreign_save_psd_empty(in[i])) continue; + if (vips_foreign_save_psd_depth(in[i]) != depth || (in[i]->Bands != (int)colors && in[i]->Bands != (int)colors + 1) + || (mode == MINIPSD_MODE_CMYK) != (in[i]->Type == VIPS_INTERPRETATION_CMYK)) { + vips_error(nickname, "layer %d: not the mode or depth of the others (no layer is converted)", i); + return -1; + } + alpha |= in[i]->Bands > (int)colors; + } + + /* The canvas: the options; else the composite's size; else from (left, top), every layer in it. */ + width = psd->width; + height = psd->height; + if (width <= 0 || height <= 0) { + if (psd->composite) { + width = psd->composite->Xsize; + height = psd->composite->Ysize; + } else { + for (i = 0, width = height = 0; i < n; ++i) + if (!vips_foreign_save_psd_empty(in[i])) { + width = VIPS_MAX(width, in[i]->Xoffset + in[i]->Xsize - psd->left); + height = VIPS_MAX(height, in[i]->Yoffset + in[i]->Ysize - psd->top); + } + } + } + if (width <= 0 || height <= 0) { + vips_error(nickname, "%s", "no canvas: give width and height, or the composite"); + return -1; + } + if (psd->composite + && (psd->composite->Xsize != width || psd->composite->Ysize != height || vips_foreign_save_psd_depth(psd->composite) != depth + || (psd->composite->Bands != (int)colors && psd->composite->Bands != (int)colors + 1))) { + vips_error(nickname, "%s", "the composite is not the canvas's size, or not the layers' mode and depth"); + return -1; + } + + /* Each layer read once, its channels encoded as its rows come; then the composite. */ + memset(&composite_channels, 0, sizeof composite_channels); + channels = g_new0(VipsForeignSavePsdChannels, (gsize)n); + layers = g_new0(minipsd_layer, (gsize)n); + stored = g_new0(minipsd_bytes, (gsize)n * PSD_MAX_PLANES); + names = g_new0(char *, (gsize)n); + for (i = 0; i < n && !failed; ++i) { + minipsd_layer *l = &layers[i]; + if (vips_image_get_typeof(in[i], "psd-layer-name") && !vips_image_get_string(in[i], "psd-layer-name", &text)) + l->name = text; + else + l->name = names[i] = g_strdup_printf("Layer %d", i + 1); + l->opacity = (uint8_t)VIPS_CLIP(0, vips_foreign_save_psd_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX); + l->hidden = (uint8_t)(vips_foreign_save_psd_field(in[i], "psd-layer-hidden", 0) != 0); + l->clipping = (uint8_t)(vips_foreign_save_psd_field(in[i], "psd-layer-clipping", 0) != 0); + if (vips_image_get_typeof(in[i], "psd-layer-blend-mode") && !vips_image_get_string(in[i], "psd-layer-blend-mode", &text)) { + if (strlen(text) != PSD_BLEND_KEY_SIZE) { + error = "a psd-layer-blend-mode is not a four-character key, such as \"mul \""; + failed = 1; + break; + } + memcpy(l->blend_mode, text, PSD_BLEND_KEY_SIZE); + } + if (vips_foreign_save_psd_empty(in[i])) continue; /* no pixels, no rectangle: as Photoshop writes one */ + l->top = in[i]->Yoffset - psd->top; + l->left = in[i]->Xoffset - psd->left; + l->bottom = l->top + in[i]->Ysize; + l->right = l->left + in[i]->Xsize; + l->no_alpha = (uint8_t)(in[i]->Bands == (int)colors); + l->stored = &stored[(size_t)i * PSD_MAX_PLANES]; + target = &channels[i]; + if (vips_foreign_save_psd_channels_init(&channels[i], in[i], depth, vips_foreign_save_psd_layer_compression(psd->compression), + 0, 0)) + memory = failed = 1; + else if (vips_foreign_save_psd_pass(in[i], depth, mode, &target, 1, &memory)) /* memory says whose error */ + failed = 1; + else if (vips_foreign_save_psd_finish(&channels[i])) + memory = failed = 1; + longest = VIPS_MAX(longest, channels[i].longest); + } + if (!failed) { + composite = psd->composite ? psd->composite : vips_foreign_save_psd_compose(psd, in, n, ref, colors, mode, depth, width, height, alpha); + if (!composite) failed = 1; + } + if (!failed) { + target = &composite_channels; + if (vips_foreign_save_psd_channels_init(&composite_channels, composite, depth, + psd->compression == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, 1, + composite->Bands > (int)colors)) + memory = failed = 1; + else if (vips_foreign_save_psd_pass(composite, depth, mode, &target, 1, &memory)) + failed = 1; + else if (vips_foreign_save_psd_finish(&composite_channels)) + memory = failed = 1; + longest = VIPS_MAX(longest, composite_channels.longest); + } + + if (!failed) { + memset(&doc, 0, sizeof doc); + doc.color_mode = (uint16_t)mode; + doc.depth = (uint16_t)depth; + doc.width = (uint32_t)width; + doc.height = (uint32_t)height; + doc.dpi = ref->Xres * MM_PER_INCH; + if (!(doc.dpi > 0.0 && doc.dpi < PSD_MAX_DPI)) doc.dpi = PSD_DEFAULT_DPI; + doc.layers = layers; + doc.layer_count = (size_t)n; + doc.composite_channels = (uint16_t)composite->Bands; + doc.composite_alpha = (uint8_t)(composite->Bands > (int)colors); + doc.compression = MINIPSD_RLE; + if (vips_image_get_typeof(ref, VIPS_META_ICC_NAME) && !vips_image_get_blob(ref, VIPS_META_ICC_NAME, &icc, &icc_size)) { + doc.icc = (const uint8_t *)icc; + doc.icc_size = icc_size; + } + vips_foreign_save_psd_keep_blob(ref, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); + vips_foreign_save_psd_keep_blob(ref, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); + vips_foreign_save_psd_keep_blob(ref, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); + doc.resources = resources; + doc.resource_count = count; + + /* PSB as psdsave chooses it. */ + filename = vips_connection_filename(VIPS_CONNECTION(psd->target)); + psb = psd->psb || (filename && vips_iscasepostfix(filename, ".psb")) || width > MINIPSD_MAX_SIDE + || height > MINIPSD_MAX_SIDE || longest > UINT16_MAX; + for (;;) { + table = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; + doc.format = psb ? MINIPSD_PSB : MINIPSD_PSD; + for (i = 0; i < n; ++i) + if (layers[i].stored) vips_foreign_save_psd_layer_blocks(&channels[i], table, stored + (size_t)i * PSD_MAX_PLANES); + doc.composite_stored = vips_foreign_save_psd_composite_block(&composite_channels, table); + if (psb || vips_foreign_save_psd_fits_psd(&doc)) break; + psb = 1; + } + rc = minipsd_write(&doc, NULL, vips_foreign_save_psd_to_target, psd->target); + } + + for (i = 0; channels && i < n; ++i) vips_foreign_save_psd_channels_free(&channels[i]); + vips_foreign_save_psd_channels_free(&composite_channels); + for (i = 0; names && i < n; ++i) g_free(names[i]); + g_free(names); + g_free(stored); + g_free(layers); + g_free(channels); + if (composite && composite != psd->composite) g_object_unref(composite); + if (error) vips_error(nickname, "%s", error); + else if (memory) vips_error(nickname, "%s", minipsd_error_string(MINIPSD_ERR_ALLOC)); + else if (rc != MINIPSD_OK) vips_error(nickname, "%s", minipsd_error_string(rc)); + if (failed || rc != MINIPSD_OK) return -1; /* an image's own error is reported where it arose */ + return vips_target_end(psd->target); +} + +static void vips_foreign_save_psd_layers_class_init(VipsForeignSavePsdLayersClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *)class; + VipsOperationClass *operation_class = (VipsOperationClass *)class; + + gobject_class->dispose = vips_foreign_save_psd_layers_dispose; + vips__foreign_psd_class_properties(class); + + object_class->nickname = "psdsave_layers_base"; + object_class->description = "save images as the layers of a PSD or PSB with minipsd"; + object_class->build = vips_foreign_save_psd_layers_build; + operation_class->flags |= VIPS_OPERATION_NOCACHE; /* it writes: never answered from the cache */ + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "in", 0, "Input", "The layers, bottom first", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdLayers, in), VIPS_TYPE_ARRAY_IMAGE); + VIPS_ARG_IMAGE(class, "composite", 20, "Composite", "The composite, as given; else composed by vips_composite", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, composite)); + VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layers' compression: rle, zip, zip-predicted, or none (the composite too)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, compression), + vips_foreign_psd_compression_get_type(), VIPS_FOREIGN_PSD_COMPRESSION_RLE); + VIPS_ARG_BOOL(class, "psb", 22, "PSB", "Save as PSB (also for a .psb file, or when a PSD cannot hold it)", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, psb), FALSE); + VIPS_ARG_INT(class, "width", 23, "Width", "The canvas's width; 0: the composite's, or every layer's", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, width), 0, MINIPSD_MAX_SIDE_PSB, 0); + VIPS_ARG_INT(class, "height", 24, "Height", "The canvas's height; 0: the composite's, or every layer's", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, height), 0, MINIPSD_MAX_SIDE_PSB, 0); + VIPS_ARG_INT(class, "left", 25, "Left", "The canvas's left edge in the layers' coordinates", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, left), -MINIPSD_MAX_SIDE_PSB, + MINIPSD_MAX_SIDE_PSB, 0); + VIPS_ARG_INT(class, "top", 26, "Top", "The canvas's top edge in the layers' coordinates", + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, top), -MINIPSD_MAX_SIDE_PSB, + MINIPSD_MAX_SIDE_PSB, 0); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_layers_init(VipsForeignSavePsdLayers *psd) +{ + (void)psd; +} + +/* --- psdsave_layers_target, psdsave_layers (a file), psdsave_layers_buffer ------------------ */ + +typedef struct _VipsForeignSavePsdLayersTarget { + VipsForeignSavePsdLayers parent_object; + VipsTarget *target; +} VipsForeignSavePsdLayersTarget; + +typedef VipsForeignSavePsdLayersClass VipsForeignSavePsdLayersTargetClass; + +G_DEFINE_TYPE(VipsForeignSavePsdLayersTarget, vips_foreign_save_psd_layers_target, vips_foreign_save_psd_layers_get_type()) + +static int vips_foreign_save_psd_layers_target_build(VipsObject *object) +{ + VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; + VipsForeignSavePsdLayersTarget *target = (VipsForeignSavePsdLayersTarget *)object; + if (target->target) { + psd->target = target->target; + g_object_ref(psd->target); + } + return VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_target_parent_class)->build(object); +} + +static void vips_foreign_save_psd_layers_target_class_init(VipsForeignSavePsdLayersTargetClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + + vips__foreign_psd_class_properties(class); + object_class->nickname = "psdsave_layers_target"; + object_class->description = "save images as the layers of a PSD or PSB to a target with minipsd"; + object_class->build = vips_foreign_save_psd_layers_target_build; + + PSD_ARGS_BEGIN + VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdLayersTarget, target), VIPS_TYPE_TARGET); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_layers_target_init(VipsForeignSavePsdLayersTarget *target) +{ + (void)target; +} + +typedef struct _VipsForeignSavePsdLayersFile { + VipsForeignSavePsdLayers parent_object; + char *filename; +} VipsForeignSavePsdLayersFile; + +typedef VipsForeignSavePsdLayersClass VipsForeignSavePsdLayersFileClass; + +G_DEFINE_TYPE(VipsForeignSavePsdLayersFile, vips_foreign_save_psd_layers_file, vips_foreign_save_psd_layers_get_type()) + +static int vips_foreign_save_psd_layers_file_build(VipsObject *object) +{ + VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; + VipsForeignSavePsdLayersFile *file = (VipsForeignSavePsdLayersFile *)object; + if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; + return VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_file_parent_class)->build(object); +} + +static void vips_foreign_save_psd_layers_file_class_init(VipsForeignSavePsdLayersFileClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + + vips__foreign_psd_class_properties(class); + object_class->nickname = "psdsave_layers"; + object_class->description = "save images as the layers of a PSD or PSB to a file with minipsd"; + object_class->build = vips_foreign_save_psd_layers_file_build; + + PSD_ARGS_BEGIN + VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdLayersFile, filename), NULL); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_layers_file_init(VipsForeignSavePsdLayersFile *file) +{ + (void)file; +} + +typedef struct _VipsForeignSavePsdLayersBuffer { + VipsForeignSavePsdLayers parent_object; + VipsArea *buf; +} VipsForeignSavePsdLayersBuffer; + +typedef VipsForeignSavePsdLayersClass VipsForeignSavePsdLayersBufferClass; + +G_DEFINE_TYPE(VipsForeignSavePsdLayersBuffer, vips_foreign_save_psd_layers_buffer, vips_foreign_save_psd_layers_get_type()) + +static int vips_foreign_save_psd_layers_buffer_build(VipsObject *object) +{ + VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; + VipsBlob *blob; + if (!(psd->target = vips_target_new_to_memory())) return -1; + if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_buffer_parent_class)->build(object)) return -1; + g_object_get(psd->target, "blob", &blob, NULL); + g_object_set(object, "buffer", blob, NULL); + vips_area_unref(VIPS_AREA(blob)); + return 0; +} + +static void vips_foreign_save_psd_layers_buffer_class_init(VipsForeignSavePsdLayersBufferClass *class) +{ + VipsObjectClass *object_class = (VipsObjectClass *)class; + + vips__foreign_psd_class_properties(class); + object_class->nickname = "psdsave_layers_buffer"; + object_class->description = "save images as the layers of a PSD or PSB to a buffer with minipsd"; + object_class->build = vips_foreign_save_psd_layers_buffer_build; + + PSD_ARGS_BEGIN + VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, + G_STRUCT_OFFSET(VipsForeignSavePsdLayersBuffer, buf), VIPS_TYPE_BLOB); + PSD_ARGS_END +} + +static void vips_foreign_save_psd_layers_buffer_init(VipsForeignSavePsdLayersBuffer *buffer) +{ + (void)buffer; +} + +/* --- The C calls, as libvips has one beside each operation ---------------------------------- */ + +int vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) +{ + VipsArrayImage *array = vips_array_image_new(in, n); + va_list ap; + int result; + va_start(ap, filename); + result = vips_call_split("psdsave_layers", ap, array, filename); + va_end(ap); + vips_area_unref(VIPS_AREA(array)); + return result; +} + +int vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) +{ + VipsArrayImage *array = vips_array_image_new(in, n); + va_list ap; + int result; + va_start(ap, target); + result = vips_call_split("psdsave_layers_target", ap, array, target); + va_end(ap); + vips_area_unref(VIPS_AREA(array)); + return result; +} + +int vips_psdsave_layers_buffer(VipsImage **in, int n, void **buf, size_t *len, ...) +{ + VipsArrayImage *array = vips_array_image_new(in, n); + VipsArea *area = NULL; + va_list ap; + int result; + va_start(ap, len); + result = vips_call_split("psdsave_layers_buffer", ap, array, &area); + va_end(ap); + vips_area_unref(VIPS_AREA(array)); + if (!result && area) { /* the bytes handed over, as libvips' own _buffer calls do */ + if (buf) { + *buf = area->data; + area->free_fn = NULL; + } + if (len) *len = area->length; + vips_area_unref(area); + } + return result; +} diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index e311aaa..b565d1a 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -22,5 +22,8 @@ int minipsd_vips_init(void) (void)vips_foreign_save_psd_file_get_type(); (void)vips_foreign_save_psd_buffer_get_type(); (void)vips_foreign_save_psd_target_get_type(); + (void)vips_foreign_save_psd_layers_file_get_type(); + (void)vips_foreign_save_psd_layers_buffer_get_type(); + (void)vips_foreign_save_psd_layers_target_get_type(); return 0; } diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index bf88a18..e194b47 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -6,7 +6,8 @@ * libvips' prebuilt Windows builds load no modules, so a program registers them itself: call * minipsd_vips_init() once, after VIPS_INIT(). vips_image_new_from_file("x.psd"), * vips_image_write_to_file(image, "x.psd") and the other ways libvips finds a loader or a saver - * then find these. + * then find these. A saver writes one image; a layered document is psdsave_layers, an + * operation over an array of images, one a layer (vips_psdsave_layers below). * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha @@ -15,6 +16,8 @@ #ifndef MINIPSD_VIPS_H #define MINIPSD_VIPS_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -40,6 +43,35 @@ typedef enum { */ int minipsd_vips_init(void); +/** + * \brief Saves images as the layers of one PSD or PSB (the operation psdsave_layers). + * + * in holds the layers, bottom first. Each image's psd-layer-* fields, the ones psdload sets, + * give its layer's name ("psd-layer-name", else "Layer N"), opacity ("psd-layer-opacity", 0 to + * 255), visibility ("psd-layer-hidden"), blend mode ("psd-layer-blend-mode", a four-character + * key such as "mul ") and clipping ("psd-layer-clipping"); its position is its xoffset and + * yoffset. "psd-layer-empty" set makes it an empty layer, no pixels, as Photoshop writes an + * unpainted one (the image's pixels are ignored). Every layer with pixels is of one mode and + * depth (grey, RGB or CMYK, with or without alpha; uchar, ushort or float); none is converted. + * + * Options, as NULL-ended name and value pairs: "composite" (a VipsImage, the canvas's size: the + * composite as given; else vips_composite of the visible layers, blend modes mapped as the spec's + * table, flattened against white when no layer has alpha), "compression" + * (VipsForeignPsdCompression, the layers'), "psb", "width" and "height" (the canvas; 0: the + * composite's size, else every layer's rectangle from (left, top)), "left" and "top" (where the + * canvas lies in the layers' coordinates). Each image is read once for its layer, and once more + * by vips_composite unless the composite is given. + * + * \return 0, or -1 with libvips' error set. + */ +int vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) G_GNUC_NULL_TERMINATED; + +/** \brief vips_psdsave_layers into a target. */ +int vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) G_GNUC_NULL_TERMINATED; + +/** \brief vips_psdsave_layers into a new buffer, *buf (g_free it) of *len bytes. */ +int vips_psdsave_layers_buffer(VipsImage **in, int n, void **buf, size_t *len, ...) G_GNUC_NULL_TERMINATED; + #ifdef __cplusplus } #endif diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index a452d5a..403b089 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -58,6 +58,7 @@ #define STORED_ZIP_PREDICTED 3 #define WIDE_W 16400 /* 32-bit noise rows longer than a PSD's two-byte counts hold */ #define WIDE_H 2 +#define COMPOSED_LEVELS 2 /* vips_composite's float rounding, doubled where the matte is undone */ #define PSD_RGB_BANDS 3 #define PSD_VERSION_AT 5 /* the header's version, 1 PSD or 2 PSB: its low byte */ @@ -159,9 +160,9 @@ static void invert_cmyk(uint8_t *pixels, size_t count, unsigned bands, size_t sa /* The composite (layer -1) or a layer as minipsd decodes it, laid out as the loader gives it: interleaved, the colour channels and transparency (a layer's always) up to MINIPSD_PLANES, - bitmap's black 0 and white 255, indexed through its palette, CMYK's ink 0 for none. NULL with - the error in *rc when - minipsd refuses it, or MINIPSD_ERR_SIZE for an empty layer, which the loader refuses too. */ + bitmap's black 0 and white 255, indexed through its palette, CMYK's ink 0 for none; an empty + layer as one pixel of zeros, as the loader gives it. NULL with the error in *rc when minipsd + refuses it. */ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *bytes, int *rc) { uint8_t workspace[WORKSPACE_SIZE], *pixels, *scratch = NULL, *p; @@ -175,7 +176,7 @@ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *b uint32_t width, height; unsigned planes, bands; uint16_t index; - int transparent = -1, n; + int transparent = -1, n, empty = 0; if ((*rc = minipsd_reader_workspace_size(&need)) != MINIPSD_OK) return NULL; if (need > sizeof workspace) { *rc = MINIPSD_ERR_ARG; @@ -192,12 +193,9 @@ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *b if (*rc == MINIPSD_OK) *rc = MINIPSD_ERR_NOT_FOUND; return NULL; } - if (view.right <= view.left || view.bottom <= view.top) { - *rc = MINIPSD_ERR_SIZE; - return NULL; - } - width = (uint32_t)(view.right - view.left); - height = (uint32_t)(view.bottom - view.top); + empty = view.right <= view.left || view.bottom <= view.top; /* no pixels: one, transparent */ + width = empty ? 1 : (uint32_t)(view.right - view.left); + height = empty ? 1 : (uint32_t)(view.bottom - view.top); planes = info.color_channels; /* then transparency, when the layer has its channel */ if (planes < MINIPSD_PLANES && view.channel[planes].size) ++planes; } @@ -215,6 +213,11 @@ static uint8_t *decode_image(const void *file, size_t size, int layer, size_t *b *rc = MINIPSD_ERR_ALLOC; return NULL; } + if (empty) { + memset(pixels, 0, *bytes); + *rc = MINIPSD_OK; + return pixels; + } minipsd_target_interleaved(&target, pixels, row, planes, info.depth); if (info.color_mode == MINIPSD_MODE_INDEXED) target.channel[0].pixel_step = (ptrdiff_t)bands; memset(&opts, 0, sizeof opts); @@ -734,6 +737,202 @@ static void check_list(const char *list) files, decoded, layers_decoded, layers); } +/* An image's pixels whole, or NULL. */ +static uint8_t *pixels_of(VipsImage *image, size_t *bytes) +{ + return image ? (uint8_t *)vips_image_write_to_memory(image, bytes) : NULL; +} + +/* The bytes of a file, as written by minipsd_write; 0 on failure. */ +static int write_file(const minipsd_doc *doc, const char *path) +{ + out_buffer b = { NULL, 0, 0 }; + FILE *f = NULL; + int ok = minipsd_write(doc, NULL, to_buffer, &b) == MINIPSD_OK && (f = fopen(path, "wb")) != NULL; + if (ok) ok = fwrite(b.data, 1, b.size, f) == b.size; + if (f && fclose(f) != 0) ok = 0; + free(b.data); + return ok; +} + +/* Two files the same, byte for byte. */ +static int same_files(const char *a, const char *b) +{ + size_t na = 0, nb = 0; + uint8_t *fa = read_all(a, &na), *fb = read_all(b, &nb); + const int same = fa && fb && na == nb && memcmp(fa, fb, na) == 0; + free(fa); + free(fb); + return same; +} + +/* An empty layer as psdsave_layers takes it, and psdload gives it: one pixel, psd-layer-empty. */ +static VipsImage *empty_layer(const char *name) +{ + VipsImage *black, *one = NULL; + if (vips_black(&black, 1, 1, "bands", RGBA, NULL)) return NULL; + if (vips_copy(black, &one, "interpretation", VIPS_INTERPRETATION_sRGB, NULL)) one = NULL; + g_object_unref(black); + if (one) { + vips_image_set_int(one, "psd-layer-empty", 1); + vips_image_set_string(one, "psd-layer-name", name); + } + return one; +} + +/* psdsave_layers: a layered document from images, each a layer. Platemaker's template (a layer + and an empty one above) byte for byte as minipsd_write makes it from the same layers; its + composite made by libvips, the layer's own; a document minipsd wrote, every layer loaded with + psdload and saved, the same bytes back (position, opacity, blend mode, clipping, an empty + layer); the canvas placed by left and top, or sized from the layers; what it refuses. */ +static void check_layers_saved(const uint8_t *rgba, const char *dir) +{ + char path[PATH_MAX_SIZE], again[PATH_MAX_SIZE]; + VipsImage *template_image = make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), *empty = empty_layer("Layer"); + VipsImage *pair[2], *back = NULL, *given = NULL, *made = NULL; + minipsd_layer layers[3]; + minipsd_doc doc; + size_t given_bytes = 0, made_bytes = 0, i; + uint8_t *a, *b; + int k; + + if (!template_image || !empty) { + check(0, "images made", "psdsave_layers"); + return; + } + vips_image_set_string(template_image, "psd-layer-name", "Template"); + pair[0] = template_image; + pair[1] = empty; + + /* Platemaker's template, its composite given (the template itself: the layer above is empty). */ + snprintf(path, sizeof path, "%s/layers-template.psd", dir); + check(vips_psdsave_layers(pair, 2, path, "composite", template_image, NULL) == 0, "saved", path); + memset(layers, 0, sizeof layers); + memset(&doc, 0, sizeof doc); + layers[0].name = "Template"; + layers[0].pixels = rgba; + layers[0].opacity = UCHAR_MAX; + layers[1].name = "Layer"; + layers[1].opacity = UCHAR_MAX; + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = layers, doc.layer_count = 2, doc.composite = rgba, doc.composite_alpha = 1; + snprintf(again, sizeof again, "%s/layers-template-minipsd.psd", dir); + check(write_file(&doc, again) && same_files(path, again), "the bytes minipsd_write makes from the same layers", path); + + /* Its composite made by libvips from the layers: the template's own pixels. */ + snprintf(again, sizeof again, "%s/layers-template-composed.psd", dir); + check(vips_psdsave_layers(pair, 2, again, NULL) == 0, "saved, composite composed", again); + given = vips_image_new_from_file(path, NULL); + made = vips_image_new_from_file(again, NULL); + a = pixels_of(given, &given_bytes); + b = pixels_of(made, &made_bytes); + { /* libvips composes in floats: exact where alpha is full; within a level, doubled by the + matte undone, where it is partial */ + size_t j, off = 0; + for (j = 0; a && b && given_bytes == made_bytes && j < given_bytes; ++j) { + const unsigned alpha = a[j - j % RGBA + 3], d = (unsigned)abs((int)a[j] - (int)b[j]); + if (alpha == UCHAR_MAX ? d != 0 : d > COMPOSED_LEVELS) ++off; + } + check(a && b && given_bytes == made_bytes && off == 0, "composite composed by libvips: the layer's", again); + } + g_free(a); + g_free(b); + VIPS_UNREF(given); + VIPS_UNREF(made); + + /* The empty layer loaded: one transparent pixel, psd-layer-empty, its name. */ + back = vips_image_new_from_file(path, "layer", 1, NULL); + { + const char *name = NULL; + check(back && back->Xsize == 1 && back->Ysize == 1 && vips_image_get_int(back, "psd-layer-empty", &k) == 0 && k == 1 + && vips_image_get_string(back, "psd-layer-name", &name) == 0 && name && strcmp(name, "Layer") == 0, + "an empty layer loads as one pixel, psd-layer-empty", path); + } + VIPS_UNREF(back); + + /* A document minipsd writes: an opaque base, a smaller layer placed, half opaque, multiplied, + clipped, and an empty layer; each loaded with psdload and saved back: the same bytes. */ + { + enum { SMALL_W = 50, SMALL_H = 40, SMALL_X = 30, SMALL_Y = 20 }; + uint8_t *base = (uint8_t *)malloc(PIXELS * RGBA), *small = (uint8_t *)malloc(SMALL_W * SMALL_H * RGBA); + VipsImage *loaded[3] = { NULL, NULL, NULL }, *composite = NULL; + memset(layers, 0, sizeof layers); + memset(&doc, 0, sizeof doc); + if (base && small) { + memcpy(base, rgba, PIXELS * RGBA); + for (i = 0; i < PIXELS; ++i) base[i * RGBA + 3] = UCHAR_MAX; /* opaque: the composite comes back exactly */ + for (i = 0; i < SMALL_W * SMALL_H * RGBA; ++i) small[i] = (uint8_t)(i * 11 + 3); + layers[0].name = "Base", layers[0].pixels = base, layers[0].opacity = UCHAR_MAX; + layers[1].name = "Clip", layers[1].pixels = small, layers[1].opacity = HALF_OPAQUE, layers[1].clipping = 1; + layers[1].top = SMALL_Y, layers[1].left = SMALL_X, layers[1].bottom = SMALL_Y + SMALL_H, layers[1].right = SMALL_X + SMALL_W; + memcpy(layers[1].blend_mode, "mul ", 4); + layers[2].name = "Empty", layers[2].opacity = UCHAR_MAX, layers[2].hidden = 1; + doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; + doc.layers = layers, doc.layer_count = 3, doc.composite_alpha = 1; + snprintf(path, sizeof path, "%s/layers-minipsd.psd", dir); + check(write_file(&doc, path), "written by minipsd", path); + for (k = 0; k < 3; ++k) loaded[k] = vips_image_new_from_file(path, "layer", k, NULL); + composite = vips_image_new_from_file(path, NULL); + snprintf(again, sizeof again, "%s/layers-resaved.psd", dir); + check(loaded[0] && loaded[1] && loaded[2] && composite + && vips_psdsave_layers(loaded, 3, again, "composite", composite, NULL) == 0, + "every layer loaded and saved", again); + check(same_files(path, again), "the same bytes back: names, position, opacity, blend mode, clipping, empty", again); + vips_error_clear(); + } + for (k = 0; k < 3; ++k) VIPS_UNREF(loaded[k]); + VIPS_UNREF(composite); + free(base); + free(small); + } + + /* The canvas: placed by left and top, sized from the layers without width and height. */ + { + enum { AT_X = 7, AT_Y = 3 }; + VipsImage *moved = NULL, *one[1]; + uint8_t workspace[WORKSPACE_SIZE], *file; + size_t size = 0; + minipsd_reader *r; + minipsd_info info; + minipsd_layer_view view; + if (vips_copy(template_image, &moved, "xoffset", AT_X, "yoffset", AT_Y, NULL)) moved = NULL; + one[0] = moved; + snprintf(path, sizeof path, "%s/layers-canvas.psd", dir); + check(moved && vips_psdsave_layers(one, 1, path, "left", AT_X, "top", AT_Y, NULL) == 0, "saved, canvas placed", path); + file = read_all(path, &size); + check(file && minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK + && info.width == WIDTH && info.height == HEIGHT && minipsd_reader_next_layer(r, &view) == MINIPSD_MORE + && view.left == 0 && view.top == 0, + "the canvas at (left, top): the layer at its corner", path); + free(file); + snprintf(path, sizeof path, "%s/layers-grown.psd", dir); + check(moved && vips_psdsave_layers(one, 1, path, NULL) == 0, "saved, canvas from the layers", path); + file = read_all(path, &size); + check(file && minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK + && info.width == WIDTH + AT_X && info.height == HEIGHT + AT_Y, + "the canvas from (0, 0), every layer in it", path); + free(file); + VIPS_UNREF(moved); + } + + /* Refused: no layers; a layer of another depth; a blend mode that is not a key. */ + { + VipsImage *deep = NULL, *mixed[2]; + snprintf(path, sizeof path, "%s/layers-refused.psd", dir); + check(vips_psdsave_layers(pair, 0, path, NULL) != 0, "no layers refused", path); + if (vips_cast(template_image, &deep, VIPS_FORMAT_USHORT, NULL)) deep = NULL; + mixed[0] = template_image; + mixed[1] = deep; + check(deep && vips_psdsave_layers(mixed, 2, path, NULL) != 0, "a layer of another depth refused", path); + vips_image_set_string(template_image, "psd-layer-blend-mode", "mul"); + check(vips_psdsave_layers(pair, 2, path, NULL) != 0, "a blend mode of three characters refused", path); + vips_error_clear(); + VIPS_UNREF(deep); + } + g_object_unref(template_image); + g_object_unref(empty); +} + int main(int argc, char **argv) { static const uint8_t icc[] = "not a real ICC profile"; @@ -988,6 +1187,8 @@ int main(int argc, char **argv) if (image) g_object_unref(image); } + check_layers_saved(rgba, argv[1]); + /* Not a PSD: psdload does not claim it. */ loader = vips_foreign_find_load_buffer(junk, sizeof junk); check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); From e8326f91c5055088f27641db6cd80b0738b00d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 23:09:00 +0100 Subject: [PATCH 18/22] feat(vips): masks and groups through psdload and psdsave_layers A layer's mask travels as an image in its psd-layer-mask field: one band of the layers' depth, its own rectangle in xoffset and yoffset, with psd-layer-mask-default-color and psd-layer-mask-disabled. A group travels as records in the list, as the file has them: below its layers an image with psd-layer-section "end", above them one with "open" or "closed" carrying the group's name, visibility, opacity and mode. psdload gives both so, the top record's mode from its 'lsct' block, and psdsave_layers writes them back: a document with a group, a clipped layer with a mask and an empty layer comes back the same bytes. The composite psdsave_layers makes now hides a hidden group's layers, multiplies their opacity by the group's, and multiplies a layer's alpha by its mask. Clipping, a group's own mode and its mask are not drawn. --- libvips-plugin/foreign/psd.c | 2 + libvips-plugin/foreign/psd.h | 6 + libvips-plugin/foreign/psdload.c | 104 +++++++++++++-- libvips-plugin/foreign/psdsave.c | 219 ++++++++++++++++++++++++------- libvips-plugin/minipsd_vips.h | 10 +- libvips-plugin/test_vips.c | 124 ++++++++++++++--- 6 files changed, 387 insertions(+), 78 deletions(-) diff --git a/libvips-plugin/foreign/psd.c b/libvips-plugin/foreign/psd.c index e34d176..0076c1a 100644 --- a/libvips-plugin/foreign/psd.c +++ b/libvips-plugin/foreign/psd.c @@ -13,6 +13,8 @@ const char *vips__foreign_psd_suffs[] = { ".psd", ".psb", NULL }; +const char *const vips__foreign_psd_sections[PSD_SECTIONS] = { NULL, "open", "closed", "end" }; + void vips__foreign_psd_class_properties(void *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); diff --git a/libvips-plugin/foreign/psd.h b/libvips-plugin/foreign/psd.h index f95d9e3..1e7d3f4 100644 --- a/libvips-plugin/foreign/psd.h +++ b/libvips-plugin/foreign/psd.h @@ -37,6 +37,12 @@ /* The suffixes libvips matches files by: .psd and .psb. */ extern const char *vips__foreign_psd_suffs[]; +/* A record's psd-layer-section, by its MINIPSD_SECTION_* type: NULL for a layer, then "open", + "closed" (a group's top record, shown open or closed) and "end" (the divider below a group's + layers). psdload sets it, psdsave_layers reads it. */ +#define PSD_SECTIONS 4 +extern const char *const vips__foreign_psd_sections[PSD_SECTIONS]; + /* GObject resets set_property and get_property in every class (g_object_base_class_init), so each class with arguments of its own sets libvips' again. */ void vips__foreign_psd_class_properties(void *class); diff --git a/libvips-plugin/foreign/psdload.c b/libvips-plugin/foreign/psdload.c index 8389deb..6c5f6cd 100644 --- a/libvips-plugin/foreign/psdload.c +++ b/libvips-plugin/foreign/psdload.c @@ -11,7 +11,9 @@ * * The image is the composite, or with `layer` one layer (its position in xoffset and yoffset), as * the file stores it, no colour converted: the mode's colour channels, then transparency where it - * has one (a layer's channel, the composite's). What libvips has no interpretation for (Lab, + * has one (a layer's channel, the composite's). A layer's look comes in psd-layer-* fields + * (name, opacity, hidden, blend mode, clipping, empty; a group record's psd-layer-section; its + * mask, an image, in psd-layer-mask), the ones psdsave_layers takes back. What libvips has no interpretation for (Lab, * multichannel) is MULTIBAND; the colour mode data is kept as "psd-color-mode-data". Indexed is * the exception: libvips has no indexed image, so it comes through its palette as sRGB, as * libvips' own loaders give PNG, GIF and TIFF palettes (a lookup, no colour converted). CMYK @@ -41,10 +43,15 @@ #define PSD_SIGNATURE_SIZE 4 #define PSD_SNIFF_SIZE 6 /* the signature and the version (1 PSD, 2 PSB), big-endian */ #define PSD_UNIT_CM 2 /* resolution info's hResUnit: pixels per cm, shown in cm */ +#define PSD_MARKER_SIZE 2 /* a stored channel's compression marker, big-endian */ #define PSD_STORED_ZIP 2 /* a channel's stored compression marker: ZIP, and ... */ #define PSD_STORED_ZIP_PRED 3 /* ... ZIP with prediction */ #define PSD_PALETTE_ENTRIES 256 /* an indexed file's palette: 256 reds, 256 greens, 256 blues */ #define PSD_COMPOSITE (-1) /* the `layer` option's default: the composite */ +#define PSD_LSCT_MODE 8 /* an 'lsct' block: the type, "8BIM", then the group's mode ... */ +#define PSD_LSCT_FULL 12 /* ... which ends here */ +#define PSD_MODE_SIZE 4 /* a blend mode's key */ +#define PSD_MASK_DISABLED 0x02 /* a mask's flag: disabled */ /* --- The base class: a PSD read from a source --------------------------------------------- */ @@ -162,11 +169,84 @@ static size_t vips_foreign_load_psd_scratch_size(const VipsForeignLoadPsd *psd) return zip ? psd->planes * psd->height * row : 0; } +/* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ +static int vips_foreign_load_psd_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) +{ + uLongf n = (uLongf)dst_size; + (void)user; + if ((uLong)src_size != src_size || n != dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ + return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; +} + +/* A group's own blend mode, from its 'lsct' block, into mode (a C string); 0 without one. The + record's own key says "norm" for a group that passes through. */ +static int vips_foreign_load_psd_group_mode(const minipsd_layer_view *view, char mode[PSD_MODE_SIZE + 1]) +{ + minipsd_block block; + size_t pos = 0; + while (minipsd_layer_next_block(view, &pos, &block) == MINIPSD_MORE) + if (memcmp(block.key, "lsct", PSD_MODE_SIZE) == 0 && block.data.size >= PSD_LSCT_FULL) { + memcpy(mode, block.data.data + PSD_LSCT_MODE, PSD_MODE_SIZE); + mode[PSD_MODE_SIZE] = '\0'; + return 1; + } + return 0; +} + +/* The layer's mask as an image on out: "psd-layer-mask", one band of the file's depth, its own + rectangle in xoffset and yoffset (one pixel with psd-layer-empty for a rectangle without area), + its colour outside in "psd-layer-mask-default-color", "psd-layer-mask-disabled". Decoded + whole: a mask is one band. */ +static int vips_foreign_load_psd_mask(VipsForeignLoadPsd *psd, VipsImage *out) +{ + const minipsd_mask *m = &psd->view.mask; + const int empty = m->right <= m->left || m->bottom <= m->top; + const uint32_t width = empty ? 1 : (uint32_t)(m->right - m->left), height = empty ? 1 : (uint32_t)(m->bottom - m->top); + const VipsBandFormat format = vips_foreign_load_psd_format(&psd->info); + const size_t row = (size_t)width * (size_t)vips_format_sizeof(format), bytes = row * height; + const int zip = m->stored.size >= PSD_MARKER_SIZE && m->stored.data + && (m->stored.data[1] == PSD_STORED_ZIP || m->stored.data[1] == PSD_STORED_ZIP_PRED); + uint8_t *pixels = (uint8_t *)g_try_malloc0(bytes), *scratch = zip ? (uint8_t *)g_try_malloc(bytes) : NULL; + VipsImage *image = NULL; + minipsd_decode_opts opts; + minipsd_plane plane; + int rc = !pixels || (zip && !scratch) ? MINIPSD_ERR_ALLOC : MINIPSD_OK; + + if (rc == MINIPSD_OK && !empty) { + memset(&opts, 0, sizeof opts); + opts.depth = psd->info.depth; + opts.inflate = vips_foreign_load_psd_inflate; + opts.scratch = scratch; + opts.scratch_size = zip ? bytes : 0; + plane.data = pixels; + plane.pixel_step = (ptrdiff_t)vips_format_sizeof(format); + plane.row_step = (ptrdiff_t)row; + rc = minipsd_decode_mask(psd->reader, &psd->view, &plane, &opts); + } + if (rc == MINIPSD_OK && !(image = vips_image_new_from_memory_copy(pixels, bytes, (int)width, (int)height, 1, format))) + rc = MINIPSD_ERR_ALLOC; + g_free(pixels); + g_free(scratch); + if (rc != MINIPSD_OK) { + vips_foreign_load_psd_error(psd, minipsd_error_string(rc)); + return -1; + } + image->Type = psd->info.depth == 16 ? VIPS_INTERPRETATION_GREY16 : VIPS_INTERPRETATION_B_W; + image->Xoffset = m->left; + image->Yoffset = m->top; + vips_image_set_int(image, "psd-layer-empty", empty); + vips_image_set_image(out, "psd-layer-mask", image); + g_object_unref(image); + vips_image_set_int(out, "psd-layer-mask-default-color", m->default_color); + vips_image_set_int(out, "psd-layer-mask-disabled", (m->flags & PSD_MASK_DISABLED) != 0); + return 0; +} + /* The layer the option names: its record, its size, its name and look as metadata. */ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) { VipsImage *out = ((VipsForeignLoad *)psd)->out; - char *name; + char *name, mode[PSD_MODE_SIZE + 1]; size_t needed = 0; int rc, i; for (i = 0; i <= psd->layer; ++i) @@ -189,12 +269,19 @@ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) && (name = (char *)vips_malloc(VIPS_OBJECT(psd), needed)) && minipsd_layer_name(&psd->view, name, needed, &needed) == MINIPSD_OK) vips_image_set_string(out, "psd-layer-name", name); - vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); + /* A group's top record: its own mode (pass through, or one it is composed in). */ + if ((psd->view.section_divider_type == MINIPSD_SECTION_GROUP || psd->view.section_divider_type == MINIPSD_SECTION_GROUP_CLOSED) + && vips_foreign_load_psd_group_mode(&psd->view, mode)) + vips_image_set_string(out, "psd-layer-blend-mode", mode); + else + vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); vips_image_set_int(out, "psd-layer-opacity", psd->view.opacity); vips_image_set_int(out, "psd-layer-hidden", psd->view.hidden); vips_image_set_int(out, "psd-layer-clipping", psd->view.clipping); vips_image_set_int(out, "psd-layer-empty", psd->empty); - return 0; + if (psd->view.section_divider_type && psd->view.section_divider_type < PSD_SECTIONS) + vips_image_set_string(out, "psd-layer-section", vips__foreign_psd_sections[psd->view.section_divider_type]); + return psd->view.has_mask && psd->info.depth >= CHAR_BIT ? vips_foreign_load_psd_mask(psd, out) : 0; } static int vips_foreign_load_psd_header(VipsForeignLoad *load) @@ -275,15 +362,6 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) return 0; } -/* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ -static int vips_foreign_load_psd_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) -{ - uLongf n = (uLongf)dst_size; - (void)user; - if ((uLong)src_size != src_size || n != dst_size) return 1; /* past zlib's uLong: 32 bits on Windows */ - return uncompress(dst, &n, src, (uLong)src_size) != Z_OK || n != dst_size; -} - /* A thread's sequence: the scratch a ZIP layer's channels stay inflated in, between its bands. */ typedef struct vips_foreign_load_psd_sequence { uint8_t *scratch; diff --git a/libvips-plugin/foreign/psdsave.c b/libvips-plugin/foreign/psdsave.c index 566bcda..49b5e67 100644 --- a/libvips-plugin/foreign/psdsave.c +++ b/libvips-plugin/foreign/psdsave.c @@ -440,6 +440,20 @@ static minipsd_bytes vips_foreign_save_psd_composite_block(VipsForeignSavePsdCha return vips_foreign_save_psd_block(c->channel, c->planes, c->height, count, c->compression); } +/* One image read once into c, encoded by compression; a composite's joined as the file stores it. + -1 on failure, *memory set when memory ran out (else the image's own error, reported). */ +static int vips_foreign_save_psd_encode_image(VipsForeignSavePsdChannels *c, VipsImage *in, int depth, int mode, int compression, + int composite, int matte, int *memory) +{ + *memory = 1; + if (vips_foreign_save_psd_channels_init(c, in, depth, compression, composite, matte)) return -1; + if (vips_foreign_save_psd_pass(in, depth, mode, &c, 1, memory)) return -1; + *memory = 1; + if (vips_foreign_save_psd_finish(c)) return -1; + *memory = 0; + return 0; +} + /* Whether a PSD holds the document: its layer section within the 4 GiB its length counts (minipsd refuses one past it, MINIPSD_ERR_SIZE), the file within PSD_MAX_FILE. Any other refusal is minipsd_write's to report. */ @@ -757,6 +771,9 @@ static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) one. The composite is given, or made by vips_composite from the visible layers. */ #define PSD_BLEND_KEY_SIZE 4 /* a blend mode's key: four characters, such as "mul " */ +#define PSD_COMPOSE_STEPS 9 /* the images vips_foreign_save_psd_compose makes for a layer */ +#define PSD_MASK_DISABLED 0x02 /* a mask's flag: disabled */ +#define PSD_DIVIDER_NAME "" /* a divider's name, as Photoshop names it */ typedef struct _VipsForeignSavePsdLayers { VipsOperation parent_object; @@ -787,9 +804,30 @@ static int vips_foreign_save_psd_field(VipsImage *image, const char *name, int f return vips_image_get_typeof(image, name) && !vips_image_get_int(image, name, &value) ? value : fallback; } +/* A record's MINIPSD_SECTION_* from its psd-layer-section: a layer without one; -1 for another + text. */ +static int vips_foreign_save_psd_section(VipsImage *image) +{ + const char *text; + int k; + if (!vips_image_get_typeof(image, "psd-layer-section") || vips_image_get_string(image, "psd-layer-section", &text)) + return MINIPSD_SECTION_LAYER; + for (k = MINIPSD_SECTION_GROUP; k < PSD_SECTIONS; ++k) + if (strcmp(text, vips__foreign_psd_sections[k]) == 0) return k; + return -1; +} + +/* No pixels: an empty layer, or a group's record. */ static int vips_foreign_save_psd_empty(VipsImage *image) { - return vips_foreign_save_psd_field(image, "psd-layer-empty", 0) != 0; + return vips_foreign_save_psd_field(image, "psd-layer-empty", 0) != 0 || vips_foreign_save_psd_section(image) != MINIPSD_SECTION_LAYER; +} + +/* The image's mask (a new reference), or NULL without one. */ +static VipsImage *vips_foreign_save_psd_mask(VipsImage *image) +{ + VipsImage *mask; + return vips_image_get_typeof(image, "psd-layer-mask") && !vips_image_get_image(image, "psd-layer-mask", &mask) ? mask : NULL; } /* A PSD blend key as a libvips blend mode, as the spec's table maps them (Working with libvips); @@ -814,26 +852,56 @@ static VipsBlendMode vips_foreign_save_psd_blend_mode(const char key[PSD_BLEND_K return VIPS_BLEND_MODE_OVER; } +/* Each layer's visibility and opacity (0 to 1) with its groups': a hidden group hides its layers, + its opacity multiplies theirs. Walked top down: a group's top record comes before its layers, + its divider after them. */ +static void vips_foreign_save_psd_groups(VipsImage **in, int n, int *hidden, double *opacity) +{ + int *group_hidden = g_new0(int, (gsize)n + 1), level = 0, i; + double *group_opacity = g_new(double, (gsize)n + 1); + group_opacity[0] = 1.0; + for (i = n - 1; i >= 0; --i) { + const int section = vips_foreign_save_psd_section(in[i]); + const int own = vips_foreign_save_psd_field(in[i], "psd-layer-hidden", 0) != 0; + const double alpha = VIPS_CLIP(0, vips_foreign_save_psd_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX) / (double)UCHAR_MAX; + hidden[i] = group_hidden[level] || own; + opacity[i] = group_opacity[level] * alpha; + if (section == MINIPSD_SECTION_GROUP || section == MINIPSD_SECTION_GROUP_CLOSED) { + ++level; + group_hidden[level] = hidden[i]; + group_opacity[level] = opacity[i]; + } else if (section == MINIPSD_SECTION_GROUP_END && level > 0) { + --level; + } + } + g_free(group_hidden); + g_free(group_opacity); +} + /* The visible layers composed by vips_composite onto a transparent canvas of width x height, at - their positions less (left, top), their opacity in their alpha, their blend modes mapped; with - no layer's transparency, flattened against white (as minipsd mattes a composite without it). - Its bands and format are the layers'. Clipping, and a blend mode libvips lacks, are drawn as - normal. NULL with an error. */ + their positions less (left, top), their opacity (with their groups') and their mask in their + alpha, their blend modes mapped; with no layer's transparency, flattened against white (as + minipsd mattes a composite without it). Its bands and format are the layers'. Clipping, a + group's blend mode and mask, and a blend mode libvips lacks are drawn as normal (pass through + for a group). NULL with an error. */ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, VipsImage **in, int n, const VipsImage *ref, size_t colors, int mode, int depth, int width, int height, int alpha) { const double max = depth == 32 ? 1.0 : depth == 16 ? (double)UINT16_MAX : (double)UCHAR_MAX; const int bands = (int)colors + 1; - VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(psd), 3 * n + 4); + VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(psd), PSD_COMPOSE_STEPS * n + 3); VipsImage **layers = (VipsImage **)vips_object_local_array(VIPS_OBJECT(psd), n + 1); /* the canvas, then each */ int *modes = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)(n + 1) * sizeof(int)); int *xs = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)(n + 1) * sizeof(int)); int *ys = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)(n + 1) * sizeof(int)); - double white[PSD_MAX_PLANES], scale[PSD_MAX_PLANES], offset[PSD_MAX_PLANES]; + int *hidden = (int *)vips_malloc(VIPS_OBJECT(psd), (size_t)n * sizeof(int)); + double *opacity = (double *)vips_malloc(VIPS_OBJECT(psd), (size_t)n * sizeof(double)); + double white[PSD_MAX_PLANES]; VipsImage *out, *composed = NULL; int count = 0, i, k, rc; - if (!modes || !xs || !ys) return NULL; + if (!modes || !xs || !ys || !hidden || !opacity) return NULL; + vips_foreign_save_psd_groups(in, n, hidden, opacity); if (mode == MINIPSD_MODE_CMYK && depth != 8) { /* libvips takes CMYK's alpha as 8-bit */ vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", "a 16-bit CMYK document needs its composite given"); return NULL; @@ -844,23 +912,46 @@ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, V return NULL; ++count; for (i = 0; i < n; ++i) { /* bottom first, as minipsd and vips_composite both list them */ - VipsImage *layer = in[i]; - char key[PSD_BLEND_KEY_SIZE] = { 'n', 'o', 'r', 'm' }; + VipsImage *layer = in[i], *alpha_band = NULL, *mask; + VipsImage **s = t + 2 + PSD_COMPOSE_STEPS * i; /* this layer's steps */ + char key[PSD_BLEND_KEY_SIZE] = { 'n', 'o', 'r', 'm' }; const char *given; - const int opacity = VIPS_CLIP(0, vips_foreign_save_psd_field(layer, "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX); - if (vips_foreign_save_psd_empty(layer) || vips_foreign_save_psd_field(layer, "psd-layer-hidden", 0)) continue; + if (vips_foreign_save_psd_empty(layer) || hidden[i]) continue; if (layer->Bands == (int)colors) { /* without transparency: opaque */ - if (vips_bandjoin_const1(layer, &t[2 + 3 * i], max, NULL)) return NULL; - layer = t[2 + 3 * i]; + if (vips_bandjoin_const1(layer, &s[0], max, NULL)) return NULL; + layer = s[0]; + } + /* The mask over the layer's rectangle, its colour outside its own. */ + if ((mask = vips_foreign_save_psd_mask(in[i])) && !vips_foreign_save_psd_field(in[i], "psd-layer-mask-disabled", 0)) { + const double outside = vips_foreign_save_psd_field(in[i], "psd-layer-mask-default-color", UCHAR_MAX) ? max : 0.0; + if (vips_foreign_save_psd_empty(mask)) + rc = vips_black(&s[1], layer->Xsize, layer->Ysize, NULL) || vips_linear1(s[1], &s[2], 1.0, outside, NULL); + else { + VipsArrayDouble *background = vips_array_double_new(&outside, 1); + rc = vips_embed(mask, &s[2], mask->Xoffset - in[i]->Xoffset, mask->Yoffset - in[i]->Yoffset, layer->Xsize, layer->Ysize, + "extend", VIPS_EXTEND_BACKGROUND, "background", background, NULL); + vips_area_unref(VIPS_AREA(background)); + } + if (!rc) rc = vips_extract_band(layer, &s[3], (int)colors, NULL) || vips_multiply(s[3], s[2], &s[4], NULL); + g_object_unref(mask); + if (rc) return NULL; + alpha_band = s[4]; + opacity[i] /= max; /* the mask's scale */ + } else { + VIPS_UNREF(mask); + if (opacity[i] < 1.0) { + if (vips_extract_band(layer, &s[3], (int)colors, NULL)) return NULL; + alpha_band = s[3]; + } } - if (opacity < UCHAR_MAX) { /* the opacity in the alpha */ - for (k = 0; k < bands; ++k) scale[k] = 1.0, offset[k] = 0.0; - scale[bands - 1] = opacity / (double)UCHAR_MAX; - if (vips_linear(layer, &t[3 + 3 * i], scale, offset, bands, NULL)) return NULL; - layer = t[3 + 3 * i]; + if (alpha_band) { /* the opacity (and the mask) in the alpha_band */ + if (vips_linear1(alpha_band, &s[5], opacity[i], 0.0, NULL) || vips_extract_band(layer, &s[6], 0, "n", (int)colors, NULL) + || vips_bandjoin2(s[6], s[5], &s[7], NULL)) + return NULL; + layer = s[7]; } - if (vips_copy(layer, &t[4 + 3 * i], "interpretation", ref->Type, NULL)) return NULL; - layers[count] = t[4 + 3 * i]; + if (vips_copy(layer, &s[8], "interpretation", ref->Type, NULL)) return NULL; + layers[count] = s[8]; g_object_ref(layers[count]); /* both arrays drop theirs */ if (vips_image_get_typeof(in[i], "psd-layer-blend-mode") && !vips_image_get_string(in[i], "psd-layer-blend-mode", &given) && strlen(given) == PSD_BLEND_KEY_SIZE) @@ -890,9 +981,9 @@ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, V if (rc) return NULL; composed = out; } - rc = vips_cast(composed, &t[2 + 3 * n], ref->BandFmt, NULL); /* vips_composite may give a wider format */ + rc = vips_cast(composed, &t[2 + PSD_COMPOSE_STEPS * n], ref->BandFmt, NULL); /* vips_composite may give a wider format */ g_object_unref(composed); - if (rc || vips_copy(t[2 + 3 * n], &out, "interpretation", ref->Type, NULL)) return NULL; + if (rc || vips_copy(t[2 + PSD_COMPOSE_STEPS * n], &out, "interpretation", ref->Type, NULL)) return NULL; return out; } @@ -901,8 +992,10 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; const char *nickname = VIPS_OBJECT_GET_CLASS(object)->nickname; VipsImage **in, *ref = NULL, *composite = NULL; - VipsForeignSavePsdChannels *channels = NULL, composite_channels, *target; + VipsForeignSavePsdChannels *channels = NULL, composite_channels; minipsd_layer *layers = NULL; + minipsd_mask *masks = NULL; + VipsImage **mask_images = NULL; minipsd_bytes *stored = NULL; char **names = NULL; minipsd_resource resources[PSD_MAX_RESOURCES]; @@ -911,7 +1004,8 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) const char *filename, *error = NULL, *text; size_t colors = 0, icc_size, count = 0, longest = 0; unsigned table; - int n, i, mode, depth, width, height, alpha = 0, psb, memory = 0, rc = MINIPSD_OK, failed = 0; + int n, i, mode, depth, compression, section, width, height, alpha = 0, psb, memory = 0, rc = MINIPSD_OK, + failed = 0; if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_parent_class)->build(object)) return -1; in = vips_array_image_get(psd->in, &n); @@ -932,6 +1026,10 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) return -1; } for (i = 0; i < n; ++i) { + if (vips_foreign_save_psd_section(in[i]) < 0) { + vips_error(nickname, "layer %d: psd-layer-section is not \"open\", \"closed\" or \"end\"", i); + return -1; + } if (vips_foreign_save_psd_empty(in[i])) continue; if (vips_foreign_save_psd_depth(in[i]) != depth || (in[i]->Bands != (int)colors && in[i]->Bands != (int)colors + 1) || (mode == MINIPSD_MODE_CMYK) != (in[i]->Type == VIPS_INTERPRETATION_CMYK)) { @@ -967,18 +1065,25 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) return -1; } - /* Each layer read once, its channels encoded as its rows come; then the composite. */ + /* Each layer read once, its channels encoded as its rows come, then its mask; then the + composite. channels holds the layers', then the masks'. */ memset(&composite_channels, 0, sizeof composite_channels); - channels = g_new0(VipsForeignSavePsdChannels, (gsize)n); - layers = g_new0(minipsd_layer, (gsize)n); + compression = vips_foreign_save_psd_layer_compression(psd->compression); + channels = g_new0(VipsForeignSavePsdChannels, 2 * (gsize)n); + layers = g_new0(minipsd_layer, (gsize)n); + masks = g_new0(minipsd_mask, (gsize)n); + mask_images = g_new0(VipsImage *, (gsize)n); stored = g_new0(minipsd_bytes, (gsize)n * PSD_MAX_PLANES); names = g_new0(char *, (gsize)n); for (i = 0; i < n && !failed; ++i) { - minipsd_layer *l = &layers[i]; + minipsd_layer *l = &layers[i]; + VipsImage *mask = mask_images[i] = vips_foreign_save_psd_mask(in[i]); + section = vips_foreign_save_psd_section(in[i]); if (vips_image_get_typeof(in[i], "psd-layer-name") && !vips_image_get_string(in[i], "psd-layer-name", &text)) l->name = text; else - l->name = names[i] = g_strdup_printf("Layer %d", i + 1); + l->name = section == MINIPSD_SECTION_GROUP_END ? PSD_DIVIDER_NAME : (names[i] = g_strdup_printf("Layer %d", i + 1)); + l->section_divider_type = (uint8_t)section; l->opacity = (uint8_t)VIPS_CLIP(0, vips_foreign_save_psd_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX); l->hidden = (uint8_t)(vips_foreign_save_psd_field(in[i], "psd-layer-hidden", 0) != 0); l->clipping = (uint8_t)(vips_foreign_save_psd_field(in[i], "psd-layer-clipping", 0) != 0); @@ -988,7 +1093,30 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) failed = 1; break; } - memcpy(l->blend_mode, text, PSD_BLEND_KEY_SIZE); + /* A group passing through: none given, which minipsd writes as Photoshop does. */ + if (section == MINIPSD_SECTION_LAYER || section == MINIPSD_SECTION_GROUP_END || memcmp(text, "pass", PSD_BLEND_KEY_SIZE) != 0) + memcpy(l->blend_mode, text, PSD_BLEND_KEY_SIZE); + } + if (mask) { /* one band of the layers' depth, its own rectangle; encoded as the layer is */ + if (mask->Bands != 1 || vips_foreign_save_psd_depth(mask) != depth) { + error = "a psd-layer-mask is not one band of the layers' depth"; + failed = 1; + break; + } + masks[i].default_color = vips_foreign_save_psd_field(in[i], "psd-layer-mask-default-color", UCHAR_MAX) ? UCHAR_MAX : 0; + masks[i].flags = vips_foreign_save_psd_field(in[i], "psd-layer-mask-disabled", 0) ? PSD_MASK_DISABLED : 0; + l->mask = &masks[i]; + if (!vips_foreign_save_psd_empty(mask)) { + masks[i].top = mask->Yoffset - psd->top; + masks[i].left = mask->Xoffset - psd->left; + masks[i].bottom = masks[i].top + mask->Ysize; + masks[i].right = masks[i].left + mask->Xsize; + if (vips_foreign_save_psd_encode_image(&channels[n + i], mask, depth, MINIPSD_MODE_GREY, compression, 0, 0, &memory)) { + failed = 1; + break; + } + longest = VIPS_MAX(longest, channels[n + i].longest); + } } if (vips_foreign_save_psd_empty(in[i])) continue; /* no pixels, no rectangle: as Photoshop writes one */ l->top = in[i]->Yoffset - psd->top; @@ -997,14 +1125,7 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) l->right = l->left + in[i]->Xsize; l->no_alpha = (uint8_t)(in[i]->Bands == (int)colors); l->stored = &stored[(size_t)i * PSD_MAX_PLANES]; - target = &channels[i]; - if (vips_foreign_save_psd_channels_init(&channels[i], in[i], depth, vips_foreign_save_psd_layer_compression(psd->compression), - 0, 0)) - memory = failed = 1; - else if (vips_foreign_save_psd_pass(in[i], depth, mode, &target, 1, &memory)) /* memory says whose error */ - failed = 1; - else if (vips_foreign_save_psd_finish(&channels[i])) - memory = failed = 1; + if (vips_foreign_save_psd_encode_image(&channels[i], in[i], depth, mode, compression, 0, 0, &memory)) failed = 1; longest = VIPS_MAX(longest, channels[i].longest); } if (!failed) { @@ -1012,15 +1133,10 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) if (!composite) failed = 1; } if (!failed) { - target = &composite_channels; - if (vips_foreign_save_psd_channels_init(&composite_channels, composite, depth, - psd->compression == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, 1, - composite->Bands > (int)colors)) - memory = failed = 1; - else if (vips_foreign_save_psd_pass(composite, depth, mode, &target, 1, &memory)) + if (vips_foreign_save_psd_encode_image(&composite_channels, composite, depth, mode, + psd->compression == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, 1, + composite->Bands > (int)colors, &memory)) failed = 1; - else if (vips_foreign_save_psd_finish(&composite_channels)) - memory = failed = 1; longest = VIPS_MAX(longest, composite_channels.longest); } @@ -1054,8 +1170,10 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) for (;;) { table = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; doc.format = psb ? MINIPSD_PSB : MINIPSD_PSD; - for (i = 0; i < n; ++i) + for (i = 0; i < n; ++i) { if (layers[i].stored) vips_foreign_save_psd_layer_blocks(&channels[i], table, stored + (size_t)i * PSD_MAX_PLANES); + if (channels[n + i].planes) vips_foreign_save_psd_layer_blocks(&channels[n + i], table, &masks[i].stored); + } doc.composite_stored = vips_foreign_save_psd_composite_block(&composite_channels, table); if (psb || vips_foreign_save_psd_fits_psd(&doc)) break; psb = 1; @@ -1063,7 +1181,10 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) rc = minipsd_write(&doc, NULL, vips_foreign_save_psd_to_target, psd->target); } - for (i = 0; channels && i < n; ++i) vips_foreign_save_psd_channels_free(&channels[i]); + for (i = 0; channels && i < 2 * n; ++i) vips_foreign_save_psd_channels_free(&channels[i]); + for (i = 0; mask_images && i < n; ++i) VIPS_UNREF(mask_images[i]); + g_free(mask_images); + g_free(masks); vips_foreign_save_psd_channels_free(&composite_channels); for (i = 0; names && i < n; ++i) g_free(names[i]); g_free(names); diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index e194b47..6c56a5b 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -54,9 +54,17 @@ int minipsd_vips_init(void); * unpainted one (the image's pixels are ignored). Every layer with pixels is of one mode and * depth (grey, RGB or CMYK, with or without alpha; uchar, ushort or float); none is converted. * + * A mask is an image in "psd-layer-mask": one band of the layers' depth, its own rectangle in its + * xoffset and yoffset, with "psd-layer-mask-default-color" (0 or 255, its value outside; 255 + * without it) and "psd-layer-mask-disabled". A group is records in the list, as the file has + * them: below its layers an image with "psd-layer-section" "end", above them one with "open" or + * "closed" and the group's name, visibility, opacity and blend mode ("pass", or none, passes + * through). Records have no pixels. psdload gives masks and records the same way. + * * Options, as NULL-ended name and value pairs: "composite" (a VipsImage, the canvas's size: the * composite as given; else vips_composite of the visible layers, blend modes mapped as the spec's - * table, flattened against white when no layer has alpha), "compression" + * table, masks and groups' visibility and opacity in the layers' alpha, flattened against white + * when no layer has alpha; clipping, and a group's own mode and mask, are not drawn), "compression" * (VipsForeignPsdCompression, the layers'), "psb", "width" and "height" (the canvas; 0: the * composite's size, else every layer's rectangle from (left, top)), "left" and "top" (where the * canvas lies in the layers' coordinates). Each image is read once for its layer, and once more diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index 403b089..f021b20 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -60,6 +60,8 @@ #define WIDE_H 2 #define COMPOSED_LEVELS 2 /* vips_composite's float rounding, doubled where the matte is undone */ #define PSD_RGB_BANDS 3 +#define MASK_DISABLED 0x02 /* a mask's flag */ +#define RECORDS 5 /* the round trip's: a layer, a group (divider, two layers, top record) */ #define PSD_VERSION_AT 5 /* the header's version, 1 PSD or 2 PSB: its low byte */ /* The ZIP document's two layers: one reaching past the canvas's left edge, one inside. */ @@ -780,17 +782,36 @@ static VipsImage *empty_layer(const char *name) return one; } +/* The composite's pixel (x, y) in a file: its first n bands into px; 0 on failure. */ +static int composite_pixel(const char *path, int x, int y, double *px, int n) +{ + VipsImage *image = vips_image_new_from_file(path, NULL); + double *values = NULL; + int bands = 0; + const int ok = image && !vips_getpoint(image, &values, &bands, x, y, NULL) && bands >= n; + if (ok) memcpy(px, values, (size_t)n * sizeof *values); + g_free(values); + VIPS_UNREF(image); + return ok; +} + +/* Within the composite's rounding of each other. */ +static int near(double a, double b) +{ + return fabs(a - b) <= COMPOSED_LEVELS; +} + /* psdsave_layers: a layered document from images, each a layer. Platemaker's template (a layer and an empty one above) byte for byte as minipsd_write makes it from the same layers; its composite made by libvips, the layer's own; a document minipsd wrote, every layer loaded with psdload and saved, the same bytes back (position, opacity, blend mode, clipping, an empty - layer); the canvas placed by left and top, or sized from the layers; what it refuses. */ + layer, a mask, a group); masks and groups in the composite libvips makes; the canvas placed by left and top, or sized from the layers; what it refuses. */ static void check_layers_saved(const uint8_t *rgba, const char *dir) { char path[PATH_MAX_SIZE], again[PATH_MAX_SIZE]; VipsImage *template_image = make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), *empty = empty_layer("Layer"); VipsImage *pair[2], *back = NULL, *given = NULL, *made = NULL; - minipsd_layer layers[3]; + minipsd_layer layers[RECORDS]; minipsd_doc doc; size_t given_bytes = 0, made_bytes = 0, i; uint8_t *a, *b; @@ -850,42 +871,115 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) } VIPS_UNREF(back); - /* A document minipsd writes: an opaque base, a smaller layer placed, half opaque, multiplied, - clipped, and an empty layer; each loaded with psdload and saved back: the same bytes. */ + /* A document minipsd writes: an opaque base; a smaller layer placed, half opaque, multiplied, + clipped to it, with a disabled mask of its own rectangle; a group (passing through, opacity + 200) holding a hidden empty layer; each record loaded with psdload and saved back: the same + bytes. */ { - enum { SMALL_W = 50, SMALL_H = 40, SMALL_X = 30, SMALL_Y = 20 }; - uint8_t *base = (uint8_t *)malloc(PIXELS * RGBA), *small = (uint8_t *)malloc(SMALL_W * SMALL_H * RGBA); - VipsImage *loaded[3] = { NULL, NULL, NULL }, *composite = NULL; + enum { SMALL_W = 50, SMALL_H = 40, SMALL_X = 30, SMALL_Y = 20, MASK_W = 20, MASK_H = 10, MASK_X = 35, MASK_Y = 25, + GROUP_OPACITY = 200 }; + uint8_t *base = (uint8_t *)malloc(PIXELS * RGBA), *small = (uint8_t *)malloc(SMALL_W * SMALL_H * RGBA); + uint8_t mask_pixels[MASK_W * MASK_H]; + minipsd_mask mask; + VipsImage *loaded[RECORDS] = { NULL }, *composite = NULL; + int all = 1; memset(layers, 0, sizeof layers); memset(&doc, 0, sizeof doc); + memset(&mask, 0, sizeof mask); if (base && small) { memcpy(base, rgba, PIXELS * RGBA); for (i = 0; i < PIXELS; ++i) base[i * RGBA + 3] = UCHAR_MAX; /* opaque: the composite comes back exactly */ for (i = 0; i < SMALL_W * SMALL_H * RGBA; ++i) small[i] = (uint8_t)(i * 11 + 3); + for (i = 0; i < MASK_W * MASK_H; ++i) mask_pixels[i] = (uint8_t)(i * 7); + mask.top = MASK_Y, mask.left = MASK_X, mask.bottom = MASK_Y + MASK_H, mask.right = MASK_X + MASK_W; + mask.pixels = mask_pixels, mask.flags = MASK_DISABLED; layers[0].name = "Base", layers[0].pixels = base, layers[0].opacity = UCHAR_MAX; layers[1].name = "Clip", layers[1].pixels = small, layers[1].opacity = HALF_OPAQUE, layers[1].clipping = 1; layers[1].top = SMALL_Y, layers[1].left = SMALL_X, layers[1].bottom = SMALL_Y + SMALL_H, layers[1].right = SMALL_X + SMALL_W; + layers[1].mask = &mask; memcpy(layers[1].blend_mode, "mul ", 4); - layers[2].name = "Empty", layers[2].opacity = UCHAR_MAX, layers[2].hidden = 1; + layers[2].name = "", layers[2].opacity = UCHAR_MAX, layers[2].section_divider_type = MINIPSD_SECTION_GROUP_END; + layers[3].name = "Empty", layers[3].opacity = UCHAR_MAX, layers[3].hidden = 1; + layers[4].name = "Group", layers[4].opacity = GROUP_OPACITY, layers[4].section_divider_type = MINIPSD_SECTION_GROUP; doc.width = WIDTH, doc.height = HEIGHT, doc.dpi = DPI; - doc.layers = layers, doc.layer_count = 3, doc.composite_alpha = 1; + doc.layers = layers, doc.layer_count = RECORDS, doc.composite_alpha = 1; snprintf(path, sizeof path, "%s/layers-minipsd.psd", dir); check(write_file(&doc, path), "written by minipsd", path); - for (k = 0; k < 3; ++k) loaded[k] = vips_image_new_from_file(path, "layer", k, NULL); + for (k = 0; k < RECORDS; ++k) all &= (loaded[k] = vips_image_new_from_file(path, "layer", k, NULL)) != NULL; composite = vips_image_new_from_file(path, NULL); snprintf(again, sizeof again, "%s/layers-resaved.psd", dir); - check(loaded[0] && loaded[1] && loaded[2] && composite - && vips_psdsave_layers(loaded, 3, again, "composite", composite, NULL) == 0, - "every layer loaded and saved", again); - check(same_files(path, again), "the same bytes back: names, position, opacity, blend mode, clipping, empty", again); + check(all && composite && vips_psdsave_layers(loaded, RECORDS, again, "composite", composite, NULL) == 0, + "every record loaded and saved", again); + check(same_files(path, again), + "the same bytes back: names, position, opacity, blend mode, clipping, empty, mask, group", again); vips_error_clear(); } - for (k = 0; k < 3; ++k) VIPS_UNREF(loaded[k]); + for (k = 0; k < RECORDS; ++k) VIPS_UNREF(loaded[k]); VIPS_UNREF(composite); free(base); free(small); } + /* Masks and groups in the composite libvips makes, no layer with alpha (so against white): a + mask's hole (0 inside, 255 outside) shows white; a hidden group hides its red layer; shown + at half opacity, the red is half over what is beneath; the mask disabled, no hole. */ + { + enum { HOLE_SIDE = 10, HOLE_AT = 5, INSIDE = 6 }; + static const double none[PSD_RGB_BANDS] = { 0.0, 0.0, 0.0 }, ink[PSD_RGB_BANDS] = { UCHAR_MAX, 0.0, 0.0 }; + VipsImage *rgb = NULL, *black = NULL, *hole = NULL, *flat = NULL, *lit = NULL, *red8 = NULL, *red = NULL; + VipsImage *divider = empty_layer(""), *group = empty_layer("Group"), *records[4]; + double at_corner[PSD_RGB_BANDS], in_hole[PSD_RGB_BANDS]; + const double half = HALF_OPAQUE / (double)UCHAR_MAX; + const int ready = divider && group && !vips_extract_band(template_image, &rgb, 0, "n", PSD_RGB_BANDS, NULL) + && !vips_black(&black, HOLE_SIDE, HOLE_SIDE, NULL) + && !vips_copy(black, &hole, "xoffset", HOLE_AT, "yoffset", HOLE_AT, NULL) + && !vips_black(&flat, WIDTH, HEIGHT, "bands", PSD_RGB_BANDS, NULL) + && !vips_linear(flat, &lit, none, ink, PSD_RGB_BANDS, NULL) && !vips_cast(lit, &red8, VIPS_FORMAT_UCHAR, NULL) + && !vips_copy(red8, &red, "interpretation", VIPS_INTERPRETATION_sRGB, NULL); + check(ready, "images made", "masks and groups"); + if (ready) { + vips_image_set_image(rgb, "psd-layer-mask", hole); + vips_image_set_int(rgb, "psd-layer-mask-default-color", UCHAR_MAX); + vips_image_set_string(divider, "psd-layer-section", "end"); + vips_image_set_string(group, "psd-layer-section", "open"); + vips_image_set_int(group, "psd-layer-hidden", 1); + records[0] = rgb, records[1] = divider, records[2] = red, records[3] = group; + snprintf(path, sizeof path, "%s/layers-masked.psd", dir); + check(vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, 0, 0, at_corner, PSD_RGB_BANDS) + && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS), + "saved, composite composed", path); + check(near(at_corner[0], rgba[0]) && near(at_corner[1], rgba[1]) && near(at_corner[2], rgba[2]), + "a hidden group's layer not drawn", path); + check(near(in_hole[0], UCHAR_MAX) && near(in_hole[1], UCHAR_MAX) && near(in_hole[2], UCHAR_MAX), + "the mask's hole shows white", path); + vips_image_set_int(group, "psd-layer-hidden", 0); + vips_image_set_int(group, "psd-layer-opacity", HALF_OPAQUE); + snprintf(path, sizeof path, "%s/layers-grouped.psd", dir); + check(vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, 0, 0, at_corner, PSD_RGB_BANDS) + && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS), + "saved, composite composed", path); + check(near(at_corner[0], UCHAR_MAX * half + rgba[0] * (1.0 - half)) && near(at_corner[1], rgba[1] * (1.0 - half)) + && near(in_hole[0], UCHAR_MAX) && near(in_hole[1], UCHAR_MAX * (1.0 - half)), + "a group's opacity multiplies its layer's", path); + vips_image_set_int(group, "psd-layer-hidden", 1); + vips_image_set_int(rgb, "psd-layer-mask-disabled", 1); + snprintf(path, sizeof path, "%s/layers-unmasked.psd", dir); + i = ((size_t)INSIDE * WIDTH + INSIDE) * RGBA; + check(vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS) + && near(in_hole[0], rgba[i]) && near(in_hole[1], rgba[i + 1]) && near(in_hole[2], rgba[i + 2]), + "a disabled mask hides nothing", path); + } + VIPS_UNREF(rgb); + VIPS_UNREF(black); + VIPS_UNREF(hole); + VIPS_UNREF(flat); + VIPS_UNREF(lit); + VIPS_UNREF(red8); + VIPS_UNREF(red); + VIPS_UNREF(divider); + VIPS_UNREF(group); + } + /* The canvas: placed by left and top, sized from the layers without width and height. */ { enum { AT_X = 7, AT_Y = 3 }; From 5a0abbc0825162ca96191c369ed7ce7fbc20c5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Sun, 25 Oct 2026 23:26:58 +0100 Subject: [PATCH 19/22] refactor(vips): the plugin's own names, not libvips' libvips accepts no contribution made with generative AI (its CONTRIBUTING.md), so the plugin never moves into libvips, and libvips' own names would only claim its namespace in every program that links the plugin. Types are MinipsdVipsLoad* and MinipsdVipsSave* (so are their GType names), functions minipsd_vips_*, the enum MinipsdVipsCompression (MINIPSD_VIPS_COMPRESSION_*), the calls minipsd_vips_psdsave_layers, _target and _buffer. The operations keep their nicknames: psdload, psdsave, psdsave_layers. The comments and docs that promised a move into libvips say it stays a plugin. --- docs/CODE-ARCH.md | 4 +- docs/ROADMAP.md | 7 +- docs/SPECIFICATION.md | 6 +- libvips-plugin/foreign/psd.c | 8 +- libvips-plugin/foreign/psd.h | 41 ++- libvips-plugin/foreign/psdload.c | 245 +++++++------- libvips-plugin/foreign/psdsave.c | 563 +++++++++++++++---------------- libvips-plugin/minipsd_vips.c | 18 +- libvips-plugin/minipsd_vips.h | 24 +- libvips-plugin/release/example.c | 2 +- libvips-plugin/test_vips.c | 40 +-- 11 files changed, 479 insertions(+), 479 deletions(-) diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index 8771f98..3e322e1 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -116,9 +116,9 @@ examples/ the I/O implementations; built and run in CI, nev read_file.c whole file into memory, described, every layer and the composite decoded resave.c minipsd_read with stored, one layer changed, written back: the round trip libvips-plugin/ minipsd as libvips' loader and saver (psdload, psdsave), registered by the program - minipsd_vips.h the public header: minipsd_vips_init, VipsForeignPsdCompression + minipsd_vips.h the public header: minipsd_vips_init, MinipsdVipsCompression minipsd_vips.c minipsd_vips_init: registers both sides with libvips - foreign/ as libvips/foreign/ would hold it, named as libvips names its own + foreign/ the formats' code, as libvips lays out its own; names the plugin's (MinipsdVips*) psdload.c psdload, _buffer, _source; minipsd's reader compiled in, private psdsave.c psdsave, _buffer, _target; minipsd's writer compiled in, private psd.c, psd.h internal: constants, helpers and the types both sides share diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 24a8a0d..b7b17e7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -531,8 +531,9 @@ programs that render a composite with libvips and write the layers with minipsd. the loader and saver, built first as a plugin in this repository (`libvips-plugin/`): libvips foreign classes registered by the program that links them. libvips' prebuilt Windows builds load no modules (8.18.7: `enable modules: false`), so a loadable module waits for a Linux user who wants -one. Platemaker uses it, which is the proof; whether a pull request to libvips follows is decided -once Platemaker does. +one. Platemaker uses it, which is the proof. It stays a plugin: libvips' CONTRIBUTING.md accepts no +contribution made with generative AI, and minipsd is made with it, so no pull request to libvips +follows. - [x] **S7.1** `examples/vips_composite.c`: a document's composite rendered with libvips (`vips_composite`, blend keys mapped as in the spec's @@ -560,7 +561,7 @@ once Platemaker does. way round, bitmap `B_W`, indexed through its palette: libvips has no indexed image), the rest as stored, what libvips cannot hold `MULTIBAND`, nothing converted; minipsd compiled in privately; zlib libvips' own; libvips from `cmake/minipsd_libvips.cmake` or the program. - Pages as layers, and a meson wrap, wait for a pull request to libvips. + Pages as layers, and a meson wrap, wait for a user who asks. - [x] **S7.3** Loader: a PSD or PSB as a libvips image, the composite or a layer, decoded in strips; ZIP through zlib. *Done when:* psd-tools' files that minipsd decodes load with the samples psd-tools gives; diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 8510ecf..42eadf7 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -727,7 +727,9 @@ does, and its memory images are minipsd's buffers, with no conversion: `libvips-plugin/` (`MINIPSD_VIPS_PLUGIN`, the static library `minipsd::vips`) is minipsd as libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSource` or a -`VipsTarget`, file and buffer variants that make one), so the same file can move into libvips. +`VipsTarget`, file and buffer variants that make one), in names of its own (`MinipsdVips*`, +`minipsd_vips_*`) that never meet libvips'. It stays a project apart from libvips: libvips' +CONTRIBUTING.md accepts no contribution made with generative AI, and minipsd is made with it. - **Registration.** `minipsd_vips_init()`, after `VIPS_INIT`, registers the six operations' types; libvips then finds them among its loaders and savers by `is_a` and suffix. libvips' @@ -779,7 +781,7 @@ libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSo to PSD's inverted ink. Kept: the resolution, the ICC profile, EXIF, XMP, IPTC and `psd-pixel-aspect-ratio`. Options: `compression` (the layer's channels: `rle`, the default, `zip`, `zip-predicted`, or `none`, which writes the composite raw too; otherwise the composite - is RLE; `VipsForeignPsdCompression` in `minipsd_vips.h`), `psb`, `layer_name` ("Layer 1"), + is RLE; `MinipsdVipsCompression` in `minipsd_vips.h`), `psb`, `layer_name` ("Layer 1"), `layered`. The image is read once, top to bottom, in the bands libvips hands a saver diff --git a/libvips-plugin/foreign/psd.c b/libvips-plugin/foreign/psd.c index 0076c1a..c19c3ff 100644 --- a/libvips-plugin/foreign/psd.c +++ b/libvips-plugin/foreign/psd.c @@ -11,18 +11,18 @@ #include #include -const char *vips__foreign_psd_suffs[] = { ".psd", ".psb", NULL }; +const char *minipsd_vips__suffs[] = { ".psd", ".psb", NULL }; -const char *const vips__foreign_psd_sections[PSD_SECTIONS] = { NULL, "open", "closed", "end" }; +const char *const minipsd_vips__sections[PSD_SECTIONS] = { NULL, "open", "closed", "end" }; -void vips__foreign_psd_class_properties(void *class) +void minipsd_vips__class_properties(void *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); gobject_class->set_property = vips_object_set_property; gobject_class->get_property = vips_object_get_property; } -void vips__foreign_psd_invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) +void minipsd_vips__invert_cmyk(void *pixels, size_t count, unsigned bands, int depth) { size_t i; unsigned c; diff --git a/libvips-plugin/foreign/psd.h b/libvips-plugin/foreign/psd.h index 1e7d3f4..3f65409 100644 --- a/libvips-plugin/foreign/psd.h +++ b/libvips-plugin/foreign/psd.h @@ -3,15 +3,14 @@ * \brief Internal to the plugin: what the loader and the saver share. * * Constants, the PSD_ARGS_* pair, the few helpers both sides use (defined once in psd.c, prefixed - * vips__foreign_psd_, as libvips prefixes what its parts share), and the operations' types, which - * minipsd_vips_init registers. As libvips keeps TIFF's shared code in foreign/tiff.c and tiff.h. + * minipsd_vips__), and the operations' types, which minipsd_vips_init registers. * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha */ -#ifndef VIPS_FOREIGN_PSD_H -#define VIPS_FOREIGN_PSD_H +#ifndef MINIPSD_VIPS_PSD_H +#define MINIPSD_VIPS_PSD_H #include @@ -35,36 +34,36 @@ #endif /* The suffixes libvips matches files by: .psd and .psb. */ -extern const char *vips__foreign_psd_suffs[]; +extern const char *minipsd_vips__suffs[]; /* A record's psd-layer-section, by its MINIPSD_SECTION_* type: NULL for a layer, then "open", "closed" (a group's top record, shown open or closed) and "end" (the divider below a group's layers). psdload sets it, psdsave_layers reads it. */ #define PSD_SECTIONS 4 -extern const char *const vips__foreign_psd_sections[PSD_SECTIONS]; +extern const char *const minipsd_vips__sections[PSD_SECTIONS]; /* GObject resets set_property and get_property in every class (g_object_base_class_init), so each class with arguments of its own sets libvips' again. */ -void vips__foreign_psd_class_properties(void *class); +void minipsd_vips__class_properties(void *class); /* CMYK the other way round, in place: PSD stores ink inverted (full: no ink), libvips holds it as TIFF does (0: no ink), as its own jpegload makes Adobe's inverted CMYK and ImageMagick makes PSD's. The colour channels only, not transparency; both ways, loading and saving. count pixels of bands samples, each of depth bits (8, 16, or 32 as floats). */ -void vips__foreign_psd_invert_cmyk(void *pixels, size_t count, unsigned bands, int depth); +void minipsd_vips__invert_cmyk(void *pixels, size_t count, unsigned bands, int depth); /* The operations: each abstract base, then the three made from it (psdload.c, psdsave.c). */ -GType vips_foreign_load_psd_get_type(void); -GType vips_foreign_load_psd_source_get_type(void); -GType vips_foreign_load_psd_file_get_type(void); -GType vips_foreign_load_psd_buffer_get_type(void); -GType vips_foreign_save_psd_get_type(void); -GType vips_foreign_save_psd_target_get_type(void); -GType vips_foreign_save_psd_file_get_type(void); -GType vips_foreign_save_psd_buffer_get_type(void); -GType vips_foreign_save_psd_layers_get_type(void); -GType vips_foreign_save_psd_layers_target_get_type(void); -GType vips_foreign_save_psd_layers_file_get_type(void); -GType vips_foreign_save_psd_layers_buffer_get_type(void); +GType minipsd_vips_load_get_type(void); +GType minipsd_vips_load_source_get_type(void); +GType minipsd_vips_load_file_get_type(void); +GType minipsd_vips_load_buffer_get_type(void); +GType minipsd_vips_save_get_type(void); +GType minipsd_vips_save_target_get_type(void); +GType minipsd_vips_save_file_get_type(void); +GType minipsd_vips_save_buffer_get_type(void); +GType minipsd_vips_save_layers_get_type(void); +GType minipsd_vips_save_layers_target_get_type(void); +GType minipsd_vips_save_layers_file_get_type(void); +GType minipsd_vips_save_layers_buffer_get_type(void); -#endif /* VIPS_FOREIGN_PSD_H */ +#endif /* MINIPSD_VIPS_PSD_H */ diff --git a/libvips-plugin/foreign/psdload.c b/libvips-plugin/foreign/psdload.c index 6c5f6cd..d3e1ccc 100644 --- a/libvips-plugin/foreign/psdload.c +++ b/libvips-plugin/foreign/psdload.c @@ -3,10 +3,9 @@ * \brief psdload, psdload_buffer, psdload_source: a PSD or PSB as a libvips image. * * Laid out as libvips' own loaders are (a base class over a VipsSource; the file and buffer - * variants make one), named as libvips names its own, in foreign/ as libvips keeps its formats: - * the directory moves into libvips/foreign/ as it is (minipsd.h into its own foreign/minipsd/, - * as libvips keeps the libraries it carries, libnsgif and qoi). The file is mapped (vips_source_map) and read in place by - * minipsd's reader; nothing is copied. + * variants make one), in the plugin's own names (MinipsdVips*, minipsd_vips_*), so they never + * meet libvips' own. The file is mapped (vips_source_map) and read in place by minipsd's + * reader; nothing is copied. * Pixels are decoded in bands of rows as libvips asks for them, from its threads at once. * * The image is the composite, or with `layer` one layer (its position in xoffset and yoffset), as @@ -55,7 +54,7 @@ /* --- The base class: a PSD read from a source --------------------------------------------- */ -typedef struct _VipsForeignLoadPsd { +typedef struct _MinipsdVipsLoad { VipsForeignLoad parent_object; int layer; /* the option: -1 the composite, else a layer, bottom first */ VipsSource *source; /* set by the subclass's build */ @@ -71,51 +70,51 @@ typedef struct _VipsForeignLoadPsd { int empty; /* the layer has no pixels: one transparent pixel stands for it */ size_t scratch_size; /* a ZIP layer's channels inflated whole; 0 without ZIP */ double xres, yres; /* pixels per mm */ -} VipsForeignLoadPsd; +} MinipsdVipsLoad; -typedef VipsForeignLoadClass VipsForeignLoadPsdClass; +typedef VipsForeignLoadClass MinipsdVipsLoadClass; -G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadPsd, vips_foreign_load_psd, VIPS_TYPE_FOREIGN_LOAD) +G_DEFINE_ABSTRACT_TYPE(MinipsdVipsLoad, minipsd_vips_load, VIPS_TYPE_FOREIGN_LOAD) /* Do these bytes start a PSD or a PSB? */ -static int vips_foreign_load_psd_is_a_bytes(const unsigned char *data, size_t size) +static int minipsd_vips_load_is_a_bytes(const unsigned char *data, size_t size) { return size >= PSD_SNIFF_SIZE && memcmp(data, PSD_SIGNATURE, PSD_SIGNATURE_SIZE) == 0 && data[4] == 0 && (data[5] == MINIPSD_PSD || data[5] == MINIPSD_PSB); } -static gboolean vips_foreign_load_psd_is_a_source(VipsSource *source) +static gboolean minipsd_vips_load_is_a_source(VipsSource *source) { const unsigned char *data = vips_source_sniff(source, PSD_SNIFF_SIZE); - return data && vips_foreign_load_psd_is_a_bytes(data, PSD_SNIFF_SIZE); + return data && minipsd_vips_load_is_a_bytes(data, PSD_SNIFF_SIZE); } -static void vips_foreign_load_psd_dispose(GObject *gobject) +static void minipsd_vips_load_dispose(GObject *gobject) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)gobject; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)gobject; VIPS_UNREF(psd->source); - G_OBJECT_CLASS(vips_foreign_load_psd_parent_class)->dispose(gobject); + G_OBJECT_CLASS(minipsd_vips_load_parent_class)->dispose(gobject); } -static VipsForeignFlags vips_foreign_load_psd_get_flags(VipsForeignLoad *load) +static VipsForeignFlags minipsd_vips_load_get_flags(VipsForeignLoad *load) { (void)load; return VIPS_FOREIGN_SEQUENTIAL; /* bands of rows; libvips keeps the image for random access */ } -static void vips_foreign_load_psd_error(VipsForeignLoadPsd *psd, const char *message) +static void minipsd_vips_load_error(MinipsdVipsLoad *psd, const char *message) { vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", message); } /* The bands' format: a sample of depth bits. Bitmap's 1-bit samples come as bytes. */ -static VipsBandFormat vips_foreign_load_psd_format(const minipsd_info *info) +static VipsBandFormat minipsd_vips_load_format(const minipsd_info *info) { return info->depth == 32 ? VIPS_FORMAT_FLOAT : info->depth == 16 ? VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR; } /* What the samples are, as libvips names it; MULTIBAND where it has no name for the mode. */ -static VipsInterpretation vips_foreign_load_psd_interpretation(const minipsd_info *info) +static VipsInterpretation minipsd_vips_load_interpretation(const minipsd_info *info) { switch (info->color_mode) { case MINIPSD_MODE_RGB: @@ -135,11 +134,11 @@ static VipsInterpretation vips_foreign_load_psd_interpretation(const minipsd_inf } /* The image's fields: on out, and on the image load() generates. */ -static int vips_foreign_load_psd_fields(VipsForeignLoadPsd *psd, VipsImage *image) +static int minipsd_vips_load_fields(MinipsdVipsLoad *psd, VipsImage *image) { vips_image_init_fields(image, (int)psd->width, (int)psd->height, (int)psd->bands, - vips_foreign_load_psd_format(&psd->info), VIPS_CODING_NONE, - vips_foreign_load_psd_interpretation(&psd->info), psd->xres, psd->yres); + minipsd_vips_load_format(&psd->info), VIPS_CODING_NONE, + minipsd_vips_load_interpretation(&psd->info), psd->xres, psd->yres); if (psd->layer >= 0) { image->Xoffset = psd->view.left; image->Yoffset = psd->view.top; @@ -148,7 +147,7 @@ static int vips_foreign_load_psd_fields(VipsForeignLoadPsd *psd, VipsImage *imag } /* A resource's bytes as a blob of the image, when the file has it. */ -static void vips_foreign_load_psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, const char *name) +static void minipsd_vips_load_set_blob(MinipsdVipsLoad *psd, uint16_t id, const char *name) { minipsd_resource res; if (minipsd_reader_find_resource(psd->reader, id, &res) == MINIPSD_OK && res.data.size) @@ -157,7 +156,7 @@ static void vips_foreign_load_psd_set_blob(VipsForeignLoadPsd *psd, uint16_t id, /* Bytes to inflate the layer's ZIP channels whole, each in its own part (scratch_kept); 0 when none of the planes is ZIP. */ -static size_t vips_foreign_load_psd_scratch_size(const VipsForeignLoadPsd *psd) +static size_t minipsd_vips_load_scratch_size(const MinipsdVipsLoad *psd) { const size_t row = ((size_t)psd->width * psd->info.depth + 7) / 8; unsigned i; @@ -170,7 +169,7 @@ static size_t vips_foreign_load_psd_scratch_size(const VipsForeignLoadPsd *psd) } /* zlib's uncompress, as minipsd_inflate_fn: the stream must fill dst exactly. */ -static int vips_foreign_load_psd_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) +static int minipsd_vips_load_inflate(void *user, const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size) { uLongf n = (uLongf)dst_size; (void)user; @@ -180,7 +179,7 @@ static int vips_foreign_load_psd_inflate(void *user, const uint8_t *src, size_t /* A group's own blend mode, from its 'lsct' block, into mode (a C string); 0 without one. The record's own key says "norm" for a group that passes through. */ -static int vips_foreign_load_psd_group_mode(const minipsd_layer_view *view, char mode[PSD_MODE_SIZE + 1]) +static int minipsd_vips_load_group_mode(const minipsd_layer_view *view, char mode[PSD_MODE_SIZE + 1]) { minipsd_block block; size_t pos = 0; @@ -197,12 +196,12 @@ static int vips_foreign_load_psd_group_mode(const minipsd_layer_view *view, char rectangle in xoffset and yoffset (one pixel with psd-layer-empty for a rectangle without area), its colour outside in "psd-layer-mask-default-color", "psd-layer-mask-disabled". Decoded whole: a mask is one band. */ -static int vips_foreign_load_psd_mask(VipsForeignLoadPsd *psd, VipsImage *out) +static int minipsd_vips_load_mask(MinipsdVipsLoad *psd, VipsImage *out) { const minipsd_mask *m = &psd->view.mask; const int empty = m->right <= m->left || m->bottom <= m->top; const uint32_t width = empty ? 1 : (uint32_t)(m->right - m->left), height = empty ? 1 : (uint32_t)(m->bottom - m->top); - const VipsBandFormat format = vips_foreign_load_psd_format(&psd->info); + const VipsBandFormat format = minipsd_vips_load_format(&psd->info); const size_t row = (size_t)width * (size_t)vips_format_sizeof(format), bytes = row * height; const int zip = m->stored.size >= PSD_MARKER_SIZE && m->stored.data && (m->stored.data[1] == PSD_STORED_ZIP || m->stored.data[1] == PSD_STORED_ZIP_PRED); @@ -215,7 +214,7 @@ static int vips_foreign_load_psd_mask(VipsForeignLoadPsd *psd, VipsImage *out) if (rc == MINIPSD_OK && !empty) { memset(&opts, 0, sizeof opts); opts.depth = psd->info.depth; - opts.inflate = vips_foreign_load_psd_inflate; + opts.inflate = minipsd_vips_load_inflate; opts.scratch = scratch; opts.scratch_size = zip ? bytes : 0; plane.data = pixels; @@ -228,7 +227,7 @@ static int vips_foreign_load_psd_mask(VipsForeignLoadPsd *psd, VipsImage *out) g_free(pixels); g_free(scratch); if (rc != MINIPSD_OK) { - vips_foreign_load_psd_error(psd, minipsd_error_string(rc)); + minipsd_vips_load_error(psd, minipsd_error_string(rc)); return -1; } image->Type = psd->info.depth == 16 ? VIPS_INTERPRETATION_GREY16 : VIPS_INTERPRETATION_B_W; @@ -243,7 +242,7 @@ static int vips_foreign_load_psd_mask(VipsForeignLoadPsd *psd, VipsImage *out) } /* The layer the option names: its record, its size, its name and look as metadata. */ -static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) +static int minipsd_vips_load_find_layer(MinipsdVipsLoad *psd) { VipsImage *out = ((VipsForeignLoad *)psd)->out; char *name, mode[PSD_MODE_SIZE + 1]; @@ -251,7 +250,7 @@ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) int rc, i; for (i = 0; i <= psd->layer; ++i) if ((rc = minipsd_reader_next_layer(psd->reader, &psd->view)) != MINIPSD_MORE) { - vips_foreign_load_psd_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); + minipsd_vips_load_error(psd, rc == MINIPSD_OK ? "no such layer" : minipsd_error_string(rc)); return -1; } /* An empty layer (no area: Photoshop writes its new, unpainted layers so) has no pixels, and a @@ -271,7 +270,7 @@ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) vips_image_set_string(out, "psd-layer-name", name); /* A group's top record: its own mode (pass through, or one it is composed in). */ if ((psd->view.section_divider_type == MINIPSD_SECTION_GROUP || psd->view.section_divider_type == MINIPSD_SECTION_GROUP_CLOSED) - && vips_foreign_load_psd_group_mode(&psd->view, mode)) + && minipsd_vips_load_group_mode(&psd->view, mode)) vips_image_set_string(out, "psd-layer-blend-mode", mode); else vips_image_set_string(out, "psd-layer-blend-mode", psd->view.blend_mode); @@ -280,13 +279,13 @@ static int vips_foreign_load_psd_find_layer(VipsForeignLoadPsd *psd) vips_image_set_int(out, "psd-layer-clipping", psd->view.clipping); vips_image_set_int(out, "psd-layer-empty", psd->empty); if (psd->view.section_divider_type && psd->view.section_divider_type < PSD_SECTIONS) - vips_image_set_string(out, "psd-layer-section", vips__foreign_psd_sections[psd->view.section_divider_type]); - return psd->view.has_mask && psd->info.depth >= CHAR_BIT ? vips_foreign_load_psd_mask(psd, out) : 0; + vips_image_set_string(out, "psd-layer-section", minipsd_vips__sections[psd->view.section_divider_type]); + return psd->view.has_mask && psd->info.depth >= CHAR_BIT ? minipsd_vips_load_mask(psd, out) : 0; } -static int vips_foreign_load_psd_header(VipsForeignLoad *load) +static int minipsd_vips_load_header(VipsForeignLoad *load) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)load; minipsd_resource res; minipsd_resolution_info resolution; minipsd_version_info version; @@ -304,7 +303,7 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) return -1; rc = minipsd_reader_init(&psd->reader, data, size, workspace, workspace_size, NULL, &psd->info); if (rc != MINIPSD_OK) { - vips_foreign_load_psd_error(psd, minipsd_error_string(rc)); + minipsd_vips_load_error(psd, minipsd_error_string(rc)); return -1; } @@ -312,8 +311,8 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) psd->height = psd->info.height; /* Channels past MINIPSD_PLANES (a multichannel file's sixth on) are not decoded. */ psd->planes = VIPS_MIN(psd->info.color_channels + (psd->info.composite_has_alpha ? 1u : 0u), MINIPSD_PLANES); - if (psd->layer >= 0 && vips_foreign_load_psd_find_layer(psd)) return -1; - psd->scratch_size = psd->layer >= 0 && !psd->empty ? vips_foreign_load_psd_scratch_size(psd) : 0; + if (psd->layer >= 0 && minipsd_vips_load_find_layer(psd)) return -1; + psd->scratch_size = psd->layer >= 0 && !psd->empty ? minipsd_vips_load_scratch_size(psd) : 0; /* Indexed: the palette's RGB, and transparency where resource 1047 names an index. */ psd->bands = psd->planes; @@ -321,7 +320,7 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) psd->indexed = psd->info.color_mode == MINIPSD_MODE_INDEXED; if (psd->indexed) { if (psd->info.color_mode_data.size < PSD_RGB * PSD_PALETTE_ENTRIES || !psd->info.color_mode_data.data) { - vips_foreign_load_psd_error(psd, "an indexed file without its palette"); + minipsd_vips_load_error(psd, "an indexed file without its palette"); return -1; } if (minipsd_reader_find_resource(psd->reader, MINIPSD_RES_TRANSPARENCY_INDEX, &res) == MINIPSD_OK @@ -340,15 +339,15 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) } psd->xres = xres > 0.0 ? xres / MM_PER_INCH : 1.0; psd->yres = yres > 0.0 ? yres / MM_PER_INCH : 1.0; - if (vips_foreign_load_psd_fields(psd, load->out)) return -1; + if (minipsd_vips_load_fields(psd, load->out)) return -1; if (xres > 0.0) vips_image_set_string(load->out, VIPS_META_RESOLUTION_UNIT, unit); vips_image_set_int(load->out, VIPS_META_BITS_PER_SAMPLE, psd->info.depth); if (psd->info.icc.size) vips_image_set_blob_copy(load->out, VIPS_META_ICC_NAME, psd->info.icc.data, psd->info.icc.size); - vips_foreign_load_psd_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); - vips_foreign_load_psd_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); - vips_foreign_load_psd_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); + minipsd_vips_load_set_blob(psd, MINIPSD_RES_EXIF_DATA_1, VIPS_META_EXIF_NAME); + minipsd_vips_load_set_blob(psd, MINIPSD_RES_XMP_METADATA, VIPS_META_XMP_NAME); + minipsd_vips_load_set_blob(psd, MINIPSD_RES_IPTC_NAA, VIPS_META_IPTC_NAME); if (psd->info.color_mode_data.size) vips_image_set_blob_copy(load->out, "psd-color-mode-data", psd->info.color_mode_data.data, psd->info.color_mode_data.size); @@ -363,15 +362,15 @@ static int vips_foreign_load_psd_header(VipsForeignLoad *load) } /* A thread's sequence: the scratch a ZIP layer's channels stay inflated in, between its bands. */ -typedef struct vips_foreign_load_psd_sequence { +typedef struct minipsd_vips_load_sequence { uint8_t *scratch; int kept; -} vips_foreign_load_psd_sequence; +} minipsd_vips_load_sequence; -static void *vips_foreign_load_psd_start(VipsImage *out, void *a, void *b) +static void *minipsd_vips_load_start(VipsImage *out, void *a, void *b) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; - vips_foreign_load_psd_sequence *seq = g_new0(vips_foreign_load_psd_sequence, 1); + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)a; + minipsd_vips_load_sequence *seq = g_new0(minipsd_vips_load_sequence, 1); (void)out, (void)b; /* ponytail: every thread inflates a ZIP layer whole into its own scratch; one shared scratch under a lock would bound memory at one copy, if layers that large turn up. */ @@ -382,17 +381,17 @@ static void *vips_foreign_load_psd_start(VipsImage *out, void *a, void *b) return seq; } -static int vips_foreign_load_psd_stop(void *seq, void *a, void *b) +static int minipsd_vips_load_stop(void *seq, void *a, void *b) { (void)a, (void)b; - g_free(((vips_foreign_load_psd_sequence *)seq)->scratch); + g_free(((minipsd_vips_load_sequence *)seq)->scratch); g_free(seq); return 0; } /* Indexed, in place: each pixel's index (in its first band) becomes the palette's RGB, and alpha 0 at the transparent index. */ -static void vips_foreign_load_psd_expand_palette(const VipsForeignLoadPsd *psd, VipsPel *row, int width) +static void minipsd_vips_load_expand_palette(const MinipsdVipsLoad *psd, VipsPel *row, int width) { const uint8_t *palette = psd->info.color_mode_data.data; int x; @@ -407,10 +406,10 @@ static void vips_foreign_load_psd_expand_palette(const VipsForeignLoadPsd *psd, /* One band of rows, decoded straight into the region. vips_sequential asks for whole rows; calls run at once from libvips' threads on the one reader, which minipsd allows on a whole file. */ -static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) +static int minipsd_vips_load_generate(VipsRegion *region, void *seq, void *a, void *b, gboolean *stop) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)a; - vips_foreign_load_psd_sequence *sequence = (vips_foreign_load_psd_sequence *)seq; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)a; + minipsd_vips_load_sequence *sequence = (minipsd_vips_load_sequence *)seq; const VipsRect *rect = ®ion->valid; minipsd_decode_opts opts; minipsd_target target; @@ -420,7 +419,7 @@ static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a (void)b, (void)stop; if (rect->left != 0 || rect->width != region->im->Xsize) { - vips_foreign_load_psd_error(psd, "only whole rows are decoded"); + minipsd_vips_load_error(psd, "only whole rows are decoded"); return -1; } if (psd->empty) { /* no pixels: transparent, and colour 0 */ @@ -436,7 +435,7 @@ static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a if (psd->layer < 0) { rc = minipsd_decode_composite(psd->reader, &target, &opts); } else { - opts.inflate = vips_foreign_load_psd_inflate; + opts.inflate = minipsd_vips_load_inflate; opts.scratch = sequence->scratch; opts.scratch_size = psd->scratch_size; opts.scratch_kept = sequence->kept; @@ -444,16 +443,16 @@ static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a sequence->kept = rc == MINIPSD_OK && sequence->scratch; } if (rc != MINIPSD_OK) { - vips_foreign_load_psd_error(psd, minipsd_error_string(rc)); + minipsd_vips_load_error(psd, minipsd_error_string(rc)); return -1; } for (y = 0; y < rect->height; ++y) { row = top + (size_t)y * skip; if (psd->indexed) { - vips_foreign_load_psd_expand_palette(psd, row, rect->width); + minipsd_vips_load_expand_palette(psd, row, rect->width); } else if (psd->info.color_mode == MINIPSD_MODE_CMYK) { - vips__foreign_psd_invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); + minipsd_vips__invert_cmyk(row, (size_t)rect->width, psd->bands, psd->info.depth); } else if (psd->info.color_mode == MINIPSD_MODE_BITMAP) { /* Bitmap decodes to 1 for black, 0 for white; libvips' B_W is 0 black, 255 white. */ for (x = 0; x < rect->width; ++x) row[x] = row[x] ? 0 : UCHAR_MAX; @@ -462,188 +461,188 @@ static int vips_foreign_load_psd_generate(VipsRegion *region, void *seq, void *a return 0; } -static int vips_foreign_load_psd_load(VipsForeignLoad *load) +static int minipsd_vips_load_load(VipsForeignLoad *load) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)load; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)load; VipsImage **t = (VipsImage **)vips_object_local_array(VIPS_OBJECT(load), 2); t[0] = vips_image_new(); - if (vips_foreign_load_psd_fields(psd, t[0]) - || vips_image_generate(t[0], vips_foreign_load_psd_start, vips_foreign_load_psd_generate, - vips_foreign_load_psd_stop, psd, NULL) + if (minipsd_vips_load_fields(psd, t[0]) + || vips_image_generate(t[0], minipsd_vips_load_start, minipsd_vips_load_generate, + minipsd_vips_load_stop, psd, NULL) || vips_sequential(t[0], &t[1], NULL) || vips_image_write(t[1], load->real)) return -1; return 0; } -static void vips_foreign_load_psd_class_init(VipsForeignLoadPsdClass *class) +static void minipsd_vips_load_class_init(MinipsdVipsLoadClass *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - gobject_class->dispose = vips_foreign_load_psd_dispose; - vips__foreign_psd_class_properties(class); + gobject_class->dispose = minipsd_vips_load_dispose; + minipsd_vips__class_properties(class); object_class->nickname = "psdload_base"; object_class->description = "load PSD and PSB with minipsd"; - load_class->get_flags = vips_foreign_load_psd_get_flags; - load_class->header = vips_foreign_load_psd_header; - load_class->load = vips_foreign_load_psd_load; + load_class->get_flags = minipsd_vips_load_get_flags; + load_class->header = minipsd_vips_load_header; + load_class->load = minipsd_vips_load_load; PSD_ARGS_BEGIN VIPS_ARG_INT(class, "layer", 20, "Layer", "Load this layer (bottom first), -1 the composite", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignLoadPsd, layer), PSD_COMPOSITE, INT_MAX, + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsLoad, layer), PSD_COMPOSITE, INT_MAX, PSD_COMPOSITE); PSD_ARGS_END } -static void vips_foreign_load_psd_init(VipsForeignLoadPsd *psd) +static void minipsd_vips_load_init(MinipsdVipsLoad *psd) { psd->layer = PSD_COMPOSITE; } /* --- psdload_source ----------------------------------------------------------------------- */ -typedef struct _VipsForeignLoadPsdSource { - VipsForeignLoadPsd parent_object; +typedef struct _MinipsdVipsLoadSource { + MinipsdVipsLoad parent_object; VipsSource *source; -} VipsForeignLoadPsdSource; +} MinipsdVipsLoadSource; -typedef VipsForeignLoadPsdClass VipsForeignLoadPsdSourceClass; +typedef MinipsdVipsLoadClass MinipsdVipsLoadSourceClass; -G_DEFINE_TYPE(VipsForeignLoadPsdSource, vips_foreign_load_psd_source, vips_foreign_load_psd_get_type()) +G_DEFINE_TYPE(MinipsdVipsLoadSource, minipsd_vips_load_source, minipsd_vips_load_get_type()) -static int vips_foreign_load_psd_source_build(VipsObject *object) +static int minipsd_vips_load_source_build(VipsObject *object) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; - VipsForeignLoadPsdSource *source = (VipsForeignLoadPsdSource *)object; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)object; + MinipsdVipsLoadSource *source = (MinipsdVipsLoadSource *)object; if (source->source) { psd->source = source->source; g_object_ref(psd->source); } - return VIPS_OBJECT_CLASS(vips_foreign_load_psd_source_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_load_source_parent_class)->build(object); } -static void vips_foreign_load_psd_source_class_init(VipsForeignLoadPsdSourceClass *class) +static void minipsd_vips_load_source_class_init(MinipsdVipsLoadSourceClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class); VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdload_source"; object_class->description = "load PSD and PSB from a source with minipsd"; - object_class->build = vips_foreign_load_psd_source_build; + object_class->build = minipsd_vips_load_source_build; operation_class->flags |= VIPS_OPERATION_NOCACHE; - load_class->is_a_source = vips_foreign_load_psd_is_a_source; + load_class->is_a_source = minipsd_vips_load_is_a_source; PSD_ARGS_BEGIN VIPS_ARG_OBJECT(class, "source", 1, "Source", "Source to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignLoadPsdSource, source), VIPS_TYPE_SOURCE); + G_STRUCT_OFFSET(MinipsdVipsLoadSource, source), VIPS_TYPE_SOURCE); PSD_ARGS_END } -static void vips_foreign_load_psd_source_init(VipsForeignLoadPsdSource *source) +static void minipsd_vips_load_source_init(MinipsdVipsLoadSource *source) { (void)source; } /* --- psdload (a file) ---------------------------------------------------------------------- */ -typedef struct _VipsForeignLoadPsdFile { - VipsForeignLoadPsd parent_object; +typedef struct _MinipsdVipsLoadFile { + MinipsdVipsLoad parent_object; char *filename; -} VipsForeignLoadPsdFile; +} MinipsdVipsLoadFile; -typedef VipsForeignLoadPsdClass VipsForeignLoadPsdFileClass; +typedef MinipsdVipsLoadClass MinipsdVipsLoadFileClass; -G_DEFINE_TYPE(VipsForeignLoadPsdFile, vips_foreign_load_psd_file, vips_foreign_load_psd_get_type()) +G_DEFINE_TYPE(MinipsdVipsLoadFile, minipsd_vips_load_file, minipsd_vips_load_get_type()) -static gboolean vips_foreign_load_psd_is_a_file(const char *filename) +static gboolean minipsd_vips_load_is_a_file(const char *filename) { VipsSource *source = vips_source_new_from_file(filename); gboolean is_a; if (!source) return FALSE; - is_a = vips_foreign_load_psd_is_a_source(source); + is_a = minipsd_vips_load_is_a_source(source); g_object_unref(source); return is_a; } -static int vips_foreign_load_psd_file_build(VipsObject *object) +static int minipsd_vips_load_file_build(VipsObject *object) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; - VipsForeignLoadPsdFile *file = (VipsForeignLoadPsdFile *)object; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)object; + MinipsdVipsLoadFile *file = (MinipsdVipsLoadFile *)object; if (file->filename && !(psd->source = vips_source_new_from_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_load_psd_file_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_load_file_parent_class)->build(object); } -static void vips_foreign_load_psd_file_class_init(VipsForeignLoadPsdFileClass *class) +static void minipsd_vips_load_file_class_init(MinipsdVipsLoadFileClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignClass *foreign_class = (VipsForeignClass *)class; VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdload"; object_class->description = "load PSD and PSB with minipsd"; - object_class->build = vips_foreign_load_psd_file_build; - foreign_class->suffs = vips__foreign_psd_suffs; - load_class->is_a = vips_foreign_load_psd_is_a_file; + object_class->build = minipsd_vips_load_file_build; + foreign_class->suffs = minipsd_vips__suffs; + load_class->is_a = minipsd_vips_load_is_a_file; PSD_ARGS_BEGIN VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignLoadPsdFile, filename), NULL); + G_STRUCT_OFFSET(MinipsdVipsLoadFile, filename), NULL); PSD_ARGS_END } -static void vips_foreign_load_psd_file_init(VipsForeignLoadPsdFile *file) +static void minipsd_vips_load_file_init(MinipsdVipsLoadFile *file) { (void)file; } /* --- psdload_buffer ------------------------------------------------------------------------ */ -typedef struct _VipsForeignLoadPsdBuffer { - VipsForeignLoadPsd parent_object; +typedef struct _MinipsdVipsLoadBuffer { + MinipsdVipsLoad parent_object; VipsBlob *blob; -} VipsForeignLoadPsdBuffer; +} MinipsdVipsLoadBuffer; -typedef VipsForeignLoadPsdClass VipsForeignLoadPsdBufferClass; +typedef MinipsdVipsLoadClass MinipsdVipsLoadBufferClass; -G_DEFINE_TYPE(VipsForeignLoadPsdBuffer, vips_foreign_load_psd_buffer, vips_foreign_load_psd_get_type()) +G_DEFINE_TYPE(MinipsdVipsLoadBuffer, minipsd_vips_load_buffer, minipsd_vips_load_get_type()) -static gboolean vips_foreign_load_psd_is_a_buffer(const void *data, size_t size) +static gboolean minipsd_vips_load_is_a_buffer(const void *data, size_t size) { - return vips_foreign_load_psd_is_a_bytes((const unsigned char *)data, size); + return minipsd_vips_load_is_a_bytes((const unsigned char *)data, size); } -static int vips_foreign_load_psd_buffer_build(VipsObject *object) +static int minipsd_vips_load_buffer_build(VipsObject *object) { - VipsForeignLoadPsd *psd = (VipsForeignLoadPsd *)object; - VipsForeignLoadPsdBuffer *buffer = (VipsForeignLoadPsdBuffer *)object; + MinipsdVipsLoad *psd = (MinipsdVipsLoad *)object; + MinipsdVipsLoadBuffer *buffer = (MinipsdVipsLoadBuffer *)object; if (buffer->blob && !(psd->source = vips_source_new_from_blob(buffer->blob))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_load_psd_buffer_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_load_buffer_parent_class)->build(object); } -static void vips_foreign_load_psd_buffer_class_init(VipsForeignLoadPsdBufferClass *class) +static void minipsd_vips_load_buffer_class_init(MinipsdVipsLoadBufferClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignLoadClass *load_class = (VipsForeignLoadClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdload_buffer"; object_class->description = "load PSD and PSB from a buffer with minipsd"; - object_class->build = vips_foreign_load_psd_buffer_build; - load_class->is_a_buffer = vips_foreign_load_psd_is_a_buffer; + object_class->build = minipsd_vips_load_buffer_build; + load_class->is_a_buffer = minipsd_vips_load_is_a_buffer; PSD_ARGS_BEGIN VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to load from", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignLoadPsdBuffer, blob), VIPS_TYPE_BLOB); + G_STRUCT_OFFSET(MinipsdVipsLoadBuffer, blob), VIPS_TYPE_BLOB); PSD_ARGS_END } -static void vips_foreign_load_psd_buffer_init(VipsForeignLoadPsdBuffer *buffer) +static void minipsd_vips_load_buffer_init(MinipsdVipsLoadBuffer *buffer) { (void)buffer; } diff --git a/libvips-plugin/foreign/psdsave.c b/libvips-plugin/foreign/psdsave.c index 49b5e67..7cbdfc0 100644 --- a/libvips-plugin/foreign/psdsave.c +++ b/libvips-plugin/foreign/psdsave.c @@ -3,9 +3,8 @@ * \brief psdsave, psdsave_buffer, psdsave_target: an image as a PSD or PSB, one layer or flat. * * Laid out as libvips' own savers are (a base class over a VipsTarget; the file and buffer - * variants make one), named as libvips names its own, in foreign/ as libvips keeps its formats: - * the directory moves into libvips/foreign/ as it is (minipsd.h into its own foreign/minipsd/, - * as libvips keeps the libraries it carries, libnsgif and qoi). An image with alpha is written as one layer that is also the + * variants make one), in the plugin's own names (MinipsdVips*, minipsd_vips_*), so they never + * meet libvips' own. An image with alpha is written as one layer that is also the * composite; one without, flat, as Photoshop saves an image without layers: the composite only, * without transparency (with `layered`, one layer without transparency too). Grey, RGB or CMYK, * 8 or 16 bits, grey and RGB also 32, CMYK back to PSD's inverted ink; the resolution, the ICC @@ -46,58 +45,58 @@ #define PSD_MAX_RESOURCES 3 /* EXIF, XMP, IPTC */ #define PSD_MAX_PLANES 5 /* the most bands it takes: CMYK and alpha */ -typedef struct _VipsForeignSavePsd { +typedef struct _MinipsdVipsSave { VipsForeignSave parent_object; VipsTarget *target; /* set by the subclass's build */ gboolean psb; /* the option; also a .psb file, or a side past PSD's */ - VipsForeignPsdCompression compression; /* the option: the layer's channels; none: the composite's too */ + MinipsdVipsCompression compression; /* the option: the layer's channels; none: the composite's too */ char *layer_name; /* the option */ gboolean layered; /* the option: a layer also for an image without alpha */ -} VipsForeignSavePsd; +} MinipsdVipsSave; -typedef VipsForeignSaveClass VipsForeignSavePsdClass; +typedef VipsForeignSaveClass MinipsdVipsSaveClass; -G_DEFINE_ABSTRACT_TYPE(VipsForeignSavePsd, vips_foreign_save_psd, VIPS_TYPE_FOREIGN_SAVE) +G_DEFINE_ABSTRACT_TYPE(MinipsdVipsSave, minipsd_vips_save, VIPS_TYPE_FOREIGN_SAVE) -/* VipsForeignPsdCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none", +/* MinipsdVipsCompression as a GEnum, so libvips takes it by its nick ("rle", "zip", "none", "zip-predicted"). */ -static GType vips_foreign_psd_compression_get_type(void) +static GType minipsd_vips_compression_get_type(void) { static gsize type = 0; if (g_once_init_enter(&type)) { static const GEnumValue values[] = { - { VIPS_FOREIGN_PSD_COMPRESSION_RLE, "VIPS_FOREIGN_PSD_COMPRESSION_RLE", "rle" }, - { VIPS_FOREIGN_PSD_COMPRESSION_ZIP, "VIPS_FOREIGN_PSD_COMPRESSION_ZIP", "zip" }, - { VIPS_FOREIGN_PSD_COMPRESSION_NONE, "VIPS_FOREIGN_PSD_COMPRESSION_NONE", "none" }, - { VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED, "VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED", "zip-predicted" }, + { MINIPSD_VIPS_COMPRESSION_RLE, "MINIPSD_VIPS_COMPRESSION_RLE", "rle" }, + { MINIPSD_VIPS_COMPRESSION_ZIP, "MINIPSD_VIPS_COMPRESSION_ZIP", "zip" }, + { MINIPSD_VIPS_COMPRESSION_NONE, "MINIPSD_VIPS_COMPRESSION_NONE", "none" }, + { MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED, "MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED", "zip-predicted" }, { 0, NULL, NULL }, }; - g_once_init_leave(&type, g_enum_register_static("VipsForeignPsdCompression", values)); + g_once_init_leave(&type, g_enum_register_static("MinipsdVipsCompression", values)); } return (GType)type; } /* What libvips' band formats become: 8-bit, 16-bit and float samples, as PSD has them. */ -static VipsBandFormat vips_foreign_save_psd_save_formats[] = { +static VipsBandFormat minipsd_vips_save_save_formats[] = { /* UCHAR, CHAR, USHORT, SHORT, UINT, INT, FLOAT, COMPLEX, DOUBLE, DPCOMPLEX */ VIPS_FORMAT_UCHAR, VIPS_FORMAT_UCHAR, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_USHORT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT, VIPS_FORMAT_FLOAT }; -static void vips_foreign_save_psd_dispose(GObject *gobject) +static void minipsd_vips_save_dispose(GObject *gobject) { - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)gobject; + MinipsdVipsSave *psd = (MinipsdVipsSave *)gobject; VIPS_UNREF(psd->target); - G_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->dispose(gobject); + G_OBJECT_CLASS(minipsd_vips_save_parent_class)->dispose(gobject); } -static int vips_foreign_save_psd_to_target(void *user, const void *data, size_t size) +static int minipsd_vips_save_to_target(void *user, const void *data, size_t size) { return vips_target_write((VipsTarget *)user, data, size) == 0; /* 0 stops the writer */ } /* A blob of the image as a resource to keep, when it has one. */ -static void vips_foreign_save_psd_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, +static void minipsd_vips_save_keep_blob(VipsImage *image, const char *name, uint16_t id, minipsd_resource *res, size_t *count) { const void *data; @@ -112,7 +111,7 @@ static void vips_foreign_save_psd_keep_blob(VipsImage *image, const char *name, /* The PSD mode of an image's bands: grey, RGB or CMYK (by its interpretation), each with or without alpha, its colour channels in *colors; -1 for any other band count. */ -static int vips_foreign_save_psd_mode(const VipsImage *in, size_t *colors) +static int minipsd_vips_save_mode(const VipsImage *in, size_t *colors) { if (in->Type == VIPS_INTERPRETATION_CMYK && (in->Bands == PSD_CMYK || in->Bands == PSD_CMYK + 1)) { *colors = PSD_CMYK; @@ -130,7 +129,7 @@ static int vips_foreign_save_psd_mode(const VipsImage *in, size_t *colors) } /* The PSD depth of an image's samples: 8, 16, 32 (floats); 0 for another format. */ -static int vips_foreign_save_psd_depth(const VipsImage *in) +static int minipsd_vips_save_depth(const VipsImage *in) { return in->BandFmt == VIPS_FORMAT_UCHAR ? 8 : in->BandFmt == VIPS_FORMAT_USHORT ? 16 : in->BandFmt == VIPS_FORMAT_FLOAT ? 32 : 0; } @@ -148,13 +147,13 @@ static int vips_foreign_save_psd_depth(const VipsImage *in) #define PSD_ZIP_ROOM 65536 /* deflate's output grows by at least this */ /* Bytes that grow, past what guint can count. */ -typedef struct _VipsForeignSavePsdBytes { +typedef struct _MinipsdVipsSaveBytes { uint8_t *data; size_t size, room; -} VipsForeignSavePsdBytes; +} MinipsdVipsSaveBytes; /* Room for extra bytes more: exactly, or at least doubled. */ -static int vips_foreign_save_psd_grow(VipsForeignSavePsdBytes *b, size_t extra, int exact) +static int minipsd_vips_save_grow(MinipsdVipsSaveBytes *b, size_t extra, int exact) { if (extra > b->room - b->size) { const size_t room = exact ? b->size + extra : VIPS_MAX(b->room * 2, b->size + extra); @@ -166,9 +165,9 @@ static int vips_foreign_save_psd_grow(VipsForeignSavePsdBytes *b, size_t extra, return 0; } -static int vips_foreign_save_psd_append(VipsForeignSavePsdBytes *b, const void *data, size_t size) +static int minipsd_vips_save_append(MinipsdVipsSaveBytes *b, const void *data, size_t size) { - if (vips_foreign_save_psd_grow(b, size, 0)) return -1; + if (minipsd_vips_save_grow(b, size, 0)) return -1; memcpy(b->data + b->size, data, size); b->size += size; return 0; @@ -177,29 +176,29 @@ static int vips_foreign_save_psd_append(VipsForeignSavePsdBytes *b, const void * /* One channel: room ahead (head) for its marker and its rows' sizes, the most a PSB needs, then its rows encoded one after another; each one's size (RLE), or its deflate (ZIP). The block a file stores is then written in front of the rows, where they lie: never copied. */ -typedef struct _VipsForeignSavePsdChannel { - VipsForeignSavePsdBytes rows; +typedef struct _MinipsdVipsSaveChannel { + MinipsdVipsSaveBytes rows; size_t head; size_t *sizes; z_stream zip; int zipping; -} VipsForeignSavePsdChannel; +} MinipsdVipsSaveChannel; /* One image's channels as a file stores them: a layer's, each channel its own block, or the composite's, one block, its channels joined behind the first's head, its rows matted when the image has alpha (as minipsd mattes a composite it is given). */ -typedef struct _VipsForeignSavePsdChannels { - VipsForeignSavePsdChannel channel[PSD_MAX_PLANES]; +typedef struct _MinipsdVipsSaveChannels { + MinipsdVipsSaveChannel channel[PSD_MAX_PLANES]; unsigned planes; /* the image's bands: colour channels, then alpha */ int compression; /* MINIPSD_RLE, _RAW, _ZIP, _ZIP_PREDICTED */ int composite, matte; uint32_t height; size_t longest; /* the longest RLE row: a PSD counts to 65535 */ -} VipsForeignSavePsdChannels; +} MinipsdVipsSaveChannels; /* One image read once, top to bottom, each row encoded into every target's channels. */ -typedef struct _VipsForeignSavePsdPass { - VipsForeignSavePsdChannels **targets; +typedef struct _MinipsdVipsSavePass { + MinipsdVipsSaveChannels **targets; unsigned count, planes; int depth, mode; uint32_t width; @@ -208,9 +207,9 @@ typedef struct _VipsForeignSavePsdPass { uint8_t *ink; /* one row of pixels, CMYK inverted to PSD's ink */ uint8_t *matted; /* one row of pixels, matted for a composite */ int failed; /* memory ran out, or deflate failed, while the bands came */ -} VipsForeignSavePsdPass; +} MinipsdVipsSavePass; -static void vips_foreign_save_psd_channel_free(VipsForeignSavePsdChannel *c) +static void minipsd_vips_save_channel_free(MinipsdVipsSaveChannel *c) { g_free(c->rows.data); g_free(c->sizes); @@ -218,16 +217,16 @@ static void vips_foreign_save_psd_channel_free(VipsForeignSavePsdChannel *c) memset(c, 0, sizeof *c); } -static void vips_foreign_save_psd_channels_free(VipsForeignSavePsdChannels *c) +static void minipsd_vips_save_channels_free(MinipsdVipsSaveChannels *c) { unsigned k; - for (k = 0; k < PSD_MAX_PLANES; ++k) vips_foreign_save_psd_channel_free(&c->channel[k]); + for (k = 0; k < PSD_MAX_PLANES; ++k) minipsd_vips_save_channel_free(&c->channel[k]); memset(c, 0, sizeof *c); } /* A channel ready for rows: the head for the tables of channels channels (the composite's first holds them all, its others none), raw rows' room known ahead, ZIP's deflate started. */ -static int vips_foreign_save_psd_channel_init(VipsForeignSavePsdChannel *c, int compression, unsigned channels, uint32_t height, +static int minipsd_vips_save_channel_init(MinipsdVipsSaveChannel *c, int compression, unsigned channels, uint32_t height, size_t row_bytes) { const int rle = compression == MINIPSD_RLE, raw = compression == MINIPSD_RAW; @@ -237,14 +236,14 @@ static int vips_foreign_save_psd_channel_init(VipsForeignSavePsdChannel *c, int if (deflateInit(&c->zip, PSD_ZIP_LEVEL) != Z_OK) return -1; c->zipping = 1; } - if (vips_foreign_save_psd_grow(&c->rows, c->head + (raw ? (size_t)(channels ? channels : 1u) * height * row_bytes : 0), 1)) + if (minipsd_vips_save_grow(&c->rows, c->head + (raw ? (size_t)(channels ? channels : 1u) * height * row_bytes : 0), 1)) return -1; c->rows.size = c->head; return 0; } /* An image's channels ready for its rows: a layer's (composite 0) or the composite's. */ -static int vips_foreign_save_psd_channels_init(VipsForeignSavePsdChannels *c, VipsImage *in, int depth, int compression, +static int minipsd_vips_save_channels_init(MinipsdVipsSaveChannels *c, VipsImage *in, int depth, int compression, int composite, int matte) { const size_t row_bytes = (size_t)MINIPSD_ROW_BYTES((uint32_t)in->Xsize, depth); @@ -256,19 +255,19 @@ static int vips_foreign_save_psd_channels_init(VipsForeignSavePsdChannels *c, Vi c->matte = matte; c->height = (uint32_t)in->Ysize; for (k = 0; k < c->planes; ++k) - if (vips_foreign_save_psd_channel_init(&c->channel[k], compression, composite ? (k ? 0 : c->planes) : 1, c->height, row_bytes)) + if (minipsd_vips_save_channel_init(&c->channel[k], compression, composite ? (k ? 0 : c->planes) : 1, c->height, row_bytes)) return -1; return 0; } /* data deflated onto the channel's rows; with Z_FINISH, the stream ended. */ -static int vips_foreign_save_psd_deflate(VipsForeignSavePsdChannel *c, const uint8_t *data, size_t size, int flush) +static int minipsd_vips_save_deflate(MinipsdVipsSaveChannel *c, const uint8_t *data, size_t size, int flush) { int rc; c->zip.next_in = data; c->zip.avail_in = (uInt)size; /* a row: within uInt */ do { - if (vips_foreign_save_psd_grow(&c->rows, PSD_ZIP_ROOM, 0)) return -1; + if (minipsd_vips_save_grow(&c->rows, PSD_ZIP_ROOM, 0)) return -1; c->zip.next_out = c->rows.data + c->rows.size; c->zip.avail_out = (uInt)VIPS_MIN(c->rows.room - c->rows.size, (size_t)UINT_MAX); rc = deflate(&c->zip, flush); @@ -280,10 +279,10 @@ static int vips_foreign_save_psd_deflate(VipsForeignSavePsdChannel *c, const uin /* Row y of channel k out of interleaved pixels, encoded into t's channel: PackBits, raw, or deflated (raw or predicted rows into the channel's stream). */ -static int vips_foreign_save_psd_encode(VipsForeignSavePsdPass *p, VipsForeignSavePsdChannels *t, const uint8_t *pixels, unsigned k, +static int minipsd_vips_save_encode(MinipsdVipsSavePass *p, MinipsdVipsSaveChannels *t, const uint8_t *pixels, unsigned k, uint32_t y) { - VipsForeignSavePsdChannel *c = &t->channel[k]; + MinipsdVipsSaveChannel *c = &t->channel[k]; const size_t sample = (size_t)(p->depth / CHAR_BIT); size_t n = (size_t)MINIPSD_ROW_BYTES(p->width, p->depth); const int rc = t->compression == MINIPSD_ZIP_PREDICTED @@ -293,17 +292,17 @@ static int vips_foreign_save_psd_encode(VipsForeignSavePsdPass *p, VipsForeignSa pixels + k * sample, p->planes * sample, p->width, p->row, p->row_size, &n); if (rc != MINIPSD_OK) return -1; - if (c->zipping) return vips_foreign_save_psd_deflate(c, p->row, n, Z_NO_FLUSH); + if (c->zipping) return minipsd_vips_save_deflate(c, p->row, n, Z_NO_FLUSH); if (c->sizes) { c->sizes[y] = n; if (n > t->longest) t->longest = n; } - return vips_foreign_save_psd_append(&c->rows, p->row, n); + return minipsd_vips_save_append(&c->rows, p->row, n); } /* count rows of interleaved pixels, from row y on, stride bytes apart: CMYK inverted to PSD's ink, then each target's channels encoded, a composite's rows matted when it asks (RGB with alpha). */ -static int vips_foreign_save_psd_encode_rows(VipsForeignSavePsdPass *p, const uint8_t *rows, uint32_t y, uint32_t count, +static int minipsd_vips_save_encode_rows(MinipsdVipsSavePass *p, const uint8_t *rows, uint32_t y, uint32_t count, size_t stride) { const size_t bytes = (size_t)p->width * p->planes * (size_t)(p->depth / CHAR_BIT); @@ -314,28 +313,28 @@ static int vips_foreign_save_psd_encode_rows(VipsForeignSavePsdPass *p, const ui int matted = 0; if (p->mode == MINIPSD_MODE_CMYK) { memcpy(p->ink, rows, bytes); - vips__foreign_psd_invert_cmyk(p->ink, p->width, p->planes, p->depth); + minipsd_vips__invert_cmyk(p->ink, p->width, p->planes, p->depth); pixels = p->ink; } for (t = 0; t < p->count; ++t) { - VipsForeignSavePsdChannels *target = p->targets[t]; + MinipsdVipsSaveChannels *target = p->targets[t]; if (target->matte && !matted) { /* as minipsd mattes a composite it is given (RGB; the others as they are) */ memcpy(p->matted, pixels, bytes); (void)minipsd_matte_row(p->mode, p->depth, p->matted, p->width); matted = 1; } for (k = 0; k < p->planes; ++k) - if (vips_foreign_save_psd_encode(p, target, target->matte ? p->matted : pixels, k, y + i)) return -1; + if (minipsd_vips_save_encode(p, target, target->matte ? p->matted : pixels, k, y + i)) return -1; } } return 0; } /* A band of the image, as libvips hands it: once, top to bottom; the region is the band. */ -static int vips_foreign_save_psd_band(VipsRegion *region, VipsRect *area, void *user) +static int minipsd_vips_save_band(VipsRegion *region, VipsRect *area, void *user) { - VipsForeignSavePsdPass *p = (VipsForeignSavePsdPass *)user; - if (vips_foreign_save_psd_encode_rows(p, VIPS_REGION_ADDR_TOPLEFT(region), (uint32_t)area->top, (uint32_t)area->height, + MinipsdVipsSavePass *p = (MinipsdVipsSavePass *)user; + if (minipsd_vips_save_encode_rows(p, VIPS_REGION_ADDR_TOPLEFT(region), (uint32_t)area->top, (uint32_t)area->height, (size_t)VIPS_REGION_LSKIP(region))) { p->failed = 1; return -1; @@ -346,10 +345,10 @@ static int vips_foreign_save_psd_band(VipsRegion *region, VipsRect *area, void * /* in read once, top to bottom, in the bands libvips hands a saver, its rows encoded into count targets' channels (each with in's bands). -1 on failure, *memory set when memory ran out or a deflate failed; otherwise the error is the image's own, reported where it arose. */ -static int vips_foreign_save_psd_pass(VipsImage *in, int depth, int mode, VipsForeignSavePsdChannels **targets, unsigned count, +static int minipsd_vips_save_pass(VipsImage *in, int depth, int mode, MinipsdVipsSaveChannels **targets, unsigned count, int *memory) { - VipsForeignSavePsdPass p; + MinipsdVipsSavePass p; const size_t pixels = (size_t)in->Xsize * (size_t)in->Bands * (size_t)(depth / CHAR_BIT); int rc; memset(&p, 0, sizeof p); @@ -363,7 +362,7 @@ static int vips_foreign_save_psd_pass(VipsImage *in, int depth, int mode, VipsFo p.row = (uint8_t *)g_try_malloc(p.row_size); p.ink = (uint8_t *)g_try_malloc(pixels); p.matted = (uint8_t *)g_try_malloc(pixels); - rc = !p.row || !p.ink || !p.matted ? -1 : vips_sink_disc(in, vips_foreign_save_psd_band, &p); + rc = !p.row || !p.ink || !p.matted ? -1 : vips_sink_disc(in, minipsd_vips_save_band, &p); *memory = rc && (!p.row || !p.ink || !p.matted || p.failed); g_free(p.row); g_free(p.ink); @@ -373,20 +372,20 @@ static int vips_foreign_save_psd_pass(VipsImage *in, int depth, int mode, VipsFo /* Every row in: the deflates ended, a composite's channels joined behind the first's head (the file stores them as one block). */ -static int vips_foreign_save_psd_finish(VipsForeignSavePsdChannels *c) +static int minipsd_vips_save_finish(MinipsdVipsSaveChannels *c) { - VipsForeignSavePsdBytes *first = &c->channel[0].rows; + MinipsdVipsSaveBytes *first = &c->channel[0].rows; size_t total = first->size; unsigned k; for (k = 0; k < c->planes; ++k) if (c->channel[k].zipping) { - if (vips_foreign_save_psd_deflate(&c->channel[k], NULL, 0, Z_FINISH)) return -1; + if (minipsd_vips_save_deflate(&c->channel[k], NULL, 0, Z_FINISH)) return -1; (void)deflateEnd(&c->channel[k].zip); c->channel[k].zipping = 0; } if (!c->composite) return 0; for (k = 1; k < c->planes; ++k) total += c->channel[k].rows.size; - if (vips_foreign_save_psd_grow(first, total - first->size, 1)) return -1; + if (minipsd_vips_save_grow(first, total - first->size, 1)) return -1; for (k = 1; k < c->planes; ++k) { memcpy(first->data + first->size, c->channel[k].rows.data, c->channel[k].rows.size); first->size += c->channel[k].rows.size; @@ -397,7 +396,7 @@ static int vips_foreign_save_psd_finish(VipsForeignSavePsdChannels *c) } /* Puts n, big-endian, in size bytes. */ -static uint8_t *vips_foreign_save_psd_put(uint8_t *p, uint64_t n, unsigned size) +static uint8_t *minipsd_vips_save_put(uint8_t *p, uint64_t n, unsigned size) { unsigned i; for (i = 0; i < size; ++i) p[i] = (uint8_t)(n >> (CHAR_BIT * (size - 1 - i))); @@ -407,7 +406,7 @@ static uint8_t *vips_foreign_save_psd_put(uint8_t *p, uint64_t n, unsigned size) /* The block a file stores for channels c[0] .. c[channels - 1], whose rows c[0] holds: the marker, an RLE one's row sizes in count bytes each, every channel's in turn, then the rows. Written into c[0]'s head, just ahead of the rows. */ -static minipsd_bytes vips_foreign_save_psd_block(VipsForeignSavePsdChannel *c, unsigned channels, uint32_t height, unsigned count, +static minipsd_bytes minipsd_vips_save_block(MinipsdVipsSaveChannel *c, unsigned channels, uint32_t height, unsigned count, int compression) { const int rle = compression == MINIPSD_RLE; @@ -416,11 +415,11 @@ static minipsd_bytes vips_foreign_save_psd_block(VipsForeignSavePsdChannel *c, u const uint8_t marker = rle ? PSD_STORED_RLE : compression == MINIPSD_RAW ? PSD_STORED_RAW : compression == MINIPSD_ZIP ? PSD_STORED_ZIP : PSD_STORED_ZIP_PREDICTED; minipsd_bytes block; - uint8_t *p = vips_foreign_save_psd_put(c->rows.data + start, marker, PSD_MARKER_SIZE); + uint8_t *p = minipsd_vips_save_put(c->rows.data + start, marker, PSD_MARKER_SIZE); unsigned k; uint32_t y; for (k = 0; rle && k < channels; ++k) - for (y = 0; y < height; ++y) p = vips_foreign_save_psd_put(p, c[k].sizes[y], count); + for (y = 0; y < height; ++y) p = minipsd_vips_save_put(p, c[k].sizes[y], count); block.data = c->rows.data + start; block.size = c->rows.size - start; return block; @@ -428,28 +427,28 @@ static minipsd_bytes vips_foreign_save_psd_block(VipsForeignSavePsdChannel *c, u /* A layer's channels as a PSD (count 2) or a PSB (4) stores them, into stored: the colour channels, then transparency. */ -static void vips_foreign_save_psd_layer_blocks(VipsForeignSavePsdChannels *c, unsigned count, minipsd_bytes *stored) +static void minipsd_vips_save_layer_blocks(MinipsdVipsSaveChannels *c, unsigned count, minipsd_bytes *stored) { unsigned k; - for (k = 0; k < c->planes; ++k) stored[k] = vips_foreign_save_psd_block(&c->channel[k], 1, c->height, count, c->compression); + for (k = 0; k < c->planes; ++k) stored[k] = minipsd_vips_save_block(&c->channel[k], 1, c->height, count, c->compression); } /* The composite's channels as a PSD or a PSB stores them: one block. */ -static minipsd_bytes vips_foreign_save_psd_composite_block(VipsForeignSavePsdChannels *c, unsigned count) +static minipsd_bytes minipsd_vips_save_composite_block(MinipsdVipsSaveChannels *c, unsigned count) { - return vips_foreign_save_psd_block(c->channel, c->planes, c->height, count, c->compression); + return minipsd_vips_save_block(c->channel, c->planes, c->height, count, c->compression); } /* One image read once into c, encoded by compression; a composite's joined as the file stores it. -1 on failure, *memory set when memory ran out (else the image's own error, reported). */ -static int vips_foreign_save_psd_encode_image(VipsForeignSavePsdChannels *c, VipsImage *in, int depth, int mode, int compression, +static int minipsd_vips_save_encode_image(MinipsdVipsSaveChannels *c, VipsImage *in, int depth, int mode, int compression, int composite, int matte, int *memory) { *memory = 1; - if (vips_foreign_save_psd_channels_init(c, in, depth, compression, composite, matte)) return -1; - if (vips_foreign_save_psd_pass(in, depth, mode, &c, 1, memory)) return -1; + if (minipsd_vips_save_channels_init(c, in, depth, compression, composite, matte)) return -1; + if (minipsd_vips_save_pass(in, depth, mode, &c, 1, memory)) return -1; *memory = 1; - if (vips_foreign_save_psd_finish(c)) return -1; + if (minipsd_vips_save_finish(c)) return -1; *memory = 0; return 0; } @@ -457,7 +456,7 @@ static int vips_foreign_save_psd_encode_image(VipsForeignSavePsdChannels *c, Vip /* Whether a PSD holds the document: its layer section within the 4 GiB its length counts (minipsd refuses one past it, MINIPSD_ERR_SIZE), the file within PSD_MAX_FILE. Any other refusal is minipsd_write's to report. */ -static int vips_foreign_save_psd_fits_psd(const minipsd_doc *doc) +static int minipsd_vips_save_fits_psd(const minipsd_doc *doc) { minipsd_writer *w; void *workspace; @@ -472,22 +471,22 @@ static int vips_foreign_save_psd_fits_psd(const minipsd_doc *doc) } /* MINIPSD_* compression by the option: the layers'; the composite's is RLE, raw with none. */ -static int vips_foreign_save_psd_layer_compression(VipsForeignPsdCompression compression) +static int minipsd_vips_save_layer_compression(MinipsdVipsCompression compression) { static const int by_option[] = { MINIPSD_RLE, MINIPSD_ZIP, MINIPSD_RAW, MINIPSD_ZIP_PREDICTED }; return by_option[compression]; } -static int vips_foreign_save_psd_build(VipsObject *object) +static int minipsd_vips_save_build(VipsObject *object) { VipsForeignSave *save = (VipsForeignSave *)object; - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + MinipsdVipsSave *psd = (MinipsdVipsSave *)object; VipsImage *in; minipsd_resource resources[PSD_MAX_RESOURCES]; minipsd_bytes stored[PSD_MAX_PLANES]; minipsd_layer layer; minipsd_doc doc; - VipsForeignSavePsdChannels layer_channels, composite_channels, *targets[2]; + MinipsdVipsSaveChannels layer_channels, composite_channels, *targets[2]; const void *icc; const char *filename; size_t icc_size, colors = 0, count = 0; @@ -495,17 +494,17 @@ static int vips_foreign_save_psd_build(VipsObject *object) unsigned n = 0, table; int rc, mode, alpha, layered, psb, memory = 0; - if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_parent_class)->build(object)) return -1; + if (VIPS_OBJECT_CLASS(minipsd_vips_save_parent_class)->build(object)) return -1; in = save->ready; /* The mode from the bands: grey, RGB or CMYK, each with or without alpha. */ memset(&doc, 0, sizeof doc); - if ((mode = vips_foreign_save_psd_mode(in, &colors)) < 0) { + if ((mode = minipsd_vips_save_mode(in, &colors)) < 0) { vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha"); return -1; } doc.color_mode = (uint16_t)mode; - doc.depth = (uint16_t)vips_foreign_save_psd_depth(in); /* format_table made it one of the three */ + doc.depth = (uint16_t)minipsd_vips_save_depth(in); /* format_table made it one of the three */ alpha = (size_t)in->Bands > colors; /* transparency as the image has it: none, none added */ layered = alpha || psd->layered; /* without alpha: flat, unless asked */ doc.width = (uint32_t)in->Xsize; @@ -518,26 +517,26 @@ static int vips_foreign_save_psd_build(VipsObject *object) feeds the layer and the composite. */ memset(&layer_channels, 0, sizeof layer_channels); if (layered) { - memory = vips_foreign_save_psd_channels_init(&layer_channels, in, doc.depth, - vips_foreign_save_psd_layer_compression(psd->compression), 0, 0); + memory = minipsd_vips_save_channels_init(&layer_channels, in, doc.depth, + minipsd_vips_save_layer_compression(psd->compression), 0, 0); targets[n++] = &layer_channels; } if (!memory) - memory = vips_foreign_save_psd_channels_init(&composite_channels, in, doc.depth, - psd->compression == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, + memory = minipsd_vips_save_channels_init(&composite_channels, in, doc.depth, + psd->compression == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, 1, alpha); else memset(&composite_channels, 0, sizeof composite_channels); targets[n++] = &composite_channels; - if (!memory && vips_foreign_save_psd_pass(in, doc.depth, doc.color_mode, targets, n, &memory) && !memory) { - vips_foreign_save_psd_channels_free(&layer_channels); /* the image's own error, reported where it arose */ - vips_foreign_save_psd_channels_free(&composite_channels); + if (!memory && minipsd_vips_save_pass(in, doc.depth, doc.color_mode, targets, n, &memory) && !memory) { + minipsd_vips_save_channels_free(&layer_channels); /* the image's own error, reported where it arose */ + minipsd_vips_save_channels_free(&composite_channels); return -1; } - if (!memory) memory = vips_foreign_save_psd_finish(&layer_channels) || vips_foreign_save_psd_finish(&composite_channels); + if (!memory) memory = minipsd_vips_save_finish(&layer_channels) || minipsd_vips_save_finish(&composite_channels); if (memory) { - vips_foreign_save_psd_channels_free(&layer_channels); - vips_foreign_save_psd_channels_free(&composite_channels); + minipsd_vips_save_channels_free(&layer_channels); + minipsd_vips_save_channels_free(&composite_channels); vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(MINIPSD_ERR_ALLOC)); return -1; } @@ -562,9 +561,9 @@ static int vips_foreign_save_psd_build(VipsObject *object) if (vips_image_get_typeof(in, "psd-pixel-aspect-ratio") && !vips_image_get_double(in, "psd-pixel-aspect-ratio", &ratio) && ratio > 0.0) doc.pixel_aspect_ratio = ratio; - vips_foreign_save_psd_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); - vips_foreign_save_psd_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); - vips_foreign_save_psd_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); + minipsd_vips_save_keep_blob(in, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); + minipsd_vips_save_keep_blob(in, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); + minipsd_vips_save_keep_blob(in, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); doc.resources = resources; doc.resource_count = count; @@ -577,15 +576,15 @@ static int vips_foreign_save_psd_build(VipsObject *object) for (;;) { /* the blocks for the format, again for a PSB when a PSD cannot hold them */ table = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; doc.format = psb ? MINIPSD_PSB : MINIPSD_PSD; - if (layered) vips_foreign_save_psd_layer_blocks(&layer_channels, table, stored); - doc.composite_stored = vips_foreign_save_psd_composite_block(&composite_channels, table); - if (psb || vips_foreign_save_psd_fits_psd(&doc)) break; + if (layered) minipsd_vips_save_layer_blocks(&layer_channels, table, stored); + doc.composite_stored = minipsd_vips_save_composite_block(&composite_channels, table); + if (psb || minipsd_vips_save_fits_psd(&doc)) break; psb = 1; } - rc = minipsd_write(&doc, NULL, vips_foreign_save_psd_to_target, psd->target); - vips_foreign_save_psd_channels_free(&layer_channels); - vips_foreign_save_psd_channels_free(&composite_channels); + rc = minipsd_write(&doc, NULL, minipsd_vips_save_to_target, psd->target); + minipsd_vips_save_channels_free(&layer_channels); + minipsd_vips_save_channels_free(&composite_channels); if (rc != MINIPSD_OK) { vips_error(VIPS_OBJECT_GET_CLASS(object)->nickname, "%s", minipsd_error_string(rc)); return -1; @@ -593,18 +592,18 @@ static int vips_foreign_save_psd_build(VipsObject *object) return vips_target_end(psd->target); } -static void vips_foreign_save_psd_class_init(VipsForeignSavePsdClass *class) +static void minipsd_vips_save_class_init(MinipsdVipsSaveClass *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignSaveClass *save_class = (VipsForeignSaveClass *)class; - gobject_class->dispose = vips_foreign_save_psd_dispose; - vips__foreign_psd_class_properties(class); + gobject_class->dispose = minipsd_vips_save_dispose; + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_base"; object_class->description = "save PSD and PSB with minipsd"; - object_class->build = vips_foreign_save_psd_build; + object_class->build = minipsd_vips_save_build; /* Any bands: the build takes grey, RGB and CMYK, with or without alpha, and refuses the rest. */ #if VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 17) @@ -612,152 +611,152 @@ static void vips_foreign_save_psd_class_init(VipsForeignSavePsdClass *class) #else save_class->saveable = VIPS_SAVEABLE_ANY; #endif - save_class->format_table = vips_foreign_save_psd_save_formats; + save_class->format_table = minipsd_vips_save_save_formats; PSD_ARGS_BEGIN VIPS_ARG_BOOL(class, "psb", 20, "PSB", "Save as PSB (also for a .psb file, or a side past 30000)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, psb), FALSE); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSave, psb), FALSE); VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layer's compression: rle, zip, zip-predicted, or none (the composite too)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, compression), - vips_foreign_psd_compression_get_type(), VIPS_FOREIGN_PSD_COMPRESSION_RLE); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSave, compression), + minipsd_vips_compression_get_type(), MINIPSD_VIPS_COMPRESSION_RLE); VIPS_ARG_STRING(class, "layer_name", 22, "Layer name", "The layer's name, UTF-8", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, layer_name), PSD_LAYER_NAME); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSave, layer_name), PSD_LAYER_NAME); VIPS_ARG_BOOL(class, "layered", 23, "Layered", "Without alpha, a layer too, without transparency (else flat)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsd, layered), FALSE); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSave, layered), FALSE); PSD_ARGS_END } -static void vips_foreign_save_psd_init(VipsForeignSavePsd *psd) +static void minipsd_vips_save_init(MinipsdVipsSave *psd) { (void)psd; } /* --- psdsave_target ------------------------------------------------------------------------ */ -typedef struct _VipsForeignSavePsdTarget { - VipsForeignSavePsd parent_object; +typedef struct _MinipsdVipsSaveTarget { + MinipsdVipsSave parent_object; VipsTarget *target; -} VipsForeignSavePsdTarget; +} MinipsdVipsSaveTarget; -typedef VipsForeignSavePsdClass VipsForeignSavePsdTargetClass; +typedef MinipsdVipsSaveClass MinipsdVipsSaveTargetClass; -G_DEFINE_TYPE(VipsForeignSavePsdTarget, vips_foreign_save_psd_target, vips_foreign_save_psd_get_type()) +G_DEFINE_TYPE(MinipsdVipsSaveTarget, minipsd_vips_save_target, minipsd_vips_save_get_type()) -static int vips_foreign_save_psd_target_build(VipsObject *object) +static int minipsd_vips_save_target_build(VipsObject *object) { - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; - VipsForeignSavePsdTarget *target = (VipsForeignSavePsdTarget *)object; + MinipsdVipsSave *psd = (MinipsdVipsSave *)object; + MinipsdVipsSaveTarget *target = (MinipsdVipsSaveTarget *)object; if (target->target) { psd->target = target->target; g_object_ref(psd->target); } - return VIPS_OBJECT_CLASS(vips_foreign_save_psd_target_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_save_target_parent_class)->build(object); } -static void vips_foreign_save_psd_target_class_init(VipsForeignSavePsdTargetClass *class) +static void minipsd_vips_save_target_class_init(MinipsdVipsSaveTargetClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignClass *foreign_class = (VipsForeignClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_target"; object_class->description = "save PSD and PSB to a target with minipsd"; - object_class->build = vips_foreign_save_psd_target_build; - foreign_class->suffs = vips__foreign_psd_suffs; + object_class->build = minipsd_vips_save_target_build; + foreign_class->suffs = minipsd_vips__suffs; PSD_ARGS_BEGIN VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdTarget, target), VIPS_TYPE_TARGET); + G_STRUCT_OFFSET(MinipsdVipsSaveTarget, target), VIPS_TYPE_TARGET); PSD_ARGS_END } -static void vips_foreign_save_psd_target_init(VipsForeignSavePsdTarget *target) +static void minipsd_vips_save_target_init(MinipsdVipsSaveTarget *target) { (void)target; } /* --- psdsave (a file) ---------------------------------------------------------------------- */ -typedef struct _VipsForeignSavePsdFile { - VipsForeignSavePsd parent_object; +typedef struct _MinipsdVipsSaveFile { + MinipsdVipsSave parent_object; char *filename; -} VipsForeignSavePsdFile; +} MinipsdVipsSaveFile; -typedef VipsForeignSavePsdClass VipsForeignSavePsdFileClass; +typedef MinipsdVipsSaveClass MinipsdVipsSaveFileClass; -G_DEFINE_TYPE(VipsForeignSavePsdFile, vips_foreign_save_psd_file, vips_foreign_save_psd_get_type()) +G_DEFINE_TYPE(MinipsdVipsSaveFile, minipsd_vips_save_file, minipsd_vips_save_get_type()) -static int vips_foreign_save_psd_file_build(VipsObject *object) +static int minipsd_vips_save_file_build(VipsObject *object) { - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; - VipsForeignSavePsdFile *file = (VipsForeignSavePsdFile *)object; + MinipsdVipsSave *psd = (MinipsdVipsSave *)object; + MinipsdVipsSaveFile *file = (MinipsdVipsSaveFile *)object; if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_save_psd_file_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_save_file_parent_class)->build(object); } -static void vips_foreign_save_psd_file_class_init(VipsForeignSavePsdFileClass *class) +static void minipsd_vips_save_file_class_init(MinipsdVipsSaveFileClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignClass *foreign_class = (VipsForeignClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdsave"; object_class->description = "save PSD and PSB to a file with minipsd"; - object_class->build = vips_foreign_save_psd_file_build; - foreign_class->suffs = vips__foreign_psd_suffs; + object_class->build = minipsd_vips_save_file_build; + foreign_class->suffs = minipsd_vips__suffs; PSD_ARGS_BEGIN VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdFile, filename), NULL); + G_STRUCT_OFFSET(MinipsdVipsSaveFile, filename), NULL); PSD_ARGS_END } -static void vips_foreign_save_psd_file_init(VipsForeignSavePsdFile *file) +static void minipsd_vips_save_file_init(MinipsdVipsSaveFile *file) { (void)file; } /* --- psdsave_buffer ------------------------------------------------------------------------ */ -typedef struct _VipsForeignSavePsdBuffer { - VipsForeignSavePsd parent_object; +typedef struct _MinipsdVipsSaveBuffer { + MinipsdVipsSave parent_object; VipsArea *buf; -} VipsForeignSavePsdBuffer; +} MinipsdVipsSaveBuffer; -typedef VipsForeignSavePsdClass VipsForeignSavePsdBufferClass; +typedef MinipsdVipsSaveClass MinipsdVipsSaveBufferClass; -G_DEFINE_TYPE(VipsForeignSavePsdBuffer, vips_foreign_save_psd_buffer, vips_foreign_save_psd_get_type()) +G_DEFINE_TYPE(MinipsdVipsSaveBuffer, minipsd_vips_save_buffer, minipsd_vips_save_get_type()) -static int vips_foreign_save_psd_buffer_build(VipsObject *object) +static int minipsd_vips_save_buffer_build(VipsObject *object) { - VipsForeignSavePsd *psd = (VipsForeignSavePsd *)object; + MinipsdVipsSave *psd = (MinipsdVipsSave *)object; VipsBlob *blob; if (!(psd->target = vips_target_new_to_memory())) return -1; - if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_buffer_parent_class)->build(object)) return -1; + if (VIPS_OBJECT_CLASS(minipsd_vips_save_buffer_parent_class)->build(object)) return -1; g_object_get(psd->target, "blob", &blob, NULL); g_object_set(object, "buffer", blob, NULL); vips_area_unref(VIPS_AREA(blob)); return 0; } -static void vips_foreign_save_psd_buffer_class_init(VipsForeignSavePsdBufferClass *class) +static void minipsd_vips_save_buffer_class_init(MinipsdVipsSaveBufferClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; VipsForeignClass *foreign_class = (VipsForeignClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_buffer"; object_class->description = "save PSD and PSB to a buffer with minipsd"; - object_class->build = vips_foreign_save_psd_buffer_build; - foreign_class->suffs = vips__foreign_psd_suffs; + object_class->build = minipsd_vips_save_buffer_build; + foreign_class->suffs = minipsd_vips__suffs; PSD_ARGS_BEGIN VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdBuffer, buf), VIPS_TYPE_BLOB); + G_STRUCT_OFFSET(MinipsdVipsSaveBuffer, buf), VIPS_TYPE_BLOB); PSD_ARGS_END } -static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) +static void minipsd_vips_save_buffer_init(MinipsdVipsSaveBuffer *buffer) { (void)buffer; } @@ -771,34 +770,34 @@ static void vips_foreign_save_psd_buffer_init(VipsForeignSavePsdBuffer *buffer) one. The composite is given, or made by vips_composite from the visible layers. */ #define PSD_BLEND_KEY_SIZE 4 /* a blend mode's key: four characters, such as "mul " */ -#define PSD_COMPOSE_STEPS 9 /* the images vips_foreign_save_psd_compose makes for a layer */ +#define PSD_COMPOSE_STEPS 9 /* the images minipsd_vips_save_compose makes for a layer */ #define PSD_MASK_DISABLED 0x02 /* a mask's flag: disabled */ #define PSD_DIVIDER_NAME "" /* a divider's name, as Photoshop names it */ -typedef struct _VipsForeignSavePsdLayers { +typedef struct _MinipsdVipsSaveLayers { VipsOperation parent_object; VipsTarget *target; /* set by the subclass's build */ VipsArrayImage *in; /* the layers, bottom first */ VipsImage *composite; /* the option: the composite, as given */ - VipsForeignPsdCompression compression; + MinipsdVipsCompression compression; gboolean psb; int width, height; /* the canvas: 0, from the composite or the layers */ int left, top; /* where the canvas lies in the layers' coordinates */ -} VipsForeignSavePsdLayers; +} MinipsdVipsSaveLayers; -typedef VipsOperationClass VipsForeignSavePsdLayersClass; +typedef VipsOperationClass MinipsdVipsSaveLayersClass; -G_DEFINE_ABSTRACT_TYPE(VipsForeignSavePsdLayers, vips_foreign_save_psd_layers, VIPS_TYPE_OPERATION) +G_DEFINE_ABSTRACT_TYPE(MinipsdVipsSaveLayers, minipsd_vips_save_layers, VIPS_TYPE_OPERATION) -static void vips_foreign_save_psd_layers_dispose(GObject *gobject) +static void minipsd_vips_save_layers_dispose(GObject *gobject) { - VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)gobject; + MinipsdVipsSaveLayers *psd = (MinipsdVipsSaveLayers *)gobject; VIPS_UNREF(psd->target); - G_OBJECT_CLASS(vips_foreign_save_psd_layers_parent_class)->dispose(gobject); + G_OBJECT_CLASS(minipsd_vips_save_layers_parent_class)->dispose(gobject); } /* An int field of an image, or fallback without it. */ -static int vips_foreign_save_psd_field(VipsImage *image, const char *name, int fallback) +static int minipsd_vips_save_field(VipsImage *image, const char *name, int fallback) { int value; return vips_image_get_typeof(image, name) && !vips_image_get_int(image, name, &value) ? value : fallback; @@ -806,25 +805,25 @@ static int vips_foreign_save_psd_field(VipsImage *image, const char *name, int f /* A record's MINIPSD_SECTION_* from its psd-layer-section: a layer without one; -1 for another text. */ -static int vips_foreign_save_psd_section(VipsImage *image) +static int minipsd_vips_save_section(VipsImage *image) { const char *text; int k; if (!vips_image_get_typeof(image, "psd-layer-section") || vips_image_get_string(image, "psd-layer-section", &text)) return MINIPSD_SECTION_LAYER; for (k = MINIPSD_SECTION_GROUP; k < PSD_SECTIONS; ++k) - if (strcmp(text, vips__foreign_psd_sections[k]) == 0) return k; + if (strcmp(text, minipsd_vips__sections[k]) == 0) return k; return -1; } /* No pixels: an empty layer, or a group's record. */ -static int vips_foreign_save_psd_empty(VipsImage *image) +static int minipsd_vips_save_empty(VipsImage *image) { - return vips_foreign_save_psd_field(image, "psd-layer-empty", 0) != 0 || vips_foreign_save_psd_section(image) != MINIPSD_SECTION_LAYER; + return minipsd_vips_save_field(image, "psd-layer-empty", 0) != 0 || minipsd_vips_save_section(image) != MINIPSD_SECTION_LAYER; } /* The image's mask (a new reference), or NULL without one. */ -static VipsImage *vips_foreign_save_psd_mask(VipsImage *image) +static VipsImage *minipsd_vips_save_mask(VipsImage *image) { VipsImage *mask; return vips_image_get_typeof(image, "psd-layer-mask") && !vips_image_get_image(image, "psd-layer-mask", &mask) ? mask : NULL; @@ -832,7 +831,7 @@ static VipsImage *vips_foreign_save_psd_mask(VipsImage *image) /* A PSD blend key as a libvips blend mode, as the spec's table maps them (Working with libvips); a key libvips has no counterpart for is drawn "over", as minipsd's own composer draws it. */ -static VipsBlendMode vips_foreign_save_psd_blend_mode(const char key[PSD_BLEND_KEY_SIZE]) +static VipsBlendMode minipsd_vips_save_blend_mode(const char key[PSD_BLEND_KEY_SIZE]) { static const struct { char key[PSD_BLEND_KEY_SIZE + 1]; @@ -855,15 +854,15 @@ static VipsBlendMode vips_foreign_save_psd_blend_mode(const char key[PSD_BLEND_K /* Each layer's visibility and opacity (0 to 1) with its groups': a hidden group hides its layers, its opacity multiplies theirs. Walked top down: a group's top record comes before its layers, its divider after them. */ -static void vips_foreign_save_psd_groups(VipsImage **in, int n, int *hidden, double *opacity) +static void minipsd_vips_save_groups(VipsImage **in, int n, int *hidden, double *opacity) { int *group_hidden = g_new0(int, (gsize)n + 1), level = 0, i; double *group_opacity = g_new(double, (gsize)n + 1); group_opacity[0] = 1.0; for (i = n - 1; i >= 0; --i) { - const int section = vips_foreign_save_psd_section(in[i]); - const int own = vips_foreign_save_psd_field(in[i], "psd-layer-hidden", 0) != 0; - const double alpha = VIPS_CLIP(0, vips_foreign_save_psd_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX) / (double)UCHAR_MAX; + const int section = minipsd_vips_save_section(in[i]); + const int own = minipsd_vips_save_field(in[i], "psd-layer-hidden", 0) != 0; + const double alpha = VIPS_CLIP(0, minipsd_vips_save_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX) / (double)UCHAR_MAX; hidden[i] = group_hidden[level] || own; opacity[i] = group_opacity[level] * alpha; if (section == MINIPSD_SECTION_GROUP || section == MINIPSD_SECTION_GROUP_CLOSED) { @@ -884,7 +883,7 @@ static void vips_foreign_save_psd_groups(VipsImage **in, int n, int *hidden, dou minipsd mattes a composite without it). Its bands and format are the layers'. Clipping, a group's blend mode and mask, and a blend mode libvips lacks are drawn as normal (pass through for a group). NULL with an error. */ -static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, VipsImage **in, int n, const VipsImage *ref, +static VipsImage *minipsd_vips_save_compose(MinipsdVipsSaveLayers *psd, VipsImage **in, int n, const VipsImage *ref, size_t colors, int mode, int depth, int width, int height, int alpha) { const double max = depth == 32 ? 1.0 : depth == 16 ? (double)UINT16_MAX : (double)UCHAR_MAX; @@ -901,7 +900,7 @@ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, V int count = 0, i, k, rc; if (!modes || !xs || !ys || !hidden || !opacity) return NULL; - vips_foreign_save_psd_groups(in, n, hidden, opacity); + minipsd_vips_save_groups(in, n, hidden, opacity); if (mode == MINIPSD_MODE_CMYK && depth != 8) { /* libvips takes CMYK's alpha as 8-bit */ vips_error(VIPS_OBJECT_GET_CLASS(psd)->nickname, "%s", "a 16-bit CMYK document needs its composite given"); return NULL; @@ -916,15 +915,15 @@ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, V VipsImage **s = t + 2 + PSD_COMPOSE_STEPS * i; /* this layer's steps */ char key[PSD_BLEND_KEY_SIZE] = { 'n', 'o', 'r', 'm' }; const char *given; - if (vips_foreign_save_psd_empty(layer) || hidden[i]) continue; + if (minipsd_vips_save_empty(layer) || hidden[i]) continue; if (layer->Bands == (int)colors) { /* without transparency: opaque */ if (vips_bandjoin_const1(layer, &s[0], max, NULL)) return NULL; layer = s[0]; } /* The mask over the layer's rectangle, its colour outside its own. */ - if ((mask = vips_foreign_save_psd_mask(in[i])) && !vips_foreign_save_psd_field(in[i], "psd-layer-mask-disabled", 0)) { - const double outside = vips_foreign_save_psd_field(in[i], "psd-layer-mask-default-color", UCHAR_MAX) ? max : 0.0; - if (vips_foreign_save_psd_empty(mask)) + if ((mask = minipsd_vips_save_mask(in[i])) && !minipsd_vips_save_field(in[i], "psd-layer-mask-disabled", 0)) { + const double outside = minipsd_vips_save_field(in[i], "psd-layer-mask-default-color", UCHAR_MAX) ? max : 0.0; + if (minipsd_vips_save_empty(mask)) rc = vips_black(&s[1], layer->Xsize, layer->Ysize, NULL) || vips_linear1(s[1], &s[2], 1.0, outside, NULL); else { VipsArrayDouble *background = vips_array_double_new(&outside, 1); @@ -956,7 +955,7 @@ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, V if (vips_image_get_typeof(in[i], "psd-layer-blend-mode") && !vips_image_get_string(in[i], "psd-layer-blend-mode", &given) && strlen(given) == PSD_BLEND_KEY_SIZE) memcpy(key, given, PSD_BLEND_KEY_SIZE); - modes[count - 1] = (int)vips_foreign_save_psd_blend_mode(key); + modes[count - 1] = (int)minipsd_vips_save_blend_mode(key); xs[count - 1] = in[i]->Xoffset - psd->left; /* where the layer goes; it may reach past the canvas */ ys[count - 1] = in[i]->Yoffset - psd->top; ++count; @@ -987,12 +986,12 @@ static VipsImage *vips_foreign_save_psd_compose(VipsForeignSavePsdLayers *psd, V return out; } -static int vips_foreign_save_psd_layers_build(VipsObject *object) +static int minipsd_vips_save_layers_build(VipsObject *object) { - VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; + MinipsdVipsSaveLayers *psd = (MinipsdVipsSaveLayers *)object; const char *nickname = VIPS_OBJECT_GET_CLASS(object)->nickname; VipsImage **in, *ref = NULL, *composite = NULL; - VipsForeignSavePsdChannels *channels = NULL, composite_channels; + MinipsdVipsSaveChannels *channels = NULL, composite_channels; minipsd_layer *layers = NULL; minipsd_mask *masks = NULL; VipsImage **mask_images = NULL; @@ -1007,7 +1006,7 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) int n, i, mode, depth, compression, section, width, height, alpha = 0, psb, memory = 0, rc = MINIPSD_OK, failed = 0; - if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_parent_class)->build(object)) return -1; + if (VIPS_OBJECT_CLASS(minipsd_vips_save_layers_parent_class)->build(object)) return -1; in = vips_array_image_get(psd->in, &n); if (n < 1) { vips_error(nickname, "%s", "no layers"); @@ -1017,21 +1016,21 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) /* The mode and depth: the first layer with pixels decides, else the composite, else the first layer; every other image must agree. */ for (i = 0; i < n && !ref; ++i) - if (!vips_foreign_save_psd_empty(in[i])) ref = in[i]; + if (!minipsd_vips_save_empty(in[i])) ref = in[i]; if (!ref) ref = psd->composite ? psd->composite : in[0]; - mode = vips_foreign_save_psd_mode(ref, &colors); - depth = vips_foreign_save_psd_depth(ref); + mode = minipsd_vips_save_mode(ref, &colors); + depth = minipsd_vips_save_depth(ref); if (mode < 0 || !depth) { vips_error(nickname, "%s", "PSD takes grey, RGB or CMYK, each with or without alpha, as uchar, ushort or float"); return -1; } for (i = 0; i < n; ++i) { - if (vips_foreign_save_psd_section(in[i]) < 0) { + if (minipsd_vips_save_section(in[i]) < 0) { vips_error(nickname, "layer %d: psd-layer-section is not \"open\", \"closed\" or \"end\"", i); return -1; } - if (vips_foreign_save_psd_empty(in[i])) continue; - if (vips_foreign_save_psd_depth(in[i]) != depth || (in[i]->Bands != (int)colors && in[i]->Bands != (int)colors + 1) + if (minipsd_vips_save_empty(in[i])) continue; + if (minipsd_vips_save_depth(in[i]) != depth || (in[i]->Bands != (int)colors && in[i]->Bands != (int)colors + 1) || (mode == MINIPSD_MODE_CMYK) != (in[i]->Type == VIPS_INTERPRETATION_CMYK)) { vips_error(nickname, "layer %d: not the mode or depth of the others (no layer is converted)", i); return -1; @@ -1048,7 +1047,7 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) height = psd->composite->Ysize; } else { for (i = 0, width = height = 0; i < n; ++i) - if (!vips_foreign_save_psd_empty(in[i])) { + if (!minipsd_vips_save_empty(in[i])) { width = VIPS_MAX(width, in[i]->Xoffset + in[i]->Xsize - psd->left); height = VIPS_MAX(height, in[i]->Yoffset + in[i]->Ysize - psd->top); } @@ -1059,7 +1058,7 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) return -1; } if (psd->composite - && (psd->composite->Xsize != width || psd->composite->Ysize != height || vips_foreign_save_psd_depth(psd->composite) != depth + && (psd->composite->Xsize != width || psd->composite->Ysize != height || minipsd_vips_save_depth(psd->composite) != depth || (psd->composite->Bands != (int)colors && psd->composite->Bands != (int)colors + 1))) { vips_error(nickname, "%s", "the composite is not the canvas's size, or not the layers' mode and depth"); return -1; @@ -1068,8 +1067,8 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) /* Each layer read once, its channels encoded as its rows come, then its mask; then the composite. channels holds the layers', then the masks'. */ memset(&composite_channels, 0, sizeof composite_channels); - compression = vips_foreign_save_psd_layer_compression(psd->compression); - channels = g_new0(VipsForeignSavePsdChannels, 2 * (gsize)n); + compression = minipsd_vips_save_layer_compression(psd->compression); + channels = g_new0(MinipsdVipsSaveChannels, 2 * (gsize)n); layers = g_new0(minipsd_layer, (gsize)n); masks = g_new0(minipsd_mask, (gsize)n); mask_images = g_new0(VipsImage *, (gsize)n); @@ -1077,16 +1076,16 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) names = g_new0(char *, (gsize)n); for (i = 0; i < n && !failed; ++i) { minipsd_layer *l = &layers[i]; - VipsImage *mask = mask_images[i] = vips_foreign_save_psd_mask(in[i]); - section = vips_foreign_save_psd_section(in[i]); + VipsImage *mask = mask_images[i] = minipsd_vips_save_mask(in[i]); + section = minipsd_vips_save_section(in[i]); if (vips_image_get_typeof(in[i], "psd-layer-name") && !vips_image_get_string(in[i], "psd-layer-name", &text)) l->name = text; else l->name = section == MINIPSD_SECTION_GROUP_END ? PSD_DIVIDER_NAME : (names[i] = g_strdup_printf("Layer %d", i + 1)); l->section_divider_type = (uint8_t)section; - l->opacity = (uint8_t)VIPS_CLIP(0, vips_foreign_save_psd_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX); - l->hidden = (uint8_t)(vips_foreign_save_psd_field(in[i], "psd-layer-hidden", 0) != 0); - l->clipping = (uint8_t)(vips_foreign_save_psd_field(in[i], "psd-layer-clipping", 0) != 0); + l->opacity = (uint8_t)VIPS_CLIP(0, minipsd_vips_save_field(in[i], "psd-layer-opacity", UCHAR_MAX), UCHAR_MAX); + l->hidden = (uint8_t)(minipsd_vips_save_field(in[i], "psd-layer-hidden", 0) != 0); + l->clipping = (uint8_t)(minipsd_vips_save_field(in[i], "psd-layer-clipping", 0) != 0); if (vips_image_get_typeof(in[i], "psd-layer-blend-mode") && !vips_image_get_string(in[i], "psd-layer-blend-mode", &text)) { if (strlen(text) != PSD_BLEND_KEY_SIZE) { error = "a psd-layer-blend-mode is not a four-character key, such as \"mul \""; @@ -1098,43 +1097,43 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) memcpy(l->blend_mode, text, PSD_BLEND_KEY_SIZE); } if (mask) { /* one band of the layers' depth, its own rectangle; encoded as the layer is */ - if (mask->Bands != 1 || vips_foreign_save_psd_depth(mask) != depth) { + if (mask->Bands != 1 || minipsd_vips_save_depth(mask) != depth) { error = "a psd-layer-mask is not one band of the layers' depth"; failed = 1; break; } - masks[i].default_color = vips_foreign_save_psd_field(in[i], "psd-layer-mask-default-color", UCHAR_MAX) ? UCHAR_MAX : 0; - masks[i].flags = vips_foreign_save_psd_field(in[i], "psd-layer-mask-disabled", 0) ? PSD_MASK_DISABLED : 0; + masks[i].default_color = minipsd_vips_save_field(in[i], "psd-layer-mask-default-color", UCHAR_MAX) ? UCHAR_MAX : 0; + masks[i].flags = minipsd_vips_save_field(in[i], "psd-layer-mask-disabled", 0) ? PSD_MASK_DISABLED : 0; l->mask = &masks[i]; - if (!vips_foreign_save_psd_empty(mask)) { + if (!minipsd_vips_save_empty(mask)) { masks[i].top = mask->Yoffset - psd->top; masks[i].left = mask->Xoffset - psd->left; masks[i].bottom = masks[i].top + mask->Ysize; masks[i].right = masks[i].left + mask->Xsize; - if (vips_foreign_save_psd_encode_image(&channels[n + i], mask, depth, MINIPSD_MODE_GREY, compression, 0, 0, &memory)) { + if (minipsd_vips_save_encode_image(&channels[n + i], mask, depth, MINIPSD_MODE_GREY, compression, 0, 0, &memory)) { failed = 1; break; } longest = VIPS_MAX(longest, channels[n + i].longest); } } - if (vips_foreign_save_psd_empty(in[i])) continue; /* no pixels, no rectangle: as Photoshop writes one */ + if (minipsd_vips_save_empty(in[i])) continue; /* no pixels, no rectangle: as Photoshop writes one */ l->top = in[i]->Yoffset - psd->top; l->left = in[i]->Xoffset - psd->left; l->bottom = l->top + in[i]->Ysize; l->right = l->left + in[i]->Xsize; l->no_alpha = (uint8_t)(in[i]->Bands == (int)colors); l->stored = &stored[(size_t)i * PSD_MAX_PLANES]; - if (vips_foreign_save_psd_encode_image(&channels[i], in[i], depth, mode, compression, 0, 0, &memory)) failed = 1; + if (minipsd_vips_save_encode_image(&channels[i], in[i], depth, mode, compression, 0, 0, &memory)) failed = 1; longest = VIPS_MAX(longest, channels[i].longest); } if (!failed) { - composite = psd->composite ? psd->composite : vips_foreign_save_psd_compose(psd, in, n, ref, colors, mode, depth, width, height, alpha); + composite = psd->composite ? psd->composite : minipsd_vips_save_compose(psd, in, n, ref, colors, mode, depth, width, height, alpha); if (!composite) failed = 1; } if (!failed) { - if (vips_foreign_save_psd_encode_image(&composite_channels, composite, depth, mode, - psd->compression == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, 1, + if (minipsd_vips_save_encode_image(&composite_channels, composite, depth, mode, + psd->compression == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW : MINIPSD_RLE, 1, composite->Bands > (int)colors, &memory)) failed = 1; longest = VIPS_MAX(longest, composite_channels.longest); @@ -1157,9 +1156,9 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) doc.icc = (const uint8_t *)icc; doc.icc_size = icc_size; } - vips_foreign_save_psd_keep_blob(ref, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); - vips_foreign_save_psd_keep_blob(ref, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); - vips_foreign_save_psd_keep_blob(ref, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); + minipsd_vips_save_keep_blob(ref, VIPS_META_EXIF_NAME, MINIPSD_RES_EXIF_DATA_1, resources, &count); + minipsd_vips_save_keep_blob(ref, VIPS_META_XMP_NAME, MINIPSD_RES_XMP_METADATA, resources, &count); + minipsd_vips_save_keep_blob(ref, VIPS_META_IPTC_NAME, MINIPSD_RES_IPTC_NAA, resources, &count); doc.resources = resources; doc.resource_count = count; @@ -1171,21 +1170,21 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) table = psb ? PSD_COUNT_PSB : PSD_COUNT_PSD; doc.format = psb ? MINIPSD_PSB : MINIPSD_PSD; for (i = 0; i < n; ++i) { - if (layers[i].stored) vips_foreign_save_psd_layer_blocks(&channels[i], table, stored + (size_t)i * PSD_MAX_PLANES); - if (channels[n + i].planes) vips_foreign_save_psd_layer_blocks(&channels[n + i], table, &masks[i].stored); + if (layers[i].stored) minipsd_vips_save_layer_blocks(&channels[i], table, stored + (size_t)i * PSD_MAX_PLANES); + if (channels[n + i].planes) minipsd_vips_save_layer_blocks(&channels[n + i], table, &masks[i].stored); } - doc.composite_stored = vips_foreign_save_psd_composite_block(&composite_channels, table); - if (psb || vips_foreign_save_psd_fits_psd(&doc)) break; + doc.composite_stored = minipsd_vips_save_composite_block(&composite_channels, table); + if (psb || minipsd_vips_save_fits_psd(&doc)) break; psb = 1; } - rc = minipsd_write(&doc, NULL, vips_foreign_save_psd_to_target, psd->target); + rc = minipsd_write(&doc, NULL, minipsd_vips_save_to_target, psd->target); } - for (i = 0; channels && i < 2 * n; ++i) vips_foreign_save_psd_channels_free(&channels[i]); + for (i = 0; channels && i < 2 * n; ++i) minipsd_vips_save_channels_free(&channels[i]); for (i = 0; mask_images && i < n; ++i) VIPS_UNREF(mask_images[i]); g_free(mask_images); g_free(masks); - vips_foreign_save_psd_channels_free(&composite_channels); + minipsd_vips_save_channels_free(&composite_channels); for (i = 0; names && i < n; ++i) g_free(names[i]); g_free(names); g_free(stored); @@ -1199,171 +1198,171 @@ static int vips_foreign_save_psd_layers_build(VipsObject *object) return vips_target_end(psd->target); } -static void vips_foreign_save_psd_layers_class_init(VipsForeignSavePsdLayersClass *class) +static void minipsd_vips_save_layers_class_init(MinipsdVipsSaveLayersClass *class) { GObjectClass *gobject_class = G_OBJECT_CLASS(class); VipsObjectClass *object_class = (VipsObjectClass *)class; VipsOperationClass *operation_class = (VipsOperationClass *)class; - gobject_class->dispose = vips_foreign_save_psd_layers_dispose; - vips__foreign_psd_class_properties(class); + gobject_class->dispose = minipsd_vips_save_layers_dispose; + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_layers_base"; object_class->description = "save images as the layers of a PSD or PSB with minipsd"; - object_class->build = vips_foreign_save_psd_layers_build; + object_class->build = minipsd_vips_save_layers_build; operation_class->flags |= VIPS_OPERATION_NOCACHE; /* it writes: never answered from the cache */ PSD_ARGS_BEGIN VIPS_ARG_BOXED(class, "in", 0, "Input", "The layers, bottom first", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdLayers, in), VIPS_TYPE_ARRAY_IMAGE); + G_STRUCT_OFFSET(MinipsdVipsSaveLayers, in), VIPS_TYPE_ARRAY_IMAGE); VIPS_ARG_IMAGE(class, "composite", 20, "Composite", "The composite, as given; else composed by vips_composite", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, composite)); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, composite)); VIPS_ARG_ENUM(class, "compression", 21, "Compression", "The layers' compression: rle, zip, zip-predicted, or none (the composite too)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, compression), - vips_foreign_psd_compression_get_type(), VIPS_FOREIGN_PSD_COMPRESSION_RLE); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, compression), + minipsd_vips_compression_get_type(), MINIPSD_VIPS_COMPRESSION_RLE); VIPS_ARG_BOOL(class, "psb", 22, "PSB", "Save as PSB (also for a .psb file, or when a PSD cannot hold it)", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, psb), FALSE); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, psb), FALSE); VIPS_ARG_INT(class, "width", 23, "Width", "The canvas's width; 0: the composite's, or every layer's", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, width), 0, MINIPSD_MAX_SIDE_PSB, 0); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, width), 0, MINIPSD_MAX_SIDE_PSB, 0); VIPS_ARG_INT(class, "height", 24, "Height", "The canvas's height; 0: the composite's, or every layer's", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, height), 0, MINIPSD_MAX_SIDE_PSB, 0); + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, height), 0, MINIPSD_MAX_SIDE_PSB, 0); VIPS_ARG_INT(class, "left", 25, "Left", "The canvas's left edge in the layers' coordinates", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, left), -MINIPSD_MAX_SIDE_PSB, + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, left), -MINIPSD_MAX_SIDE_PSB, MINIPSD_MAX_SIDE_PSB, 0); VIPS_ARG_INT(class, "top", 26, "Top", "The canvas's top edge in the layers' coordinates", - VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(VipsForeignSavePsdLayers, top), -MINIPSD_MAX_SIDE_PSB, + VIPS_ARGUMENT_OPTIONAL_INPUT, G_STRUCT_OFFSET(MinipsdVipsSaveLayers, top), -MINIPSD_MAX_SIDE_PSB, MINIPSD_MAX_SIDE_PSB, 0); PSD_ARGS_END } -static void vips_foreign_save_psd_layers_init(VipsForeignSavePsdLayers *psd) +static void minipsd_vips_save_layers_init(MinipsdVipsSaveLayers *psd) { (void)psd; } /* --- psdsave_layers_target, psdsave_layers (a file), psdsave_layers_buffer ------------------ */ -typedef struct _VipsForeignSavePsdLayersTarget { - VipsForeignSavePsdLayers parent_object; +typedef struct _MinipsdVipsSaveLayersTarget { + MinipsdVipsSaveLayers parent_object; VipsTarget *target; -} VipsForeignSavePsdLayersTarget; +} MinipsdVipsSaveLayersTarget; -typedef VipsForeignSavePsdLayersClass VipsForeignSavePsdLayersTargetClass; +typedef MinipsdVipsSaveLayersClass MinipsdVipsSaveLayersTargetClass; -G_DEFINE_TYPE(VipsForeignSavePsdLayersTarget, vips_foreign_save_psd_layers_target, vips_foreign_save_psd_layers_get_type()) +G_DEFINE_TYPE(MinipsdVipsSaveLayersTarget, minipsd_vips_save_layers_target, minipsd_vips_save_layers_get_type()) -static int vips_foreign_save_psd_layers_target_build(VipsObject *object) +static int minipsd_vips_save_layers_target_build(VipsObject *object) { - VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; - VipsForeignSavePsdLayersTarget *target = (VipsForeignSavePsdLayersTarget *)object; + MinipsdVipsSaveLayers *psd = (MinipsdVipsSaveLayers *)object; + MinipsdVipsSaveLayersTarget *target = (MinipsdVipsSaveLayersTarget *)object; if (target->target) { psd->target = target->target; g_object_ref(psd->target); } - return VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_target_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_save_layers_target_parent_class)->build(object); } -static void vips_foreign_save_psd_layers_target_class_init(VipsForeignSavePsdLayersTargetClass *class) +static void minipsd_vips_save_layers_target_class_init(MinipsdVipsSaveLayersTargetClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_layers_target"; object_class->description = "save images as the layers of a PSD or PSB to a target with minipsd"; - object_class->build = vips_foreign_save_psd_layers_target_build; + object_class->build = minipsd_vips_save_layers_target_build; PSD_ARGS_BEGIN VIPS_ARG_OBJECT(class, "target", 1, "Target", "Target to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdLayersTarget, target), VIPS_TYPE_TARGET); + G_STRUCT_OFFSET(MinipsdVipsSaveLayersTarget, target), VIPS_TYPE_TARGET); PSD_ARGS_END } -static void vips_foreign_save_psd_layers_target_init(VipsForeignSavePsdLayersTarget *target) +static void minipsd_vips_save_layers_target_init(MinipsdVipsSaveLayersTarget *target) { (void)target; } -typedef struct _VipsForeignSavePsdLayersFile { - VipsForeignSavePsdLayers parent_object; +typedef struct _MinipsdVipsSaveLayersFile { + MinipsdVipsSaveLayers parent_object; char *filename; -} VipsForeignSavePsdLayersFile; +} MinipsdVipsSaveLayersFile; -typedef VipsForeignSavePsdLayersClass VipsForeignSavePsdLayersFileClass; +typedef MinipsdVipsSaveLayersClass MinipsdVipsSaveLayersFileClass; -G_DEFINE_TYPE(VipsForeignSavePsdLayersFile, vips_foreign_save_psd_layers_file, vips_foreign_save_psd_layers_get_type()) +G_DEFINE_TYPE(MinipsdVipsSaveLayersFile, minipsd_vips_save_layers_file, minipsd_vips_save_layers_get_type()) -static int vips_foreign_save_psd_layers_file_build(VipsObject *object) +static int minipsd_vips_save_layers_file_build(VipsObject *object) { - VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; - VipsForeignSavePsdLayersFile *file = (VipsForeignSavePsdLayersFile *)object; + MinipsdVipsSaveLayers *psd = (MinipsdVipsSaveLayers *)object; + MinipsdVipsSaveLayersFile *file = (MinipsdVipsSaveLayersFile *)object; if (file->filename && !(psd->target = vips_target_new_to_file(file->filename))) return -1; - return VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_file_parent_class)->build(object); + return VIPS_OBJECT_CLASS(minipsd_vips_save_layers_file_parent_class)->build(object); } -static void vips_foreign_save_psd_layers_file_class_init(VipsForeignSavePsdLayersFileClass *class) +static void minipsd_vips_save_layers_file_class_init(MinipsdVipsSaveLayersFileClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_layers"; object_class->description = "save images as the layers of a PSD or PSB to a file with minipsd"; - object_class->build = vips_foreign_save_psd_layers_file_build; + object_class->build = minipsd_vips_save_layers_file_build; PSD_ARGS_BEGIN VIPS_ARG_STRING(class, "filename", 1, "Filename", "Filename to save to", VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdLayersFile, filename), NULL); + G_STRUCT_OFFSET(MinipsdVipsSaveLayersFile, filename), NULL); PSD_ARGS_END } -static void vips_foreign_save_psd_layers_file_init(VipsForeignSavePsdLayersFile *file) +static void minipsd_vips_save_layers_file_init(MinipsdVipsSaveLayersFile *file) { (void)file; } -typedef struct _VipsForeignSavePsdLayersBuffer { - VipsForeignSavePsdLayers parent_object; +typedef struct _MinipsdVipsSaveLayersBuffer { + MinipsdVipsSaveLayers parent_object; VipsArea *buf; -} VipsForeignSavePsdLayersBuffer; +} MinipsdVipsSaveLayersBuffer; -typedef VipsForeignSavePsdLayersClass VipsForeignSavePsdLayersBufferClass; +typedef MinipsdVipsSaveLayersClass MinipsdVipsSaveLayersBufferClass; -G_DEFINE_TYPE(VipsForeignSavePsdLayersBuffer, vips_foreign_save_psd_layers_buffer, vips_foreign_save_psd_layers_get_type()) +G_DEFINE_TYPE(MinipsdVipsSaveLayersBuffer, minipsd_vips_save_layers_buffer, minipsd_vips_save_layers_get_type()) -static int vips_foreign_save_psd_layers_buffer_build(VipsObject *object) +static int minipsd_vips_save_layers_buffer_build(VipsObject *object) { - VipsForeignSavePsdLayers *psd = (VipsForeignSavePsdLayers *)object; + MinipsdVipsSaveLayers *psd = (MinipsdVipsSaveLayers *)object; VipsBlob *blob; if (!(psd->target = vips_target_new_to_memory())) return -1; - if (VIPS_OBJECT_CLASS(vips_foreign_save_psd_layers_buffer_parent_class)->build(object)) return -1; + if (VIPS_OBJECT_CLASS(minipsd_vips_save_layers_buffer_parent_class)->build(object)) return -1; g_object_get(psd->target, "blob", &blob, NULL); g_object_set(object, "buffer", blob, NULL); vips_area_unref(VIPS_AREA(blob)); return 0; } -static void vips_foreign_save_psd_layers_buffer_class_init(VipsForeignSavePsdLayersBufferClass *class) +static void minipsd_vips_save_layers_buffer_class_init(MinipsdVipsSaveLayersBufferClass *class) { VipsObjectClass *object_class = (VipsObjectClass *)class; - vips__foreign_psd_class_properties(class); + minipsd_vips__class_properties(class); object_class->nickname = "psdsave_layers_buffer"; object_class->description = "save images as the layers of a PSD or PSB to a buffer with minipsd"; - object_class->build = vips_foreign_save_psd_layers_buffer_build; + object_class->build = minipsd_vips_save_layers_buffer_build; PSD_ARGS_BEGIN VIPS_ARG_BOXED(class, "buffer", 1, "Buffer", "Buffer to save to", VIPS_ARGUMENT_REQUIRED_OUTPUT, - G_STRUCT_OFFSET(VipsForeignSavePsdLayersBuffer, buf), VIPS_TYPE_BLOB); + G_STRUCT_OFFSET(MinipsdVipsSaveLayersBuffer, buf), VIPS_TYPE_BLOB); PSD_ARGS_END } -static void vips_foreign_save_psd_layers_buffer_init(VipsForeignSavePsdLayersBuffer *buffer) +static void minipsd_vips_save_layers_buffer_init(MinipsdVipsSaveLayersBuffer *buffer) { (void)buffer; } /* --- The C calls, as libvips has one beside each operation ---------------------------------- */ -int vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) +int minipsd_vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) { VipsArrayImage *array = vips_array_image_new(in, n); va_list ap; @@ -1375,7 +1374,7 @@ int vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) return result; } -int vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) +int minipsd_vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) { VipsArrayImage *array = vips_array_image_new(in, n); va_list ap; @@ -1387,7 +1386,7 @@ int vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) return result; } -int vips_psdsave_layers_buffer(VipsImage **in, int n, void **buf, size_t *len, ...) +int minipsd_vips_psdsave_layers_buffer(VipsImage **in, int n, void **buf, size_t *len, ...) { VipsArrayImage *array = vips_array_image_new(in, n); VipsArea *area = NULL; diff --git a/libvips-plugin/minipsd_vips.c b/libvips-plugin/minipsd_vips.c index b565d1a..3b3e4c1 100644 --- a/libvips-plugin/minipsd_vips.c +++ b/libvips-plugin/minipsd_vips.c @@ -16,14 +16,14 @@ int minipsd_vips_init(void) { /* Registering a type is enough: libvips finds loaders and savers among VipsForeignLoad's and VipsForeignSave's subclasses. */ - (void)vips_foreign_load_psd_file_get_type(); - (void)vips_foreign_load_psd_buffer_get_type(); - (void)vips_foreign_load_psd_source_get_type(); - (void)vips_foreign_save_psd_file_get_type(); - (void)vips_foreign_save_psd_buffer_get_type(); - (void)vips_foreign_save_psd_target_get_type(); - (void)vips_foreign_save_psd_layers_file_get_type(); - (void)vips_foreign_save_psd_layers_buffer_get_type(); - (void)vips_foreign_save_psd_layers_target_get_type(); + (void)minipsd_vips_load_file_get_type(); + (void)minipsd_vips_load_buffer_get_type(); + (void)minipsd_vips_load_source_get_type(); + (void)minipsd_vips_save_file_get_type(); + (void)minipsd_vips_save_buffer_get_type(); + (void)minipsd_vips_save_target_get_type(); + (void)minipsd_vips_save_layers_file_get_type(); + (void)minipsd_vips_save_layers_buffer_get_type(); + (void)minipsd_vips_save_layers_target_get_type(); return 0; } diff --git a/libvips-plugin/minipsd_vips.h b/libvips-plugin/minipsd_vips.h index 6c56a5b..9990c13 100644 --- a/libvips-plugin/minipsd_vips.h +++ b/libvips-plugin/minipsd_vips.h @@ -7,7 +7,7 @@ * minipsd_vips_init() once, after VIPS_INIT(). vips_image_new_from_file("x.psd"), * vips_image_write_to_file(image, "x.psd") and the other ways libvips finds a loader or a saver * then find these. A saver writes one image; a layered document is psdsave_layers, an - * operation over an array of images, one a layer (vips_psdsave_layers below). + * operation over an array of images, one a layer (minipsd_vips_psdsave_layers below). * * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha @@ -28,13 +28,13 @@ extern "C" { * From the command line or an option string, by its nick: `compression=zip`. */ typedef enum { - VIPS_FOREIGN_PSD_COMPRESSION_RLE = 0, /*!< PackBits, as Photoshop writes 8-bit layers: the default. */ - VIPS_FOREIGN_PSD_COMPRESSION_ZIP = 1, /*!< zlib, without prediction. */ - VIPS_FOREIGN_PSD_COMPRESSION_NONE = 2, /*!< Raw: the largest file. */ - VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED = 3 /*!< zlib of each sample's difference from its left + MINIPSD_VIPS_COMPRESSION_RLE = 0, /*!< PackBits, as Photoshop writes 8-bit layers: the default. */ + MINIPSD_VIPS_COMPRESSION_ZIP = 1, /*!< zlib, without prediction. */ + MINIPSD_VIPS_COMPRESSION_NONE = 2, /*!< Raw: the largest file. */ + MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED = 3 /*!< zlib of each sample's difference from its left neighbour, as Photoshop writes 16-bit layers: the smaller file at 16 and 32 bits. */ -} VipsForeignPsdCompression; +} MinipsdVipsCompression; /** * \brief Registers minipsd's operations with libvips. Safe to call more than once. @@ -65,20 +65,20 @@ int minipsd_vips_init(void); * composite as given; else vips_composite of the visible layers, blend modes mapped as the spec's * table, masks and groups' visibility and opacity in the layers' alpha, flattened against white * when no layer has alpha; clipping, and a group's own mode and mask, are not drawn), "compression" - * (VipsForeignPsdCompression, the layers'), "psb", "width" and "height" (the canvas; 0: the + * (MinipsdVipsCompression, the layers'), "psb", "width" and "height" (the canvas; 0: the * composite's size, else every layer's rectangle from (left, top)), "left" and "top" (where the * canvas lies in the layers' coordinates). Each image is read once for its layer, and once more * by vips_composite unless the composite is given. * * \return 0, or -1 with libvips' error set. */ -int vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) G_GNUC_NULL_TERMINATED; +int minipsd_vips_psdsave_layers(VipsImage **in, int n, const char *filename, ...) G_GNUC_NULL_TERMINATED; -/** \brief vips_psdsave_layers into a target. */ -int vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) G_GNUC_NULL_TERMINATED; +/** \brief minipsd_vips_psdsave_layers into a target. */ +int minipsd_vips_psdsave_layers_target(VipsImage **in, int n, VipsTarget *target, ...) G_GNUC_NULL_TERMINATED; -/** \brief vips_psdsave_layers into a new buffer, *buf (g_free it) of *len bytes. */ -int vips_psdsave_layers_buffer(VipsImage **in, int n, void **buf, size_t *len, ...) G_GNUC_NULL_TERMINATED; +/** \brief minipsd_vips_psdsave_layers into a new buffer, *buf (g_free it) of *len bytes. */ +int minipsd_vips_psdsave_layers_buffer(VipsImage **in, int n, void **buf, size_t *len, ...) G_GNUC_NULL_TERMINATED; #ifdef __cplusplus } diff --git a/libvips-plugin/release/example.c b/libvips-plugin/release/example.c index 803df47..a39e611 100644 --- a/libvips-plugin/release/example.c +++ b/libvips-plugin/release/example.c @@ -35,7 +35,7 @@ int main(int argc, char **argv) || !(back = vips_image_new_from_file(argv[1], NULL))) vips_error_exit(NULL); loader = vips_foreign_find_load(argv[1]); - ok = loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0 && back->Xsize == SIDE && back->Ysize == SIDE + ok = loader && strcmp(loader, "MinipsdVipsLoadFile") == 0 && back->Xsize == SIDE && back->Ysize == SIDE && back->Bands == BANDS; printf("%s: %dx%d, %d bands, read by %s: %s\n", argv[1], back->Xsize, back->Ysize, back->Bands, loader ? loader : "nothing", ok ? "ok" : "FAILED"); diff --git a/libvips-plugin/test_vips.c b/libvips-plugin/test_vips.c index f021b20..24c592c 100644 --- a/libvips-plugin/test_vips.c +++ b/libvips-plugin/test_vips.c @@ -385,7 +385,7 @@ static void check_document(const minipsd_doc *doc, const minipsd_write_opts *opt if (f) fclose(f); loader = vips_foreign_find_load(path); - check(loader && strcmp(loader, "VipsForeignLoadPsdFile") == 0, "found as psdload", e->name); + check(loader && strcmp(loader, "MinipsdVipsLoadFile") == 0, "found as psdload", e->name); vips_error_clear(); check_image(vips_image_new_from_file(path, NULL), doc, e, "file", &b); check_image(vips_image_new_from_file(path, "access", VIPS_ACCESS_SEQUENTIAL, NULL), doc, e, "file, sequential", &b); @@ -486,9 +486,9 @@ static int from_pixels(VipsImage *image, unsigned colors, int layered, const cha doc.composite = pixels; doc.composite_alpha = (uint8_t)alpha; doc.compression = !option || strcmp(option, "compression") != 0 ? MINIPSD_RLE - : value == VIPS_FOREIGN_PSD_COMPRESSION_ZIP ? MINIPSD_ZIP - : value == VIPS_FOREIGN_PSD_COMPRESSION_NONE ? MINIPSD_RAW - : value == VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED ? MINIPSD_ZIP_PREDICTED + : value == MINIPSD_VIPS_COMPRESSION_ZIP ? MINIPSD_ZIP + : value == MINIPSD_VIPS_COMPRESSION_NONE ? MINIPSD_RAW + : value == MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED ? MINIPSD_ZIP_PREDICTED : MINIPSD_RLE; doc.format = file_size > PSD_VERSION_AT && file[PSD_VERSION_AT] == MINIPSD_PSB ? MINIPSD_PSB : MINIPSD_PSD; opts.deflate = deflate_rows; @@ -539,7 +539,7 @@ static void check_saved(VipsImage *image, const char *name, const char *dir, int return; } saver = vips_foreign_find_save(path); - check(saver && strcmp(saver, "VipsForeignSavePsdFile") == 0, "found as psdsave", name); + check(saver && strcmp(saver, "MinipsdVipsSaveFile") == 0, "found as psdsave", name); /* The same bytes three ways. */ file = read_all(path, &file_size); @@ -827,7 +827,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) /* Platemaker's template, its composite given (the template itself: the layer above is empty). */ snprintf(path, sizeof path, "%s/layers-template.psd", dir); - check(vips_psdsave_layers(pair, 2, path, "composite", template_image, NULL) == 0, "saved", path); + check(minipsd_vips_psdsave_layers(pair, 2, path, "composite", template_image, NULL) == 0, "saved", path); memset(layers, 0, sizeof layers); memset(&doc, 0, sizeof doc); layers[0].name = "Template"; @@ -842,7 +842,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) /* Its composite made by libvips from the layers: the template's own pixels. */ snprintf(again, sizeof again, "%s/layers-template-composed.psd", dir); - check(vips_psdsave_layers(pair, 2, again, NULL) == 0, "saved, composite composed", again); + check(minipsd_vips_psdsave_layers(pair, 2, again, NULL) == 0, "saved, composite composed", again); given = vips_image_new_from_file(path, NULL); made = vips_image_new_from_file(again, NULL); a = pixels_of(given, &given_bytes); @@ -908,7 +908,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) for (k = 0; k < RECORDS; ++k) all &= (loaded[k] = vips_image_new_from_file(path, "layer", k, NULL)) != NULL; composite = vips_image_new_from_file(path, NULL); snprintf(again, sizeof again, "%s/layers-resaved.psd", dir); - check(all && composite && vips_psdsave_layers(loaded, RECORDS, again, "composite", composite, NULL) == 0, + check(all && composite && minipsd_vips_psdsave_layers(loaded, RECORDS, again, "composite", composite, NULL) == 0, "every record loaded and saved", again); check(same_files(path, again), "the same bytes back: names, position, opacity, blend mode, clipping, empty, mask, group", again); @@ -945,7 +945,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) vips_image_set_int(group, "psd-layer-hidden", 1); records[0] = rgb, records[1] = divider, records[2] = red, records[3] = group; snprintf(path, sizeof path, "%s/layers-masked.psd", dir); - check(vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, 0, 0, at_corner, PSD_RGB_BANDS) + check(minipsd_vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, 0, 0, at_corner, PSD_RGB_BANDS) && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS), "saved, composite composed", path); check(near(at_corner[0], rgba[0]) && near(at_corner[1], rgba[1]) && near(at_corner[2], rgba[2]), @@ -955,7 +955,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) vips_image_set_int(group, "psd-layer-hidden", 0); vips_image_set_int(group, "psd-layer-opacity", HALF_OPAQUE); snprintf(path, sizeof path, "%s/layers-grouped.psd", dir); - check(vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, 0, 0, at_corner, PSD_RGB_BANDS) + check(minipsd_vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, 0, 0, at_corner, PSD_RGB_BANDS) && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS), "saved, composite composed", path); check(near(at_corner[0], UCHAR_MAX * half + rgba[0] * (1.0 - half)) && near(at_corner[1], rgba[1] * (1.0 - half)) @@ -965,7 +965,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) vips_image_set_int(rgb, "psd-layer-mask-disabled", 1); snprintf(path, sizeof path, "%s/layers-unmasked.psd", dir); i = ((size_t)INSIDE * WIDTH + INSIDE) * RGBA; - check(vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS) + check(minipsd_vips_psdsave_layers(records, 4, path, NULL) == 0 && composite_pixel(path, INSIDE, INSIDE, in_hole, PSD_RGB_BANDS) && near(in_hole[0], rgba[i]) && near(in_hole[1], rgba[i + 1]) && near(in_hole[2], rgba[i + 2]), "a disabled mask hides nothing", path); } @@ -992,7 +992,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) if (vips_copy(template_image, &moved, "xoffset", AT_X, "yoffset", AT_Y, NULL)) moved = NULL; one[0] = moved; snprintf(path, sizeof path, "%s/layers-canvas.psd", dir); - check(moved && vips_psdsave_layers(one, 1, path, "left", AT_X, "top", AT_Y, NULL) == 0, "saved, canvas placed", path); + check(moved && minipsd_vips_psdsave_layers(one, 1, path, "left", AT_X, "top", AT_Y, NULL) == 0, "saved, canvas placed", path); file = read_all(path, &size); check(file && minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK && info.width == WIDTH && info.height == HEIGHT && minipsd_reader_next_layer(r, &view) == MINIPSD_MORE @@ -1000,7 +1000,7 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) "the canvas at (left, top): the layer at its corner", path); free(file); snprintf(path, sizeof path, "%s/layers-grown.psd", dir); - check(moved && vips_psdsave_layers(one, 1, path, NULL) == 0, "saved, canvas from the layers", path); + check(moved && minipsd_vips_psdsave_layers(one, 1, path, NULL) == 0, "saved, canvas from the layers", path); file = read_all(path, &size); check(file && minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) == MINIPSD_OK && info.width == WIDTH + AT_X && info.height == HEIGHT + AT_Y, @@ -1013,13 +1013,13 @@ static void check_layers_saved(const uint8_t *rgba, const char *dir) { VipsImage *deep = NULL, *mixed[2]; snprintf(path, sizeof path, "%s/layers-refused.psd", dir); - check(vips_psdsave_layers(pair, 0, path, NULL) != 0, "no layers refused", path); + check(minipsd_vips_psdsave_layers(pair, 0, path, NULL) != 0, "no layers refused", path); if (vips_cast(template_image, &deep, VIPS_FORMAT_USHORT, NULL)) deep = NULL; mixed[0] = template_image; mixed[1] = deep; - check(deep && vips_psdsave_layers(mixed, 2, path, NULL) != 0, "a layer of another depth refused", path); + check(deep && minipsd_vips_psdsave_layers(mixed, 2, path, NULL) != 0, "a layer of another depth refused", path); vips_image_set_string(template_image, "psd-layer-blend-mode", "mul"); - check(vips_psdsave_layers(pair, 2, path, NULL) != 0, "a blend mode of three characters refused", path); + check(minipsd_vips_psdsave_layers(pair, 2, path, NULL) != 0, "a blend mode of three characters refused", path); vips_error_clear(); VIPS_UNREF(deep); } @@ -1198,11 +1198,11 @@ int main(int argc, char **argv) check_saved(make_image(rgbf, 3, VIPS_FORMAT_FLOAT, VIPS_INTERPRETATION_scRGB), "save-float.psd", argv[1], 1, NULL, 0); check_saved(make_image(rgb8, 3, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-layered.psd", argv[1], 1, "layered", TRUE); check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-zip.psd", argv[1], 0, "compression", - VIPS_FOREIGN_PSD_COMPRESSION_ZIP); + MINIPSD_VIPS_COMPRESSION_ZIP); check_saved(make_image(rgba, RGBA, VIPS_FORMAT_UCHAR, VIPS_INTERPRETATION_sRGB), "save-raw.psd", argv[1], 0, "compression", - VIPS_FOREIGN_PSD_COMPRESSION_NONE); + MINIPSD_VIPS_COMPRESSION_NONE); check_saved(make_image(grey16, 2, VIPS_FORMAT_USHORT, VIPS_INTERPRETATION_GREY16), "save-predicted.psd", argv[1], 0, - "compression", VIPS_FOREIGN_PSD_COMPRESSION_ZIP_PREDICTED); + "compression", MINIPSD_VIPS_COMPRESSION_ZIP_PREDICTED); snprintf(path, sizeof path, "%s/save-predicted.psd", argv[1]); check(first_layer_compression(path) == STORED_ZIP_PREDICTED, "ZIP with prediction by option", path); { /* 32-bit noise, rows a PSD cannot count: a PSB, unasked */ @@ -1285,7 +1285,7 @@ int main(int argc, char **argv) /* Not a PSD: psdload does not claim it. */ loader = vips_foreign_find_load_buffer(junk, sizeof junk); - check(!loader || strcmp(loader, "VipsForeignLoadPsdBuffer") != 0, "junk not claimed", "junk"); + check(!loader || strcmp(loader, "MinipsdVipsLoadBuffer") != 0, "junk not claimed", "junk"); vips_error_clear(); if (argc == 3) check_list(argv[2]); From e64c33a157d4a549a0502cf0c22c777d8e7b02ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Mon, 26 Oct 2026 00:04:30 +0100 Subject: [PATCH 20/22] build(vips): the released zip carries its SBOM fragment minipsd-vips-X.Y.Z.zip now holds minipsd-vips.spdx.json, an SPDX 2.3 fragment listing every other file of the zip with its checksums, and its CMakeLists sets MINIPSD_SBOM_FILE on minipsd::vips, as minipsd sets it on minipsd::minipsd: one download, one hash, code and SBOM. Both fragments come from cmake/minipsd_spdx.cmake; minipsd's own is unchanged. tools/check_spdx.cmake checks a fragment against its files, and CI and the release run it on the unpacked zip. The Windows build's zlib headers stay zlib 1.3.1's, now with the reason: libvips' libz1 is zlib-ng built zlib-compatible and reports "1.3.1.zlib-ng", and zlib-ng's own headers exist only once it is built. --- .github/workflows/ci.yml | 6 ++- .github/workflows/release.yml | 3 +- CMakeLists.txt | 47 +++------------------- cmake/minipsd.spdx.json.in | 22 +++++------ cmake/minipsd_libvips.cmake | 5 +++ cmake/minipsd_spdx.cmake | 57 +++++++++++++++++++++++++++ docs/INSTALL.md | 7 +++- libvips-plugin/release/CMakeLists.txt | 4 +- libvips-plugin/release/README.md | 1 + tools/check_spdx.cmake | 37 +++++++++++++++++ tools/package_vips.cmake | 16 ++++++++ 11 files changed, 147 insertions(+), 58 deletions(-) create mode 100644 cmake/minipsd_spdx.cmake create mode 100644 tools/check_spdx.cmake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 659147b..d41afcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,8 +83,8 @@ jobs: ctest --preset ${{ matrix.preset }} # The plugin as released (minipsd-vips-X.Y.Z.zip, tools/package_vips.cmake): built from the - # zip alone, outside the repository, its example run; libvips as fetched above. The release - # builds it again on Linux before publishing. + # zip alone, outside the repository, its example run, its SBOM fragment checked against its + # files; libvips as fetched above. The release builds it again on Linux before publishing. - name: Check the released plugin builds and runs if: matrix.vips == 'ON' shell: pwsh @@ -93,6 +93,8 @@ jobs: cmake -DOUTPUT_DIR="$env:RUNNER_TEMP/dist" -P tools/package_vips.cmake $zip = Get-Item "$env:RUNNER_TEMP/dist/minipsd-vips-*.zip" cmake -E chdir "$env:RUNNER_TEMP" cmake -E tar xf $zip.FullName + $unpacked = Join-Path $env:RUNNER_TEMP $zip.BaseName + cmake "-DSPDX=$unpacked/minipsd-vips.spdx.json" "-DBASE=$unpacked" -DCOMPLETE=ON -P tools/check_spdx.cmake $deps = "$env:GITHUB_WORKSPACE/build/${{ matrix.preset }}/_deps" cmake -S (Join-Path $env:RUNNER_TEMP $zip.BaseName) -B "$env:RUNNER_TEMP/plugin" ` "-DFETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS=$deps/minipsd_libvips-src" ` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c94ac89..6ff7865 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,12 +72,13 @@ jobs: cd dist && for zip in minipsd-vips-*.zip; do sha256sum "$zip" > "$zip.sha256" && cat "$zip.sha256"; done # Built from the zip alone, outside the repository, against the system's libvips; its example - # saves a PSD through the plugin and reads it back. + # saves a PSD through the plugin and reads it back. Its SBOM fragment checked against its files. - name: Check the plugin builds and runs as released run: | sudo apt-get update && sudo apt-get install -y --no-install-recommends libvips-dev ninja-build mkdir "$RUNNER_TEMP/plugin" && cd "$RUNNER_TEMP/plugin" cmake -E tar xf "$GITHUB_WORKSPACE"/dist/minipsd-vips-*.zip + cmake -DSPDX="$(echo minipsd-vips-*)/minipsd-vips.spdx.json" -DBASE="$(echo minipsd-vips-*)" -DCOMPLETE=ON -P "$GITHUB_WORKSPACE"/tools/check_spdx.cmake cmake -S minipsd-vips-* -B build -G Ninja cmake --build build ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 447684a..f9c2b46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,8 +52,6 @@ target_include_directories(minipsd INTERFACE # add_subdirectory, FetchContent and find_package (a variable would not survive # FetchContent_MakeAvailable's function scope). Its timestamp honours SOURCE_DATE_EPOCH. # --------------------------------------------------------------------------- -# Every header is listed with its checksums; SPDX requires SHA-1 per file and derives the package -# verification code from those (sorted, concatenated, hashed again). file(GLOB_RECURSE _minipsd_headers RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/include" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h") list(SORT _minipsd_headers) @@ -61,46 +59,11 @@ list(SORT _minipsd_headers) # edited header would refresh neither the checksums here nor tests/'s single-file header. list(TRANSFORM _minipsd_headers PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/include/" OUTPUT_VARIABLE _minipsd_header_paths) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_minipsd_header_paths}") -set(_minipsd_sbom_file [=[ - - { - "SPDXID": "SPDXRef-File-@_minipsd_id@", - "fileName": "./include/@_minipsd_rel@", - "checksums": [ - { "algorithm": "SHA1", "checksumValue": "@_minipsd_sha1@" }, - { "algorithm": "SHA256", "checksumValue": "@_minipsd_sha256@" } - ], - "licenseConcluded": "MIT", - "licenseInfoInFiles": [ "MIT" ], - "copyrightText": "Copyright (c) 2026 Bartlomiej Mucha " - }]=]) -set(_minipsd_sbom_contains [=[ - - { - "spdxElementId": "SPDXRef-Package-minipsd", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-File-@_minipsd_id@" - }]=]) -set(_minipsd_files "") -set(_minipsd_contains "") -set(_minipsd_sha1s "") -foreach(_minipsd_rel IN LISTS _minipsd_headers) - string(REGEX REPLACE "[^A-Za-z0-9.-]" "-" _minipsd_id "${_minipsd_rel}") - file(SHA1 "${CMAKE_CURRENT_SOURCE_DIR}/include/${_minipsd_rel}" _minipsd_sha1) - file(SHA256 "${CMAKE_CURRENT_SOURCE_DIR}/include/${_minipsd_rel}" _minipsd_sha256) - list(APPEND _minipsd_sha1s "${_minipsd_sha1}") - string(CONFIGURE "${_minipsd_sbom_file}" _minipsd_entry @ONLY) - list(APPEND _minipsd_files "${_minipsd_entry}") - string(CONFIGURE "${_minipsd_sbom_contains}" _minipsd_entry @ONLY) - list(APPEND _minipsd_contains "${_minipsd_entry}") -endforeach() -list(JOIN _minipsd_files "," MINIPSD_SBOM_FILES) -list(JOIN _minipsd_contains "," MINIPSD_SBOM_CONTAINS) -list(SORT _minipsd_sha1s) -list(JOIN _minipsd_sha1s "" _minipsd_sha1s) -string(SHA1 MINIPSD_SBOM_VERIFICATION "${_minipsd_sha1s}") -string(TIMESTAMP MINIPSD_SBOM_CREATED "%Y-%m-%dT%H:%M:%SZ" UTC) -configure_file(cmake/minipsd.spdx.json.in "${CMAKE_CURRENT_BINARY_DIR}/minipsd.spdx.json" @ONLY) +include(cmake/minipsd_spdx.cmake) +minipsd_spdx(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/minipsd.spdx.json" PACKAGE minipsd VERSION "${PROJECT_VERSION}" + HOMEPAGE "${PROJECT_HOMEPAGE_URL}" DOWNLOAD "git+${PROJECT_HOMEPAGE_URL}.git@${PROJECT_VERSION}" + SUMMARY "${PROJECT_DESCRIPTION}" COMMENT "Header-only: the files below, under include/, are the whole library." + BASE "${CMAKE_CURRENT_SOURCE_DIR}/include" PREFIX "include/" FILES ${_minipsd_headers}) set_target_properties(minipsd PROPERTIES MINIPSD_SBOM_FILE "${CMAKE_CURRENT_BINARY_DIR}/minipsd.spdx.json") # --------------------------------------------------------------------------- diff --git a/cmake/minipsd.spdx.json.in b/cmake/minipsd.spdx.json.in index 72ed0fa..35d6529 100644 --- a/cmake/minipsd.spdx.json.in +++ b/cmake/minipsd.spdx.json.in @@ -2,8 +2,8 @@ "spdxVersion": "SPDX-2.3", "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", - "name": "minipsd-@PROJECT_VERSION@", - "documentNamespace": "@PROJECT_HOMEPAGE_URL@/spdx/minipsd-@PROJECT_VERSION@-@MINIPSD_SBOM_VERIFICATION@", + "name": "@S_PACKAGE@-@S_VERSION@", + "documentNamespace": "@S_HOMEPAGE@/spdx/@S_PACKAGE@-@S_VERSION@-@MINIPSD_SBOM_VERIFICATION@", "creationInfo": { "created": "@MINIPSD_SBOM_CREATED@", "creators": [ @@ -13,12 +13,12 @@ }, "packages": [ { - "SPDXID": "SPDXRef-Package-minipsd", - "name": "minipsd", - "versionInfo": "@PROJECT_VERSION@", + "SPDXID": "SPDXRef-Package-@S_PACKAGE@", + "name": "@S_PACKAGE@", + "versionInfo": "@S_VERSION@", "supplier": "Person: Bartlomiej Mucha (shadobadev@gmail.com)", - "downloadLocation": "git+@PROJECT_HOMEPAGE_URL@.git@@PROJECT_VERSION@", - "homepage": "@PROJECT_HOMEPAGE_URL@", + "downloadLocation": "@S_DOWNLOAD@", + "homepage": "@S_HOMEPAGE@", "filesAnalyzed": true, "packageVerificationCode": { "packageVerificationCodeValue": "@MINIPSD_SBOM_VERIFICATION@" @@ -27,13 +27,13 @@ "licenseDeclared": "MIT", "licenseInfoFromFiles": [ "MIT" ], "copyrightText": "Copyright (c) 2026 Bartlomiej Mucha ", - "summary": "@PROJECT_DESCRIPTION@", - "comment": "Header-only: the files below, under include/, are the whole library.", + "summary": "@S_SUMMARY@", + "comment": "@S_COMMENT@", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", - "referenceLocator": "pkg:github/ShadobaDev/minipsd@@PROJECT_VERSION@" + "referenceLocator": "pkg:github/ShadobaDev/minipsd@@S_VERSION@" } ] } @@ -44,7 +44,7 @@ { "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", - "relatedSpdxElement": "SPDXRef-Package-minipsd" + "relatedSpdxElement": "SPDXRef-Package-@S_PACKAGE@" },@MINIPSD_SBOM_CONTAINS@ ] } diff --git a/cmake/minipsd_libvips.cmake b/cmake/minipsd_libvips.cmake index 57aff1d..edb3129 100644 --- a/cmake/minipsd_libvips.cmake +++ b/cmake/minipsd_libvips.cmake @@ -43,6 +43,11 @@ if(WIN32) endif() target_include_directories(minipsd_libvips SYSTEM INTERFACE "${_vips}/include" "${_vips}/include/glib-2.0" "${_vips}/lib/glib-2.0/include") + # zlib's headers for the zlib-ng the zip links: built zlib-compatible, it is zlib 1.3.1's ABI + # (libz1.dll of 8.18.7, zlib-ng 2.3.3, reports zlibVersion() "1.3.1.zlib-ng"; deflateInit_ + # checks the version's first digit and sizeof(z_stream)). zlib-ng's own headers are made when + # it is built (zlib.h.in, zconf.h.in), so no release has them to fetch; madler's 1.3.1 are + # the same API and layout. Bump them with libvips, to the zlib version its libz1 reports. FetchContent_Declare(minipsd_zlib_headers URL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz URL_HASH SHA256=9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23 diff --git a/cmake/minipsd_spdx.cmake b/cmake/minipsd_spdx.cmake new file mode 100644 index 0000000..8aa7ad7 --- /dev/null +++ b/cmake/minipsd_spdx.cmake @@ -0,0 +1,57 @@ +# --------------------------------------------------------------------------- +# An SPDX 2.3 fragment describing one package and its files, for a consumer to merge into its own +# SBOM: minipsd's (CMakeLists.txt) and the released plugin zip's (tools/package_vips.cmake). +# Every file is listed with its checksums; SPDX requires SHA-1 per file and derives the package +# verification code from those (sorted, concatenated, hashed again). The timestamp honours +# SOURCE_DATE_EPOCH. Works in a project and in script mode (cmake -P). +# +# minipsd_spdx(OUTPUT PACKAGE VERSION HOMEPAGE DOWNLOAD +# SUMMARY COMMENT BASE [PREFIX ] FILES ) +# +# Each file is read from BASE/ and named ./PREFIX in the document. +# --------------------------------------------------------------------------- +set(_minipsd_spdx_template "${CMAKE_CURRENT_LIST_DIR}/minipsd.spdx.json.in") + +function(minipsd_spdx) + cmake_parse_arguments(PARSE_ARGV 0 S "" "OUTPUT;PACKAGE;VERSION;HOMEPAGE;DOWNLOAD;SUMMARY;COMMENT;BASE;PREFIX" "FILES") + set(_file [=[ + + { + "SPDXID": "SPDXRef-File-@_id@", + "fileName": "./@S_PREFIX@@_rel@", + "checksums": [ + { "algorithm": "SHA1", "checksumValue": "@_sha1@" }, + { "algorithm": "SHA256", "checksumValue": "@_sha256@" } + ], + "licenseConcluded": "MIT", + "licenseInfoInFiles": [ "MIT" ], + "copyrightText": "Copyright (c) 2026 Bartlomiej Mucha " + }]=]) + set(_contains [=[ + + { + "spdxElementId": "SPDXRef-Package-@S_PACKAGE@", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-File-@_id@" + }]=]) + set(_files "") + set(_relations "") + set(_sha1s "") + foreach(_rel IN LISTS S_FILES) + string(REGEX REPLACE "[^A-Za-z0-9.-]" "-" _id "${_rel}") + file(SHA1 "${S_BASE}/${_rel}" _sha1) + file(SHA256 "${S_BASE}/${_rel}" _sha256) + list(APPEND _sha1s "${_sha1}") + string(CONFIGURE "${_file}" _entry @ONLY) + list(APPEND _files "${_entry}") + string(CONFIGURE "${_contains}" _entry @ONLY) + list(APPEND _relations "${_entry}") + endforeach() + list(JOIN _files "," MINIPSD_SBOM_FILES) + list(JOIN _relations "," MINIPSD_SBOM_CONTAINS) + list(SORT _sha1s) + list(JOIN _sha1s "" _sha1s) + string(SHA1 MINIPSD_SBOM_VERIFICATION "${_sha1s}") + string(TIMESTAMP MINIPSD_SBOM_CREATED "%Y-%m-%dT%H:%M:%SZ" UTC) + configure_file("${_minipsd_spdx_template}" "${S_OUTPUT}" @ONLY) +endfunction() diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 6340822..1ba9889 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -111,6 +111,9 @@ target_link_libraries(my-app PRIVATE minipsd::vips) `add_subdirectory` of the unpacked zip does the same. On its own, the zip builds and tests its example (`cmake -S`, `cmake --build`, `ctest`); without CMake, its README says what to compile. +The zip carries its own SBOM fragment, `minipsd-vips.spdx.json` (SPDX 2.3, every other file in the +zip with its checksums, minipsd's single header among them), found as minipsd's is: +`get_target_property(sbom minipsd::vips MINIPSD_SBOM_FILE)`. **From minipsd's source.** In any of the ways at the top, turn on `MINIPSD_VIPS_PLUGIN` before adding minipsd: @@ -127,7 +130,9 @@ libvips itself ([cmake/minipsd_libvips.cmake](../cmake/minipsd_libvips.cmake)): - **Linux, macOS:** pkg-config, the system's libvips and zlib. Nothing is fetched. - **Windows:** libvips' official prebuilt build, `vips-dev-x64-web`, pinned, and zlib's headers (zlib 1.3.1, pinned, about 1.5 MB), which that build lacks; the plugin links the zlib the - build carries, `libz1`, for ZIP channels. + build carries, `libz1`, for ZIP channels. That one is zlib-ng built zlib-compatible, and + reports itself as zlib 1.3.1 (`1.3.1.zlib-ng`), so zlib 1.3.1's headers describe it; zlib-ng's + own are made when it is built, and no release carries them. A program that fetches libvips' Windows build itself points the plugin at it, before adding the plugin. Then one libvips is fetched, and the plugin is compiled against the headers of the DLLs diff --git a/libvips-plugin/release/CMakeLists.txt b/libvips-plugin/release/CMakeLists.txt index 1a7f49b..2c471ba 100644 --- a/libvips-plugin/release/CMakeLists.txt +++ b/libvips-plugin/release/CMakeLists.txt @@ -10,7 +10,7 @@ # minipsd is compiled into the plugin from foreign/minipsd/minipsd.h, private to it: a program's # own minipsd, of any version, is left alone. foreign/ is laid out as libvips/foreign/. # -# minipsd::vips the plugin, a static library +# minipsd::vips the plugin, a static library; its MINIPSD_SBOM_FILE, minipsd-vips.spdx.json # minipsd-vips-example example.c: an image saved and read back through the plugin (its test) # --------------------------------------------------------------------------- cmake_minimum_required(VERSION 3.21) # PROJECT_IS_TOP_LEVEL @@ -26,6 +26,8 @@ add_library(minipsd::vips ALIAS minipsd_vips) target_include_directories(minipsd_vips PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/foreign/minipsd") target_link_libraries(minipsd_vips PUBLIC minipsd_libvips PRIVATE minipsd_libvips_zlib) set_target_properties(minipsd_vips PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) +# The SBOM fragment of this zip, found as minipsd's own: get_target_property(... MINIPSD_SBOM_FILE). +set_target_properties(minipsd_vips PROPERTIES MINIPSD_SBOM_FILE "${CMAKE_CURRENT_SOURCE_DIR}/minipsd-vips.spdx.json") if(MINIPSD_VIPS_EXAMPLE) add_executable(minipsd-vips-example example.c) diff --git a/libvips-plugin/release/README.md b/libvips-plugin/release/README.md index ff0a2ab..6603b97 100644 --- a/libvips-plugin/release/README.md +++ b/libvips-plugin/release/README.md @@ -15,6 +15,7 @@ manual's "PSD in libvips: the plugin". | `cmake/minipsd_libvips.cmake` | how libvips is found: Windows fetches libvips' official prebuilt build (pinned), elsewhere pkg-config | | `CMakeLists.txt` | builds `minipsd::vips`, a static library | | `example.c` | an image saved and read back through the plugin: its test | +| `minipsd-vips.spdx.json` | the SBOM fragment (SPDX 2.3): every other file here, with its checksums; `get_target_property(sbom minipsd::vips MINIPSD_SBOM_FILE)` | ## Build diff --git a/tools/check_spdx.cmake b/tools/check_spdx.cmake new file mode 100644 index 0000000..6664e87 --- /dev/null +++ b/tools/check_spdx.cmake @@ -0,0 +1,37 @@ +# Checks an SPDX fragment against the files it describes: each listed file's SHA-256 as on disk, +# and with COMPLETE every file under BASE listed (but the fragment itself). +# +# cmake -DSPDX= -DBASE= [-DCOMPLETE=ON] -P tools/check_spdx.cmake +cmake_minimum_required(VERSION 3.21) + +if(NOT SPDX OR NOT BASE) + message(FATAL_ERROR "usage: cmake -DSPDX= -DBASE= [-DCOMPLETE=ON] -P tools/check_spdx.cmake") +endif() +file(READ "${SPDX}" _doc) +string(JSON _count LENGTH "${_doc}" files) +math(EXPR _last "${_count} - 1") +set(_listed "") +foreach(_i RANGE ${_last}) + string(JSON _name GET "${_doc}" files ${_i} fileName) + string(REGEX REPLACE "^\\./" "" _name "${_name}") + string(JSON _sha256 GET "${_doc}" files ${_i} checksums 1 checksumValue) + if(NOT EXISTS "${BASE}/${_name}") + message(FATAL_ERROR "${SPDX}: ${_name} is listed but missing") + endif() + file(SHA256 "${BASE}/${_name}" _actual) + if(NOT _actual STREQUAL _sha256) + message(FATAL_ERROR "${SPDX}: ${_name} differs from its checksum") + endif() + list(APPEND _listed "${_name}") +endforeach() +if(COMPLETE) + get_filename_component(_self "${SPDX}" ABSOLUTE) + file(GLOB_RECURSE _files RELATIVE "${BASE}" "${BASE}/*") + foreach(_file IN LISTS _files) + get_filename_component(_abs "${BASE}/${_file}" ABSOLUTE) + if(NOT _abs STREQUAL _self AND NOT _file IN_LIST _listed) + message(FATAL_ERROR "${SPDX}: ${_file} is not listed") + endif() + endforeach() +endif() +message(STATUS "${SPDX}: ${_count} files, checksums as on disk") diff --git a/tools/package_vips.cmake b/tools/package_vips.cmake index 99a6804..e00a5af 100644 --- a/tools/package_vips.cmake +++ b/tools/package_vips.cmake @@ -8,6 +8,7 @@ # libvips carries libnsgif and qoi # cmake/minipsd_libvips.cmake as the repository finds libvips # LICENSE.MIT +# minipsd-vips.spdx.json the SBOM fragment: every other file, with its checksums # # cmake -DOUTPUT_DIR= -P tools/package_vips.cmake cmake_minimum_required(VERSION 3.21) @@ -28,6 +29,11 @@ foreach(_part MAJOR MINOR PATCH) list(APPEND _version "${CMAKE_MATCH_1}") endforeach() list(JOIN _version "." _version) +file(STRINGS "${_root}/CMakeLists.txt" _homepage REGEX "HOMEPAGE_URL +\"[^\"]+\"") +if(NOT _homepage MATCHES "\"([^\"]+)\"") + message(FATAL_ERROR "CMakeLists.txt names no HOMEPAGE_URL") +endif() +set(_homepage "${CMAKE_MATCH_1}") set(_name "minipsd-vips-${_version}") set(_stage "${_out}/${_name}") @@ -46,6 +52,16 @@ if(_rc) message(FATAL_ERROR "tools/amalgamate.cmake failed") endif() +# The SBOM fragment: the zip's every file, as minipsd's own fragment lists its headers. +include("${_root}/cmake/minipsd_spdx.cmake") +file(GLOB_RECURSE _files RELATIVE "${_stage}" "${_stage}/*") +list(SORT _files) +minipsd_spdx(OUTPUT "${_stage}/minipsd-vips.spdx.json" PACKAGE minipsd-vips VERSION "${_version}" HOMEPAGE "${_homepage}" + DOWNLOAD "${_homepage}/releases/download/${_version}/${_name}.zip" + SUMMARY "minipsd's libvips plugin: psdload, psdsave and psdsave_layers, as a CMake project" + COMMENT "The files below are the whole zip but this document; foreign/minipsd/minipsd.h is minipsd ${_version}, one file, compiled into the plugin." + BASE "${_stage}" FILES ${_files}) + execute_process(COMMAND "${CMAKE_COMMAND}" -E tar cf "${_name}.zip" --format=zip "${_name}" WORKING_DIRECTORY "${_out}" RESULT_VARIABLE _rc) if(_rc) From c08f35ecdc2eb536da42b140e27ac0fe70c82ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Mon, 26 Oct 2026 00:41:34 +0100 Subject: [PATCH 21/22] docs(vips): what the plugin does, psdsave_layers, 0.4.0 The manual, the spec, INSTALL, the README and the zip's README now say plainly what the plugin does: it reads a file's composite or any layer, with its look, mask and groups, and writes one image, or several as the layers of one document. psdsave_layers is documented: its fields, the composite it composes and what it leaves out, and what a record loses on the way (text, effects, smart objects, adjustments' settings). INSTALL's ABI note now rests on a comparison: VipsForeignLoad and VipsForeignSave are the same in libvips 8.15.1 and 8.18.7, and a field added to either breaks a compiled plugin. CHANGELOG 0.4.0 lists the additions; ROADMAP marks S7.6 done. --- README.md | 3 +- docs/CHANGELOG.md | 15 ++++++-- docs/CHEATSHEET.md | 2 +- docs/CODE-ARCH.md | 12 ++++-- docs/GITHUB-RELEASE.md | 3 +- docs/INSTALL.md | 10 +++-- docs/MANUAL.md | 64 +++++++++++++++++++++++++++----- docs/ROADMAP.md | 16 ++++++-- docs/SPECIFICATION.md | 51 ++++++++++++++++++++++--- libvips-plugin/release/README.md | 20 +++++++++- 10 files changed, 163 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2f3674f..5cb0335 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ files are bounded by caller limits. How it does on 315 files from other programs: [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md). In a program built on libvips, [libvips-plugin/](libvips-plugin/) makes minipsd its PSD and PSB -loader and saver; see the manual's [With libvips](docs/MANUAL.md#with-libvips). +loader and saver: a file's composite or any layer in, one image or several as layers out; see +the manual's [PSD in libvips](docs/MANUAL.md#psd-in-libvips-the-plugin). ## Adding it to a project diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0849152..f83b3f0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,7 +3,7 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 rules in [TODO.md](TODO.md): breaking changes bump the MINOR, additions and fixes the PATCH. -## [0.4.0] — 2026-10-25 +## [0.4.0] — 2026-10-26 **Breaking:** `minipsd_doc.composite_alpha` now says, for every composite (given, composed from the layers, or stored), whether it has transparency; before, a given or composed composite always @@ -40,13 +40,22 @@ composite's by `composite_alpha`. `minipsd_vips_init()` after `VIPS_INIT`. `psdload` gives the composite or, with `layer`, one layer placed by its offset, decoded in bands of rows from libvips' threads; grey, RGB, CMYK, bitmap, indexed (through its palette), Lab and multichannel at every depth, with the - resolution, ICC profile, EXIF, XMP, IPTC and `psd-*` fields. `psdsave` writes an image with + resolution, ICC profile, EXIF, XMP, IPTC and `psd-*` fields: a layer's name, opacity, + visibility, blend mode and clipping, its mask (an image in `psd-layer-mask`), a group's + records (`psd-layer-section`), and a record without pixels as one transparent pixel with + `psd-layer-empty`, so every record loads. `psdsave` writes an image with alpha as one layer and its composite, one without as a flat file (with `layered`, a layer without transparency too), reading the image once, in bands, each channel encoded as its rows come: memory about the file's size. Options: `compression` (`rle`, `zip`, `zip-predicted`, `none`), `psb`, `layer_name`, `layered`; a PSB by itself when a PSD cannot hold the file. +- `psdsave_layers` (`_buffer`, `_target`; `minipsd_vips_psdsave_layers` in C): several images as + the layers of one document, bottom first, each record's look from the same `psd-layer-*` + fields, masks and groups included, so a file's records loaded with `psdload` and saved come + back. The composite is given, or composed by `vips_composite` (blend modes, opacity, masks, + groups' visibility and opacity); the canvas by `width`, `height`, `left`, `top`. - The plugin released as `minipsd-vips-X.Y.Z.zip`: a CMake project that builds as it is, with - minipsd's single header private to it and an example that is its test. + minipsd's single header private to it, an example that is its test, and its SBOM fragment + (`minipsd-vips.spdx.json`, `MINIPSD_SBOM_FILE` on `minipsd::vips`). ### Fixed - A crafted file could make `minipsd_read` and the decode calls write past their buffers: a diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 4c99e98..22c7c74 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -136,7 +136,7 @@ sudo tar -xzf dist/minipsd-0.1.0.tar.gz --strip-components=1 -C /usr/local | `minipsd-cxx17` (build target) | the header compiles warning-free as C++17 | | `minipsd-include-check` (build target) | `minipsd.h` compiles alone, then with its implementation, leaving no internal function unused | | `psd-tools-roundtrip` | documents written through minipsd, read back by `psd-tools` | -| `vips.plugin` | with `MINIPSD_VIPS_PLUGIN`: documents and every file at hand loaded through libvips (composite and layers) against minipsd's decode, saved and read back, a tall image read in bands | +| `vips.plugin` | with `MINIPSD_VIPS_PLUGIN`: documents and every file at hand loaded through libvips (composite and layers) against minipsd's decode, saved and read back, a tall image read in bands, layered documents saved with `psdsave_layers` (masks, groups, the composite composed) | | `install` | `cmake --install` into the build tree (`tests/prefix/`) | | `consumer-copy` | a project with `include/` copied in | | `consumer-subdirectory` | `add_subdirectory` (git submodule) | diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index 3e322e1..7366fac 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -115,22 +115,26 @@ examples/ the I/O implementations; built and run in CI, nev write_nonblocking.c O_NONBLOCK pipe + poll (POSIX) read_file.c whole file into memory, described, every layer and the composite decoded resave.c minipsd_read with stored, one layer changed, written back: the round trip -libvips-plugin/ minipsd as libvips' loader and saver (psdload, psdsave), registered by the program - minipsd_vips.h the public header: minipsd_vips_init, MinipsdVipsCompression +libvips-plugin/ minipsd as libvips' loader and saver (psdload, psdsave, psdsave_layers), registered by the program + minipsd_vips.h the public header: minipsd_vips_init, MinipsdVipsCompression, minipsd_vips_psdsave_layers minipsd_vips.c minipsd_vips_init: registers both sides with libvips foreign/ the formats' code, as libvips lays out its own; names the plugin's (MinipsdVips*) psdload.c psdload, _buffer, _source; minipsd's reader compiled in, private - psdsave.c psdsave, _buffer, _target; minipsd's writer compiled in, private + psdsave.c psdsave and psdsave_layers, each _buffer, _target; minipsd's writer compiled in, private psd.c, psd.h internal: constants, helpers and the types both sides share test_vips.c documents and files loaded and saved through libvips, against minipsd's decode cmake/ minipsd_libvips.cmake libvips and its zlib for the example and the plugin: fetched on Windows, else pkg-config + minipsd_spdx.cmake the SPDX fragment generator: minipsd's, and the plugin zip's tests/ test_.cpp one file per module; detail modules are tested directly fuzz/ the reader's libFuzzer target, run locally (linux-fuzz preset); replay.c runs it on regressions/, the inputs that once failed tools/ amalgamate.cmake joins include/ into one file, a release artifact + package_vips.cmake packs the plugin as released: minipsd-vips-X.Y.Z.zip, its SBOM fragment inside + check_spdx.cmake checks an SPDX fragment against its files (checksums, every file listed) + compat_report.py regenerates COMPATIBILITY.md ``` | Module (section of `minipsd.h`) | Struct | Functions (sketch) | Implementation | @@ -749,7 +753,7 @@ commit message. [ROADMAP.md](ROADMAP.md) turns these into numbered steps. | **4. Reader** | `reader.h` for the own subset, then real-application files; fuzzing | 0.2.1 | | **5. Round trip** | layer rectangles in the writer, a supplied composite, full import, kept unknown content, pass-through | 0.2.x | | **6. Features** | one row of the table above per stage, writer and reader together | 0.2.x | -| **7. Interoperability** | libvips: an example, a MANUAL chapter, and the plugin (`psdload`, `psdsave`) | 0.3.x | +| **7. Interoperability** | libvips: an example, a MANUAL chapter, and the plugin (`psdload`, `psdsave`, `psdsave_layers`) | 0.3.x, 0.4.0 | Every stage follows these rules: diff --git a/docs/GITHUB-RELEASE.md b/docs/GITHUB-RELEASE.md index 18192f9..ebada9b 100644 --- a/docs/GITHUB-RELEASE.md +++ b/docs/GITHUB-RELEASE.md @@ -13,7 +13,8 @@ Pushing the tag starts the [`Release`](../.github/workflows/release.yml) workflo `docs/CHANGELOG.md` has a section for that version; 3. `minipsd.h` as a single file: `include/` joined by `tools/amalgamate.cmake`; 4. the libvips plugin as `minipsd-vips-X.Y.Z.zip`, a CMake project that builds as it is - (`tools/package_vips.cmake`), then built from the zip alone against Ubuntu's libvips and its + (`tools/package_vips.cmake`), with its SBOM fragment, then checked against that fragment + (`tools/check_spdx.cmake`) and built from the zip alone against Ubuntu's libvips, its example run: a zip that does not build is not published; 5. a build-provenance attestation for `minipsd.h` and the zip; 6. a GitHub Release with `minipsd.h`, the zip and a `.sha256` for each, its notes taken from the diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 1ba9889..f37de5d 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -90,11 +90,13 @@ get_target_property(minipsd_sbom minipsd::minipsd MINIPSD_SBOM_FILE) ## The libvips plugin -The plugin makes minipsd libvips' loader and saver of PSD and PSB (the manual's +The plugin makes minipsd libvips' loader and saver of PSD and PSB: it reads a file's composite or +any layer, and writes one image or several as the layers of one document (the manual's [PSD in libvips](MANUAL.md#psd-in-libvips-the-plugin)). It is built from source, as part of your -program, against your libvips' headers: its classes extend libvips' own, so a compiled one would -hold for one libvips version only. Link `minipsd::vips`, then call `minipsd_vips_init()` after -`VIPS_INIT`. +program, against your libvips' headers: its classes extend libvips' `VipsForeignLoad` and +`VipsForeignSave`, so a compiled copy holds only while those keep the layout it was built +against (they are the same in libvips 8.15.1 and 8.18.7; a field added to either breaks it). +Link `minipsd::vips`, then call `minipsd_vips_init()` after `VIPS_INIT`. **From the release.** Each GitHub Release has `minipsd-vips-X.Y.Z.zip` (and its `.sha256`): the plugin as a CMake project of its own, with minipsd's single header inside it, private to the diff --git a/docs/MANUAL.md b/docs/MANUAL.md index fb23629..8e0559d 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -820,7 +820,9 @@ made it (the program, and the feature you used): that is what sets the order of minipsd writes and reads PSD; it renders nothing. When your layers are libvips images, let libvips flatten them and hand minipsd the result as the document's composite, the image a -viewer that ignores layers shows. The two share their memory layout (8-bit RGBA, straight +viewer that ignores layers shows. The plugin's `psdsave_layers` does all of it for you +([PSD in libvips: the plugin](#psd-in-libvips-the-plugin)); below is the same with minipsd's own +API. The two share their memory layout (8-bit RGBA, straight alpha, rows top down), so nothing is converted. [examples/vips_composite.c](../examples/vips_composite.c) is the whole program; its steps: @@ -854,9 +856,17 @@ build, elsewhere pkg-config finds yours. minipsd itself never depends on libvips ### PSD in libvips: the plugin -[libvips-plugin/](../libvips-plugin/) makes minipsd libvips' loader and saver of PSD and PSB, so -`vips_image_new_from_file` and `vips_image_write_to_file` take `.psd` and `.psb` in your program. -libvips' prebuilt Windows builds load no modules, so your program registers them, once, after +[libvips-plugin/](../libvips-plugin/) makes minipsd libvips' loader and saver of PSD and PSB. In +short, it: + +- **reads** a file's composite, or any one of its layers, with the layer's name, opacity, + visibility, blend mode, clipping, its mask, and the records of its groups; +- **writes** one image (`psdsave`: one layer and its composite, or a flat file), or several + images as the layers of one document (`psdsave_layers`), with masks and groups, the composite + given or composed by libvips. + +Pixels stay libvips images throughout; minipsd encodes and decodes them. libvips' prebuilt +Windows builds load no modules, so your program registers the operations, once, after `VIPS_INIT`: ```c @@ -874,13 +884,15 @@ vips_image_write_to_file(page, "copy.psd", "layer_name", "Page", NULL); names them (CMYK with 0 for no ink, the other way round from PSD), bitmap as `B_W` (black 0), indexed through its palette as sRGB, Lab and multichannel as `MULTIBAND`, each at the file's depth. With it: the resolution, the ICC profile, EXIF, XMP, IPTC, `bits-per-sample`, and - `psd-*` fields such as `psd-layer-count` and a layer's name, blend mode and opacity (the list: - the spec's [Working with libvips](SPECIFICATION.md#working-with-libvips)). Bands of rows are + `psd-*` fields: `psd-layer-count`, and a layer's look in `psd-layer-*` (the list: the spec's + [The plugin](SPECIFICATION.md#the-plugin)). A layer without pixels (an empty one, a group's + record, an adjustment) comes as one transparent pixel with `psd-layer-empty`; a group's record + also has `psd-layer-section`; a layer's mask is an image in `psd-layer-mask`. Bands of rows are decoded from libvips' threads at once; with `access=sequential` memory stays at a few bands of rows per thread, so it grows with the width and the threads, not the height. A layer without transparency comes without an alpha band. -- **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): an image with alpha as one layer - and its composite. One without alpha as a flat file, the composite only and without +- **Saving one image** (`psdsave`, `psdsave_buffer`, `psdsave_target`): an image with alpha as one + layer and its composite. One without alpha as a flat file, the composite only and without transparency, as Photoshop saves an image without layers; with `layered`, one layer without transparency as well. Grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats. Options: `compression` (`rle`, the default; `zip`; `zip-predicted`; `none`, the composite raw too), @@ -889,6 +901,40 @@ vips_image_write_to_file(page, "copy.psd", "layer_name", "Page", NULL); asked, for a file named `.psb`, or when a PSD cannot hold the image: a side above 30 000, a row longer than a PSD's two-byte count, or a file above 2 GB, which Adobe documents as Photoshop's limit for a PSD. +- **Saving layers** (`psdsave_layers`, `_buffer`, `_target`; in C `minipsd_vips_psdsave_layers` + and its two variants): a saver takes one image, so this is an operation over an array of + them, as `vips_composite` is. Each image is a layer, bottom first; its look comes from the same + `psd-layer-*` fields psdload sets, its position from `xoffset` and `yoffset`: + + ```c + VipsImage *layers[2] = { page, blank }; /* blank: any image with psd-layer-empty set */ + vips_image_set_string(page, "psd-layer-name", "Template"); + vips_image_set_int(blank, "psd-layer-empty", 1); + vips_image_set_string(blank, "psd-layer-name", "Layer"); + minipsd_vips_psdsave_layers(layers, 2, "template.psd", "composite", page, NULL); + ``` + + - `psd-layer-empty` makes a layer without pixels, as Photoshop writes a new, unpainted one. + - A mask is a one-band image of the layers' depth in `psd-layer-mask`, with its own + `xoffset` and `yoffset`, `psd-layer-mask-default-color` (its value outside, 0 or 255) and + `psd-layer-mask-disabled`. + - A group is records in the list, as the file has them: below its layers an image with + `psd-layer-section` `"end"`, above them one with `"open"` or `"closed"` that carries the + group's name, visibility, opacity and blend mode (`"pass"` passes through). + - The composite: `composite`, as given; else `vips_composite` of the visible layers, blend + modes by the spec's table, opacity, masks and the groups' visibility and opacity in the + alpha, flattened against white when no layer has alpha. Clipping, a group's own blend mode + and mask, and modes libvips lacks are not drawn; libvips computes in floats, so where alpha + is partial the composite is off by a level or two. A 16-bit CMYK document needs it given. + - The canvas: `width` and `height`; else the composite's size; else every layer from (`left`, + `top`), which also places the canvas in the layers' coordinates. + - Every layer with pixels is of one mode and depth; none is converted. `compression` and + `psb` as `psdsave`; each image is read once, in bands. + + A file's records loaded one by one with psdload and saved come back as the layers it had: the + look above, the pixels, masks and groups. Nothing else of a record travels: text, effects, + smart objects, an adjustment's settings (it comes back an empty layer) and other blocks are + left behind. A file minipsd wrote from those fields comes back byte for byte. - **Building:** `-DMINIPSD_VIPS_PLUGIN=ON` gives the static library `minipsd::vips`; link it. minipsd is compiled into it privately (`MINIPSD_STATIC`), so your own use of minipsd keeps its own. libvips comes as for the example, or from your project: define `minipsd_libvips` and @@ -896,7 +942,7 @@ vips_image_write_to_file(page, "copy.psd", "layer_name", "Page", NULL); adding minipsd, or point `FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS` at the libvips you fetched ([INSTALL.md](INSTALL.md#the-libvips-plugin)). - **Released** as `minipsd-vips-X.Y.Z.zip` with each GitHub Release: the plugin as a CMake project - of its own, minipsd inside it ([INSTALL.md](INSTALL.md#the-libvips-plugin)). + of its own, minipsd inside it, with its SBOM fragment ([INSTALL.md](INSTALL.md#the-libvips-plugin)). ## Memory and ownership diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b7b17e7..e168285 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -599,15 +599,25 @@ follows. PSD cannot hold the file; the plugin released as a zip. psdsave is now as fast as ImageMagick's `magicksave` or faster, its files 4 to 6 times smaller, with the right pixels where magicksave's are wrong (alpha) or it fails (20000 x 20000). -- [ ] **S7.6** A layered document saved through libvips: several images, each a layer with its +- [x] **S7.6** A layered document saved through libvips: several images, each a layer with its name, position, opacity, visibility and blend mode, and the composite, in one call, so that a program uses minipsd's writer through libvips as it reads through it (`psdload` gives every layer, with `layer`, and its `psd-layer-*` fields). A saver takes one image, - so this is an operation over an array of images, as `vips_composite` is. The plan, with - its questions: `temp/S7.6-PLAN.md` while it is worked on. + so this is an operation over an array of images, as `vips_composite` is. *Done when:* Platemaker's template (a "Template" layer and an empty "Layer" above it) is written through it, the bytes those of `minipsd_write` from the same layers; a file's layers loaded with `psdload` and saved with it come back with their pixels and fields. + As done: `psdsave_layers` (`_buffer`, `_target`; `minipsd_vips_psdsave_layers` in C), each + record's look from the `psd-layer-*` fields psdload sets, masks (`psd-layer-mask`) and + groups (records with `psd-layer-section`) too, Blend If left out; the composite given or + composed by `vips_composite` (masks, groups' visibility and opacity in the alpha); the + canvas by `width`, `height`, `left`, `top`. psdload now loads every record (one without + pixels as one transparent pixel with `psd-layer-empty`): psd-tools' set, 2280 of 2280 + layers. Platemaker's template is byte for byte `minipsd_write`'s, and a document with a + placed, half-opaque, multiplied, clipped layer, a disabled mask and a group comes back the + same bytes; Platemaker writes its template through it. The plugin's names became its own + (`MinipsdVips*`, `minipsd_vips_*`): libvips' CONTRIBUTING.md accepts no contribution made + with generative AI, so it never moves into libvips. The zip carries its SBOM fragment. ## 8. Test quality diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 42eadf7..609c81b 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -731,7 +731,7 @@ libvips' loader and saver, laid out as libvips' own (a base class over a `VipsSo `minipsd_vips_*`) that never meet libvips'. It stays a project apart from libvips: libvips' CONTRIBUTING.md accepts no contribution made with generative AI, and minipsd is made with it. -- **Registration.** `minipsd_vips_init()`, after `VIPS_INIT`, registers the six operations' +- **Registration.** `minipsd_vips_init()`, after `VIPS_INIT`, registers the nine operations' types; libvips then finds them among its loaders and savers by `is_a` and suffix. libvips' prebuilt Windows builds (8.18.7: `enable modules: false`) load no modules, so a loadable module would not work there; the program registers instead. The loader's priority is libvips' @@ -739,8 +739,9 @@ CONTRIBUTING.md accepts no contribution made with generative AI, and minipsd is - **Loading** (`psdload`, `psdload_buffer`, `psdload_source`). The file is mapped (`vips_source_map`) and read in place. The image is the composite, or with `layer` (bottom first; -1, the default, the composite) that layer: its rectangle, placed by `xoffset` and - `yoffset`, with transparency when the layer has it (no alpha band when it has none). An empty layer (a group's - marker, an adjustment) is refused. Bands are the mode's colour channels and transparency, up to + `yoffset`, with transparency when the layer has it (no alpha band when it has none). A record + without pixels (an empty layer, a group's record, an adjustment) is one transparent pixel + with `psd-layer-empty`, so every record loads and `layer=N` counts them all. Bands are the mode's colour channels and transparency, up to `MINIPSD_PLANES` (a multichannel file's sixth channel on is not decoded): | Mode | libvips | Samples | @@ -769,7 +770,12 @@ CONTRIBUTING.md accepts no contribution made with generative AI, and minipsd is | `psd-pixel-aspect-ratio` | 1064, 1.0 without it | | `psd-has-real-merged-data` | 1057; 1 without it | | `psd-layer-count` | the file's layers | - | `psd-layer-name`, `-blend-mode`, `-opacity`, `-hidden` | the layer, with `layer` | + | `psd-layer-name`, `-opacity`, `-hidden`, `-clipping` | the record, with `layer` | + | `psd-layer-blend-mode` | the record's key; a group's top record, its group's mode from its `lsct` block (`pass`: passes through) | + | `psd-layer-empty` | 1 for a record without pixels (one transparent pixel stands for it), else 0 | + | `psd-layer-section` | a group's records only: `open`, `closed` (the top record), `end` (the divider below its layers) | + | `psd-layer-mask` | the record's mask, an image: one band of the file's depth (`B_W`, `GREY16`), its rectangle in `xoffset`, `yoffset`; one pixel with `psd-layer-empty` for a mask without area; decoded whole, a mask being one band | + | `psd-layer-mask-default-color`, `-mask-disabled` | the mask's value outside its rectangle (0 or 255); its flag 0x02 | - **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`). Grey, RGB or CMYK (by `interpretation` and bands, each with or without alpha), 8 or 16 bits, grey and RGB as floats; @@ -797,13 +803,48 @@ CONTRIBUTING.md accepts no contribution made with generative AI, and minipsd is through `psdsave_target`), or a side past 30 000; then, the channels encoded, when a PSD cannot hold the file: an RLE row past 65 535 bytes, a layer section past 4 GiB, or a file past 2 GB, which Adobe documents as Photoshop's limit for a PSD. +- **Saving layers** (`psdsave_layers`, `psdsave_layers_buffer`, `psdsave_layers_target`; in C + `minipsd_vips_psdsave_layers`, `_buffer`, `_target`, through `vips_call_split`). A saver takes + one image, so this is a `VipsOperation` over a `VipsArrayImage`, as `vips_composite` is, + `NOCACHE` since it writes. Each image is a record, bottom first, read from the fields the + loader sets (the table above), so a file's records loaded and saved come back: + + | Field | Written as | + |---|---| + | `psd-layer-name` | the name; else "Layer N" (N from 1, bottom first), a divider "" | + | `-opacity`, `-hidden`, `-clipping` | as given; opacity clamped to 0 to 255 | + | `-blend-mode` | a four-character key, any other length refused; `pass` on a group's top record as none, which minipsd writes as Photoshop does (record `norm`, `lsct` `pass`) | + | `-empty`, `-section` | no pixels, no rectangle; `section_divider_type` (another text refused) | + | `xoffset`, `yoffset` | the rectangle, less (`left`, `top`) | + | `-mask` and its two fields | the mask: one band of the layers' depth (else refused), its rectangle from its offsets, `flags` 0x02 when disabled, encoded as the layer is | + + Every record with pixels is of one mode and depth (the first such decides, else the composite, + else the first record); none is converted. Each image is read once, in bands, into its + channels (one helper encodes a layer, a mask and the composite), and the document goes to + `minipsd_write` with them stored; PSB as `psdsave` chooses it, retried over every block. + What a record holds beyond these fields (text, effects, smart objects, an adjustment's + settings, Blend If, its other flags and blocks) is not carried. + + Options: `composite`, `compression`, `psb`, `width`, `height`, `left`, `top`. The canvas is + `width` by `height`; else the composite's size; else the smallest from (`left`, `top`) that + holds every record with pixels; (`left`, `top`) is where it lies in the records' coordinates. + The composite is the given one (the canvas's size, the records' mode and depth), or composed: + `vips_composite` of the visible records onto a transparent canvas, blend modes by the table + above (others over), opacity, a mask (not disabled; its default colour outside its rectangle) + and its groups' visibility and opacity (walked top down) in the alpha, flattened against + white (CMYK: no ink) when no record has alpha. Clipping, a group's own blend mode and mask are + not drawn. libvips computes in floats: on the test's RGBA, 4774 of 244 412 bytes are off by + 1 or 2 levels where alpha is partial, none where it is full. 16-bit CMYK needs the composite + given (libvips takes CMYK's alpha as 8-bit). - **Builds.** The plugin compiles minipsd in privately (`MINIPSD_STATIC`). On Windows it uses the zip's `libz1` (zlib-ng, built zlib-compatible) with zlib 1.3.1's headers, fetched and pinned, since the zip has none; elsewhere pkg-config's `vips` and `zlib`. A program's own libvips replaces the fetched one by `FETCHCONTENT_SOURCE_DIR_MINIPSD_LIBVIPS`, or by the targets `minipsd_libvips` and `minipsd_libvips_zlib`. Released as `minipsd-vips-X.Y.Z.zip` (`tools/package_vips.cmake`): the plugin as a CMake project, minipsd's single header private to - it, built from the zip and its example run by CI and by the release. libvips 8.17 replaced + it, with its SBOM fragment (`minipsd-vips.spdx.json`, every other file with its checksums, + `MINIPSD_SBOM_FILE` on `minipsd::vips`; `tools/check_spdx.cmake` checks it), built from the zip + and its example run by CI and by the release. libvips 8.17 replaced `VipsSaveable` with `VipsForeignSaveable`; both are handled. CI's `msvc` job runs its test, which compares every pixel with minipsd's own decode, on psd-tools' set too, and checks that a 30000-row image read sequentially leaves libvips holding a few rows' worth. diff --git a/libvips-plugin/release/README.md b/libvips-plugin/release/README.md index 6603b97..332510c 100644 --- a/libvips-plugin/release/README.md +++ b/libvips-plugin/release/README.md @@ -1,7 +1,9 @@ # minipsd's libvips plugin minipsd as libvips' loader and saver of PSD and PSB: `vips_image_new_from_file` and -`vips_image_write_to_file` take `.psd` and `.psb` in your program. MIT, as minipsd +`vips_image_write_to_file` take `.psd` and `.psb` in your program. It reads a file's composite or +any of its layers, with their look, masks and groups, and writes one image, or several as the +layers of one document (`psdsave_layers`). MIT, as minipsd (`LICENSE.MIT`). Project: ; the full description is its manual's "PSD in libvips: the plugin". @@ -79,13 +81,27 @@ vips_image_write_to_file(page, "copy.psd[compression=zip]", NULL); ``` - **Loading** (`psdload`, `psdload_buffer`, `psdload_source`): the composite, or with `layer` one - layer, bottom first. + layer, bottom first, placed by `xoffset` and `yoffset`, its look in `psd-layer-*` fields + (name, opacity, hidden, blend mode, clipping), its mask an image in `psd-layer-mask`, a + group's records marked by `psd-layer-section`, a record without pixels one transparent pixel + with `psd-layer-empty`. - **Saving** (`psdsave`, `psdsave_buffer`, `psdsave_target`): an image with alpha as one layer and the composite; without, flat (the composite only), or with `layered` one layer as well. Grey, RGB or CMYK at 8 or 16 bits, grey and RGB as floats. Options: `compression` (`rle`, the default; `zip`; `zip-predicted`; `none`), `psb`, `layer_name`, `layered`. The image is read once, in bands: memory about the file's size. A PSB when asked, for a `.psb` file, or when a PSD cannot hold the image. +- **Saving layers** (`psdsave_layers`, `_buffer`, `_target`; in C `minipsd_vips_psdsave_layers`, + declared in `minipsd_vips.h`): an array of images, one a layer, bottom first, each read from + the same fields psdload sets, so a file's layers loaded and saved come back. The composite is + given (`composite`), or composed by `vips_composite`. The canvas: `width`, `height`, `left`, + `top`. Text, effects, smart objects and adjustments' settings are not carried. + + ```c + VipsImage *layers[2] = { page, blank }; /* blank: any image with psd-layer-empty set */ + vips_image_set_int(blank, "psd-layer-empty", 1); + minipsd_vips_psdsave_layers(layers, 2, "template.psd", "composite", page, NULL); + ``` minipsd is compiled into the plugin privately, so a program that also uses minipsd itself, at any version, keeps its own. From 00aaeeb8a88331ef04b7331c699170893be4bea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mucha?= Date: Mon, 26 Oct 2026 01:33:29 +0100 Subject: [PATCH 22/22] test(writer): a mask wider than the canvas sizes minipsd_write's pieces minipsd_write cuts the file into pieces of 4 KiB, or of the widest row's bound; the widest can be a mask's, which no test had, so CI's line coverage stopped at 99.9%. WriteFailureIsReported now checks the pieces: at most 4 KiB without a wide row, more for a 5000-wide mask, within its row's bound. --- docs/ROADMAP.md | 2 +- tests/test_writer.cpp | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e168285..c3abad8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -195,7 +195,7 @@ Additive. See [Reader](CODE-ARCH.md#reader). workflow checks CI's run on the tagged commit instead of running it again. Before it: [COMPATIBILITY.md](COMPATIBILITY.md) regenerated with the version (done), the PR merged and CI green on `master`. -- [ ] **S4.12** Release **0.4.0** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): breaking, so a +- [x] **S4.12** Release **0.4.0** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): breaking, so a MINOR (`composite_alpha` for every composite, the compression constants renumbered, `minipsd_read` giving transparency as the file has it). The libvips plugin (S7.2 to S7.6), layered documents written through libvips included, and its zip; layers without transparency, a layer's own compression, the row functions; diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index e7bd9de..b7ecc1a 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -1309,18 +1309,32 @@ TEST(MinipsdWriterTest, WriteFailureIsReported) const Bytes px = testImage(128, 64); // several 4 KiB callbacks const minipsd_layer layer{"L", px.data(), 255, 0}; const minipsd_doc d = docOf(128, 64, &layer, 1); - struct Calls { int made, failAt; }; - const auto failing = [](void *user, const void *, size_t) -> int { + struct Calls { int made, failAt; size_t largest; }; + const auto failing = [](void *user, const void *, size_t size) -> int { auto *c = static_cast(user); + c->largest = std::max(c->largest, size); return ++c->made != c->failAt; }; - Calls all{0, 0}; // never fails + Calls all{0, 0, 0}; // never fails ASSERT_EQ(minipsd_write(&d, nullptr, failing, &all), MINIPSD_OK); ASSERT_GE(all.made, 3); + EXPECT_LE(all.largest, size_t(MPSD__CHUNK)) << "pieces of 4 KiB: no row is wider"; + + // A mask wider than the canvas and every layer: the pieces grow to hold its row. + constexpr uint32_t kWide = 5000; + const Bytes wide(kWide, 0x80); + const minipsd_mask mask{.bottom = 1, .right = static_cast(kWide), .default_color = 255, .pixels = wide.data()}; + minipsd_layer masked = layer; + masked.mask = &mask; + const minipsd_doc withMask = docOf(128, 64, &masked, 1); + Calls widest{0, 0, 0}; + ASSERT_EQ(minipsd_write(&withMask, nullptr, failing, &widest), MINIPSD_OK); + EXPECT_GT(widest.largest, size_t(MPSD__CHUNK)) << "the mask's row"; + EXPECT_LE(widest.largest, size_t(MINIPSD_ROW_BOUND(kWide, mpsd__doc_depth(&withMask)))); // The first piece, one in the middle, and the last. for (int failAt : {1, 2, all.made}) { - Calls c{0, failAt}; + Calls c{0, failAt, 0}; EXPECT_EQ(minipsd_write(&d, nullptr, failing, &c), MINIPSD_ERR_WRITE) << "failing call " << failAt; EXPECT_EQ(c.made, failAt) << "the writer stops calling back after the first failure"; }