diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 86d38a3..281adb6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,9 +30,13 @@ jobs:
matrix:
include:
# windows-2022, not windows-latest: the msvc preset's generator is "Visual Studio 17 2022".
- - { os: windows-2022, preset: msvc }
+ # 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 }
- { os: windows-latest, preset: mingw }
- - { os: ubuntu-latest, preset: linux-gcc }
+ # 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.
+ - { os: ubuntu-latest, preset: linux-gcc, psd_tools_files: ON }
- { os: ubuntu-latest, preset: linux-clang }
runs-on: ${{ matrix.os }}
steps:
@@ -74,7 +78,7 @@ jobs:
- name: Configure, build, test
if: matrix.preset != 'mingw'
run: |
- cmake --preset ${{ matrix.preset }} -DMINIPSD_REQUIRE_PSD_TOOLS=ON "-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_example || 'OFF' }} "-DPython3_EXECUTABLE=${{ steps.python.outputs.python-path }}"
cmake --build --preset ${{ matrix.preset }}
ctest --preset ${{ matrix.preset }}
diff --git a/README.md b/README.md
index b737b6f..0e2303e 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Procreate open as layered documents.
# Status
-**release: 0.2.0**
+**release: 0.3.0**
The API is [include/minipsd.h](include/minipsd.h); the implementation is in
[include/minipsd/](include/minipsd/).
@@ -35,7 +35,7 @@ The API is [include/minipsd.h](include/minipsd.h); the implementation is in
#include "minipsd.h"
minipsd_layer layers[2] = {
- { .name = "Template", .rgba = rgba, .opacity = 255 }, /* bottom, the full canvas */
+ { .name = "Template", .pixels = rgba, .opacity = 255 }, /* bottom, the full canvas */
{ .name = "Layer", .opacity = 255 }, /* empty layer above it */
};
minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = layers, .layer_count = 2 };
@@ -56,7 +56,9 @@ for a file (UTF-8 paths on Windows), a buffer and a non-blocking pipe are in
## What it writes
-An RGB document with transparency, 8 or 16 bits a channel: layers anywhere on or off the canvas (by default the
+A document in any colour mode at the depths Photoshop has it (RGB, grey, CMYK, Lab, duotone and
+multichannel at 8 and 16 bits, RGB and grey at 32, indexed at 8, bitmap at 1), no colour
+converted: layers anywhere on or off the canvas (by default the
full canvas), or empty, with UTF-8 names, opacity and visibility; the merged composite image,
yours or composed as a best effort; the resolution; an optional ICC profile; groups, layer
masks, blend modes, clipping, and blocks and resources kept from a file (text, effects), as
@@ -65,15 +67,18 @@ Channels are PackBits RLE or raw, or ZIP through your deflate (zlib's `compress2
implements none). Each side is limited to 30 000 px, or 300 000 px written as a
PSB (Photoshop's large document format).
-Not written (yet): 32-bit, other colour modes. The full contract is in [docs/SPECIFICATION.md](docs/SPECIFICATION.md).
+The writer composes RGB only: other modes and 32 bits bring their composite. The full contract
+is in [docs/SPECIFICATION.md](docs/SPECIFICATION.md).
## What it reads
Any PSD or PSB: its size, colour mode, depth, dpi, ICC profile, resources, and every layer
-with its name, rectangle, opacity, visibility, blend mode and raw additional data. Pixels of
+with its name, rectangle, opacity, visibility, blend mode and raw additional data. Metadata
+parsed into plain values: resources found by id and named by Adobe's table, the resolution,
+version info (whether the composite is real), guides, alpha channel names, and a layer's id,
+colour label, fill opacity and locks. Pixels of
every colour mode and depth (1, 8, 16, 32 bits) decode as stored, layers and composite, into
-memory of any layout; 8- and 16-bit RGB files also whole into a document that `minipsd_write`
-takes back. ZIP-compressed channels (Photoshop's 16- and 32-bit layers)
+memory of any layout; and whole into a document that `minipsd_write` takes back. ZIP-compressed channels (Photoshop's 16- and 32-bit layers)
decode through the caller's inflate (zlib's `uncompress`, libdeflate or miniz: minipsd implements
no deflate), and are copied as they are when such a file is read and written back. A file can be read from its start only, and untrusted
files are bounded by caller limits. How it does on 315 files from
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 952e43c..3ba54a1 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -3,12 +3,16 @@
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.0] — 2026-10-23
**Breaking:** `minipsd_write`, `minipsd_writer_workspace_size`, `minipsd_writer_init` and
`minipsd_doc_check` take a `const minipsd_write_opts *` after the document. To migrate, pass
`NULL`: the document is written as before.
+**Breaking:** `minipsd_layer.rgba` is `minipsd_layer.pixels`: the mode's colour channels then
+transparency, which in RGB is the same R, G, B, A as before. To migrate, rename the field; a
+positional initialiser needs no change.
+
### Added
- The reader, first part: `minipsd_reader_init` describes a PSD held in memory (size, depth,
colour mode, dpi, ICC profile, layer count), from the whole file or a prefix
@@ -26,6 +30,9 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 r
composite's transparency with a global `Mtrn`, `Mt16` or `Mt32` block rather than a negative
layer count; both count.
- `examples/read_file.c`: a PSD described layer by layer, every layer and the composite decoded.
+- `examples/vips_composite.c`: layers flattened by libvips (`vips_composite`, blend modes mapped)
+ and written with that composite, then read back; built with `MINIPSD_VIPS_EXAMPLE`, which on
+ Windows fetches libvips' prebuilt build. A "With libvips" chapter in the manual.
- `examples/hello_write.c` and `examples/hello_read.c`: the shortest programs that write and read
a PSD, which open a rewritten [MANUAL.md](MANUAL.md): call-order diagrams, a file read in
parts, memory, return codes and configuration.
@@ -36,7 +43,13 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 r
`minipsd_doc`. The writer writes that image, matted against white unless marked as already
matted, and composes nothing. Without one the layers are composed as before, now documented as
a best effort.
-- `minipsd_read`: a whole 8- or 16-bit RGB file into a `minipsd_doc` that `minipsd_write` takes as it
+- Every colour mode and depth written: `color_mode` and `color_mode_data` at the end of
+ `minipsd_doc` (`MINIPSD_MODE_*`, Adobe's numbers; 0 is RGB unless `depth` is 1, bitmap). Grey,
+ CMYK, Lab, duotone and multichannel at 8 and 16 bits, RGB and grey at 32 (`float` samples, the
+ layers in `Lr32`), indexed at 8 with its palette, bitmap at 1 (a byte a pixel, packed by the
+ writer); bitmap, indexed and multichannel without layers. No colour is converted; every mode
+ but RGB, and 32 bits, brings its composite, and a supplied one is written as given.
+- `minipsd_read`: a whole file of any mode and depth the writer takes into a `minipsd_doc` that `minipsd_write` takes as it
is, every byte copied into one block so the file can be freed at once; `minipsd_doc_free`;
`minipsd_read_size` and `minipsd_read_into` for the caller's own memory. `lost`
(`MINIPSD_LOST_*`) says what the document could not carry: extra channels.
@@ -113,7 +126,7 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 r
still wins.
- A stored layer without a transparency channel is written without one, as its file has it,
instead of with an opaque one.
-- A layer's all-zero rectangle means the full canvas only with `rgba`: stored channels take the
+- A layer's all-zero rectangle means the full canvas only with `pixels`: stored channels take the
rectangle as it is.
- A layer's names kept from a file: `name_pascal` at the end of `minipsd_layer` holds its legacy
name as stored, and while `name` still says what the file's names say, the legacy name and the
diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md
index 05004f2..9cdfefe 100644
--- a/docs/CHEATSHEET.md
+++ b/docs/CHEATSHEET.md
@@ -11,6 +11,7 @@ consumers, so there is nothing to ship from a build.
| `linux-gcc` | Linux / WSL | GCC + Ninja |
| `linux-clang` | Linux / WSL | Clang + Ninja |
| `linux-asan` | Linux / WSL | GCC + AddressSanitizer and UBSan (no round trip) |
+| `linux-tsan` | Linux / WSL | Clang + ThreadSanitizer; on new kernels it needs `sudo sysctl vm.mmap_rnd_bits=28` |
| `linux-tidy` | Linux / WSL | Clang + clang-tidy on the tests and the header |
| `linux-coverage` | Linux / WSL | GCC + `--coverage`; report with gcovr below |
| `linux-fuzz` | Linux / WSL | Clang + libFuzzer, ASan and UBSan; local only, see [Fuzzing](#fuzzing) |
@@ -152,7 +153,8 @@ paths also check the SBOM fragment's version and that it lists the headers.
| `MINIPSD_BUILD_TESTS` | ON only as the top-level project | the tests above |
| `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_PSD_TOOLS_FILES` | OFF (CI: OFF) | fetch psd-tools' 279 test PSDs (v1.25.0, about 22 MB, pinned by hash) and read every one |
+| `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_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 8842af0..2eaa8e7 100644
--- a/docs/CODE-ARCH.md
+++ b/docs/CODE-ARCH.md
@@ -237,7 +237,7 @@ int minipsd_reader_init (minipsd_reader **r, const void *data, size_t size,
minipsd_info *info);
uint64_t minipsd_reader_needed(const minipsd_reader *r); /* after MINIPSD_ERR_SHORT */
-int minipsd_reader_next_resource(minipsd_reader *r, minipsd_resource *res); /* MORE: got one; OK: no more */
+int minipsd_reader_next_resource(const minipsd_reader *r, size_t *pos, minipsd_resource *res); /* MORE / OK */
int minipsd_reader_next_layer (minipsd_reader *r, minipsd_layer_view *layer); /* MORE: got one; OK: no more */
int minipsd_layer_name (const minipsd_layer_view *layer, char *utf8, size_t cap, size_t *needed);
int minipsd_layer_next_block (const minipsd_layer_view *layer, size_t *pos, minipsd_block *block); /* MORE / OK */
@@ -337,7 +337,7 @@ typedef struct minipsd_target { minipsd_plane channel[MINIPSD_PLANES]; } minipsd
### Pixel conventions (`X3`)
-- **Straight (not premultiplied) alpha.** It is what `minipsd_layer.rgba` takes, so a decoded
+- **Straight (not premultiplied) alpha.** It is what `minipsd_layer.pixels` takes, so a decoded
layer can be written back unchanged (`E4`). A caller that wants premultiplied does the
multiplication; a flag can be added later without breaking anything.
- **A layer is decoded as stored.** Its opacity and hidden flag stay in the view and are not
@@ -547,13 +547,13 @@ int composite_matted; /* non-zero: composite is already matted, wri
| Field | Zero means | Notes |
|---|---|---|
-| `int32_t top, left, bottom, right` | all four zero: the full canvas (with `rgba`; stored channels take the rectangle as it is) | the fields of `minipsd_layer_view`, in its order. `rgba` holds `(right - left) * (bottom - top) * 4` bytes. A rectangle may reach past the canvas; an inverted one, or an area without `rgba`, is `MINIPSD_ERR_ARG`; a side above `MINIPSD_MAX_SIDE` is `MINIPSD_ERR_SIZE` (RLE row counts are 16 bits). Row tables and channel sizes follow the rectangle; the composed composite blends at the offset and clips to the canvas |
+| `int32_t top, left, bottom, right` | all four zero: the full canvas (with `pixels`; stored channels take the rectangle as it is) | the fields of `minipsd_layer_view`, in its order. `pixels` holds `(right - left) * (bottom - top) * 4` bytes. A rectangle may reach past the canvas; an inverted one, or an area without `pixels`, is `MINIPSD_ERR_ARG`; a side above `MINIPSD_MAX_SIDE` is `MINIPSD_ERR_SIZE` (RLE row counts are 16 bits). Row tables and channel sizes follow the rectangle; the composed composite blends at the offset and clips to the canvas |
| `char blend_mode[4]` | `norm` | written as given |
| `uint8_t clipping` | not clipped | written as given |
| `uint8_t flags` | none | the record's other flag bits (0x01 transparency locked, 0x08 and 0x10 pixel data irrelevant), written as given; 0x02 comes from `hidden` |
| `minipsd_bytes name_pascal` | the writer's own names | the legacy name kept from a file (`minipsd_read` sets it); while `name` says what the kept names say (a kept `luni`, else this one as Latin-1), both are written as they are, otherwise the writer's own |
| `minipsd_bytes additional_info` | no extra blocks | the layer's additional-information blocks as one blob, in the format of the reader's `additional_info` view; written after the writer's own blocks. `minipsd_doc_check` walks them: a malformed run is `MINIPSD_ERR_ARG` |
-| `const minipsd_bytes *stored` | encode `rgba` | four channel blobs (R, G, B, A, as `minipsd_layer_view.channel`), each the stored data with its compression marker and row counts; transparency of size 0 is none. The writer copies each as one piece instead of encoding `rgba`, and writes no transparency channel where none is stored; the sizing pass takes their sizes. `minipsd_doc_check` checks each against the rectangle (raw rows, or RLE counts adding up within the blob; ZIP passes as it is); a layer with `stored` and no `rgba` needs a supplied `composite`, since nothing could compose it. Name, opacity, visibility, `blend_mode`, clipping and `additional_info` still come from the layer |
+| `const minipsd_bytes *stored` | encode `pixels` | four channel blobs (R, G, B, A, as `minipsd_layer_view.channel`), each the stored data with its compression marker and row counts; transparency of size 0 is none. The writer copies each as one piece instead of encoding `pixels`, and writes no transparency channel where none is stored; the sizing pass takes their sizes. `minipsd_doc_check` checks each against the rectangle (raw rows, or RLE counts adding up within the blob; ZIP passes as it is); a layer with `stored` and no `pixels` needs a supplied `composite`, since nothing could compose it. Name, opacity, visibility, `blend_mode`, clipping and `additional_info` still come from the layer |
`minipsd_doc`, at the end, after `composite` and `composite_matted`:
@@ -626,7 +626,7 @@ typedef struct minipsd_read_opts {
`MINIPSD_ERR_UNSUPPORTED`, since the writer could not take it.
- **`opts->stored`** decodes nothing: each layer keeps its compressed channels, a fraction of its
pixels' size. The caller decodes the one layer it edits through the reader, from the same
- file, sets that layer's `rgba` and clears its `stored`, then writes. Untouched layers are never
+ file, sets that layer's `pixels` and clears its `stored`, then writes. Untouched layers are never
decoded nor encoded again, and the file's own composite is copied, neither rendered nor
decoded (`E1`), alpha and spot channels with it (the resources naming them are kept). ZIP
channels pass this way without an inflate; a layer without a transparency channel is written
@@ -641,8 +641,10 @@ typedef struct minipsd_read_opts {
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
decoded pixels. A block larger than `SIZE_MAX` is `MINIPSD_ERR_SIZE`.
-- **What it accepts:** 8- and 16-bit RGB, as the decoder; the document has the file's `depth`.
- Anything else is `MINIPSD_ERR_UNSUPPORTED`. Layers read from `Lr16` are the document's, so
+- **What it accepts:** what the writer takes back: the document has the file's `color_mode`,
+ `color_mode_data` and `depth`, and a file the writer would refuse (a mode at a depth it does not
+ write, layers in a flat mode) is `MINIPSD_ERR_UNSUPPORTED`, checked first with the writer's own
+ `mpsd__doc_mode_ok` on the file's header. Layers read from `Lr16` or `Lr32` are the document's, so
`layer_tail` keeps that block empty, where it was, and the writer puts them back in it.
- **No allocator passed at run time.** The caller's block and `MINIPSD_MALLOC` cover both cases;
a `{alloc, free, user}` variant is an addition when a caller needs one.
@@ -687,6 +689,7 @@ feature is serialization: written and read as data. The composite composed as a
| **Layer groups (`lsct`)** | done: `section_divider_type` (`MINIPSD_SECTION_*`); zero = layer | done: the `lsct` block, the group's mode in it; a kept one of the same type as it is; nesting checked | done: `section_divider_type` in the view | — | the composed composite draws groups flat |
| **Layer masks** | done: `mask` (`minipsd_mask`: rectangle, colour, flags, pixels or stored, kept real mask and rest) | done: mask data, channels -2 and -3 | done: the view's mask, `minipsd_decode_mask` | — | the composed composite ignores masks |
| **16-bit channels** | done: `depth` on `minipsd_doc`; zero = 8 | done: 2-byte samples, big-endian, in the layer info | done: `depth` in `minipsd_decode_opts`, layers from `Lr16`, `minipsd_read` | done: `bytes` (`mpsd__at`, samples), `packbits` over a row's bytes, `composite` against 65535 | ZIP channels decode through the caller's inflate (S6.9), or pass as stored |
+| **Typed views** | — (output only: `minipsd_resolution_info`, `minipsd_version_info`, `minipsd_grid_and_guides`, `minipsd_guide`, `minipsd_block_settings`) | — | `views.h`: a resource or a layer view in, a struct out; `find_resource`, `next_resource` with the caller's `pos` | `format`, `utf` | the writer keeps resources and blocks as bytes; nothing to write |
| **PSB (version 2)** | done: `format` on `minipsd_doc` (`MINIPSD_PSD`, `MINIPSD_PSB`; zero = PSD): the layout of everything in the document, set by `minipsd_read`; the output format a write option, zero = the document's (decided over two document fields and over a converting copy: one field never describes a wish, and a translating writer stays zero-copy, as `tiffsave`'s `bigtiff` does) | done: 8-byte lengths, 4-byte RLE counts; `minipsd_write_opts.format` on the write calls, kept bytes translated while streaming (RLE count tables, long keys' lengths), by a policy per long key in `format.h` | done: both versions, `format` in the info and the view, `minipsd_read` sets the document's | `format`: widths, long keys, blocks | sides up to 300 000 px |
## Distribution of a multi-file library
@@ -771,9 +774,9 @@ Taken at review, 2026-10-14.
Taken at the S4.1 review, 2026-10-15 (the reader):
-7. **The writer's layer source does not share the pixel descriptor.** `minipsd_layer.rgba` stays.
+7. **The writer's layer source does not share the pixel descriptor.** `minipsd_layer.pixels` stays.
A strided or row-by-row source (`W6`, `W7`) would be a field added at the end of
- `minipsd_layer`, with the same shape but `const` pointers and `rgba` as the default when it
+ `minipsd_layer`, with the same shape but `const` pointers and `pixels` as the default when it
is zero: the reader writes through the pointers and the writer only reads, so one type for
both would need a cast.
8. **Short input is `MINIPSD_ERR_SHORT` plus `minipsd_reader_needed`**, not `MINIPSD_MORE`:
diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md
index 099bce4..4ef15ef 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.2.0, over 315 files of psd-tools v1.25.0.
+0.3.0, over 315 files of psd-tools v1.25.0.
## Credits
diff --git a/docs/MANUAL.md b/docs/MANUAL.md
index 6a0a134..4d560c4 100644
--- a/docs/MANUAL.md
+++ b/docs/MANUAL.md
@@ -48,7 +48,7 @@ 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", .rgba = rgba, .opacity = 255};
+ minipsd_layer layer = {.name = "Hello", .pixels = rgba, .opacity = 255};
minipsd_doc doc = {.width = 64, .height = 64, .dpi = 72.0, .layers = &layer, .layer_count = 1};
FILE *f;
int i, rc;
@@ -177,7 +177,7 @@ The examples below write this document:
```c
minipsd_layer layers[2] = {
- { .name = "Template", .rgba = rgba, .opacity = 255 }, /* width * height * 4, straight alpha */
+ { .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 };
@@ -185,11 +185,11 @@ minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = la
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
-its rectangle on the canvas, which may reach past the edges; `rgba` then holds that rectangle:
+its rectangle on the canvas, which may reach past the edges; `pixels` then holds that rectangle:
```c
/* 64 x 32 pixels, its top-left corner at x 20, y 10 */
-minipsd_layer logo = { .name = "Logo", .rgba = logo_rgba, .opacity = 255,
+minipsd_layer logo = { .name = "Logo", .pixels = logo_rgba, .opacity = 255,
.top = 10, .left = 20, .bottom = 42, .right = 84 };
```
@@ -202,8 +202,8 @@ shows:
minipsd_mask fade = { .top = 0, .left = 0, .bottom = 32, .right = 64, .pixels = ramp };
minipsd_layer layers[4] = {
{ .name = "", .opacity = 255, .section_divider_type = MINIPSD_SECTION_GROUP_END },
- { .name = "Logo", .rgba = logo_rgba, .opacity = 255, .bottom = 32, .right = 64, .mask = &fade },
- { .name = "Shadow", .rgba = shadow_rgba, .opacity = 128, .bottom = 32, .right = 64 },
+ { .name = "Logo", .pixels = logo_rgba, .opacity = 255, .bottom = 32, .right = 64, .mask = &fade },
+ { .name = "Shadow", .pixels = shadow_rgba, .opacity = 128, .bottom = 32, .right = 64 },
{ .name = "Branding", .opacity = 255, .section_divider_type = MINIPSD_SECTION_GROUP },
};
```
@@ -213,11 +213,29 @@ a `uint16_t` in host order, 0 to 65535; the pointers keep their type, so cast:
```c
uint16_t *deep = malloc((size_t)width * height * 4 * sizeof *deep); /* straight alpha */
-minipsd_layer layer = { .name = "Deep", .rgba = (const uint8_t *)deep, .opacity = 255 };
+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 };
```
+Another colour mode sets `color_mode` (`MINIPSD_MODE_*`). A pixel is then the mode's colour
+channels then transparency, as the file stores them: nothing is converted, and CMYK is inverted
+(255 no ink). The writer composes RGB only, so every other mode brings its composite:
+
+```c
+uint8_t *cmyka = malloc((size_t)width * height * 5); /* C, M, Y, K, transparency */
+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 };
+```
+
+RGB and grey also come at 32 bits (`depth` 32), every sample a `float` from 0.0 to 1.0, again with
+a composite. Bitmap, indexed and multichannel documents have no layers, only the composite: a
+bitmap one is `depth` 1, a byte a pixel (0 white, any other black), which the writer packs; an
+indexed one is a palette entry a pixel, its 768-byte palette in `color_mode_data`; a multichannel
+one comes from a file (`minipsd_read`), its composite copied as stored.
+
A document wider or taller than 30 000 px, a long webtoon strip say, is written as a PSB, the
large document format, up to 300 000 px a side; nothing else in it changes:
@@ -401,7 +419,8 @@ Only `init` is required. Every step below it is independent of the others, so sk
do not need: a file manager stops after `init`; a viewer goes straight to the composite, as
[Read a PSD](#read-a-psd) could; a layer exporter never decodes the composite. Resources and
layers are iterators: each call returns `MINIPSD_MORE` with one item, then `MINIPSD_OK` when
-there are no more. Calling `init` again restarts both.
+there are no more. The resources' cursor is yours (`pos`, from 0), as a layer's blocks' is: walk
+them as often as you like. The layers' is the reader's: calling `init` again restarts it.
A PSB (Photoshop's large document format, sides up to 300 000 px) reads through the same calls.
`info.format` is `MINIPSD_PSB` then; the decoded pixels, names and rectangles are no different,
@@ -570,7 +589,8 @@ come as bytes, for a parser of their own. To list what a file holds, walk the re
name each id:
```c
-while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE) {
+size_t pos = 0;
+while (minipsd_reader_next_resource(r, &pos, &res) == MINIPSD_MORE) {
const char *name = minipsd_resource_id_name(res.id); /* Adobe's name; NULL if unlisted */
printf("%u %s, %zu bytes\n", (unsigned)res.id, name ? name : "(unknown)", res.data.size);
}
@@ -617,6 +637,10 @@ rc = minipsd_write(doc, NULL, to_file, f);
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`.
+
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
file, so the file's bytes can go as soon as the call returns.
@@ -648,7 +672,7 @@ flowchart TD
read["minipsd_read(data, ..., &opts, &doc, &lost)
opts.stored = 1"]
read -. "every layer's channels and the composite, copied as stored" .-> doc["doc"]
doc --> pick["the layer to change:
minipsd_reader_init, minipsd_reader_next_layer,
minipsd_decode_layer, from the same data"]
- pick --> set["layer.rgba = new pixels
layer.stored = NULL"]
+ pick --> set["layer.pixels = new pixels
layer.stored = NULL"]
set --> write["minipsd_write(doc, NULL, callback, user)"]
write -. "that layer encoded, the others copied" .-> out["the file"]
classDef minipsd stroke-dasharray: 5 5
@@ -673,6 +697,41 @@ Each is planned work. If a file of yours sets one, please
made it (the program, and the feature you used): that is what sets the order of the work.
[examples/read_file.c](../examples/read_file.c) prints the bits of any file.
+## With libvips
+
+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
+alpha, rows top down), so nothing is converted.
+[examples/vips_composite.c](../examples/vips_composite.c) is the whole program; its steps:
+
+1. **Layers out of libvips.** `vips_image_write_to_memory` gives a layer's `pixels` as it is
+ (a 3-band image takes `vips_bandjoin_const1(image, 255)` first).
+2. **The composite in libvips.** `vips_composite` onto a transparent canvas of the document's
+ size, each layer at its rectangle (`"x"`, `"y"`), its opacity multiplied into alpha
+ (`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.
+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.
+
+```c
+VipsImage *in[3] = { canvas, background, disc }, *out;
+int modes[2] = { VIPS_BLEND_MODE_OVER, VIPS_BLEND_MODE_MULTIPLY }; /* "norm", "mul " */
+int xs[2] = { 0, -32 }, ys[2] = { 0, 96 }; /* the layers' left, top */
+VipsArrayInt *x = vips_array_int_new(xs, 2), *y = vips_array_int_new(ys, 2);
+vips_composite(in, &out, 3, modes, "x", x, "y", y, NULL);
+doc.composite = vips_image_write_to_memory(out, &size); /* width * height * 4, straight alpha */
+```
+
+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.
+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.
+
## 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 e46959d..97408f1 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -183,10 +183,11 @@ Additive. See [Reader](CODE-ARCH.md#reader).
- Extensibility rules: the reader needs no opt-in macro;
- README (what it reads), the header's brief, the CMake description (SBOM, `.pc`,
packages), USE-CASES (what is done), CODE-ARCH (stages).
-- [ ] **S4.10** Release **0.3.0** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md), with stages 4 to 6:
+- [ ] **S4.10** Release **0.3.0** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md), with stages 4 to 6
+ and S7.1, the API frozen from then:
`minipsd_write_opts` became a parameter of the write calls, a break, so it is a MINOR under
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`), so that
+ 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.
## 5. Round trip and conveniences
@@ -401,7 +402,8 @@ feature; a supplied composite needs nothing. See
checks every psd-tools file's layers and composite against psd-tools. `minipsd_read`
stays 8- and 16-bit RGB until the writer takes the other modes.
- **Write.** The same modes at 1, 8, 16 and 32 bits, each where Photoshop allows it; the
- palette and the duotone data written as given.
+ palette and the duotone data written as given. *Moved to S6.19*: S6.13 is done for
+ reading.
*Done when:* psd-tools' files of every mode and depth decode to the samples psd-tools
gives, and COMPATIBILITY.md has no file described for its mode or depth; documents of
each mode and depth that minipsd writes decode the same in psd-tools and in minipsd; the
@@ -478,26 +480,44 @@ feature; a supplied composite needs nothing. See
decide whether that is acceptable. It would give a text layer's text (`TySh`, key
`Txt `), a smart object's name, effects' flags.
*Done when:* as S6.17's.
+- [x] **S6.19** Every colour mode and depth written (S6.13's Write half). Reading them is done
+ (S6.13): any mode and depth is described and decoded. The writer takes 8- and 16-bit RGB
+ only (`minipsd_doc` has no colour mode), so this step is writing grey, CMYK, Lab,
+ multichannel and duotone at 8 and 16 bits, grey and RGB at 32, bitmap at 1, indexed at 8,
+ each where Photoshop's files have layers (bitmap, indexed and multichannel have none); the
+ palette and the duotone data written as given. `minipsd_read` then takes every mode. The
+ saver (S7.4) needs it for images that are not RGB.
+ *Done when:* documents of each mode and depth that minipsd writes decode the same in
+ psd-tools and in minipsd, and `minipsd_read` round-trips psd-tools' files of every mode.
## 7. Interoperability
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; how they reach libvips is not this roadmap's.
+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.
-- [ ] **S7.1** `examples/vips_composite.c`: a document's composite rendered with libvips
+- [x] **S7.1** `examples/vips_composite.c`: a document's composite rendered with libvips
(`vips_composite`, blend keys mapped as in the spec's
[Working with libvips](SPECIFICATION.md#working-with-libvips)), then written with it;
built and run only when pkg-config finds `vips`. A "With libvips" chapter in
[MANUAL.md](MANUAL.md). The pattern a program such as Platemaker follows to export layered
PSDs with 0.3.0.
+ As done: behind `MINIPSD_VIPS_EXAMPLE` (off by default), not pkg-config alone: Windows
+ fetches libvips' prebuilt "web" build pinned to the newest release (8.18.7), elsewhere
+ pkg-config. Layers made in the example, or an 8-bit RGB file's (`minipsd_read`); the file
+ 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:
- **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.
- **Access.** Strips from several threads (S6.12, S6.14).
- **Saving.** One image as a layer with its composite, or pages as layers.
- - **What minipsd still needs.** Every colour mode and depth (S6.13); the row provider (S7.5);
+ - **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
@@ -508,7 +528,7 @@ the loader and saver; how they reach libvips is not this roadmap's.
memory; RLE, or ZIP through zlib.
*Done when:* psd-tools and GIMP open what it writes, and the loader reads it back the same.
- [ ] **S7.5** Row provider (`W6`): a layer's or the composite's rows asked from a callback instead
- of an `rgba` pointer, twice with RLE or ZIP (sizing, then writing), so it must be
+ 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.
@@ -546,3 +566,9 @@ Large cases. Each would start with a *design* step, and only when a real consume
- A layer's own alpha and spot channels (ids other than R, G, B, transparency and the masks):
no file we have holds one, and Photoshop does not write them. Until a file shows one, such a
layer sets `MINIPSD_LOST_CHANNELS`.
+- The composite's alpha and spot channels in a decoded document: `minipsd_read` without
+ `opts->stored` decodes the composite's colour channels and transparency only, and sets
+ `MINIPSD_LOST_CHANNELS` for the rest; with `opts->stored` every channel is kept (S6.8). Five of
+ psd-tools' files have such channels: `cmyk-alpha-spot.psd`, `cmyk-spot.psd`,
+ `colormodes/4x4_8bit_rgba.psd`, `effects/stroke-without-vector-mask.psd`,
+ `third-party-psds/cactus_top.psd`.
diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md
index 1087871..aab35d1 100644
--- a/docs/SPECIFICATION.md
+++ b/docs/SPECIFICATION.md
@@ -43,8 +43,8 @@ A PSD file is a sequence of five sections, all integers **big-endian**:
| # | Section | What minipsd puts there |
|---|---|---|
-| 1 | **File header** (26 bytes): signature `8BPS`, version (1 = PSD, 2 = PSB), channel count, height, width, bit depth, colour mode | version 1, 4 channels, 8 or 16 bits, RGB |
-| 2 | **Colour mode data**: a length, then a palette for indexed/duotone modes | empty (length 0) |
+| 1 | **File header** (26 bytes): signature `8BPS`, version (1 = PSD, 2 = PSB), channel count, height, width, bit depth, colour mode | the document's version, colour mode and depth; its colour channels and transparency (flat modes: none) |
+| 2 | **Colour mode data**: a length, then a palette for indexed/duotone modes | the document's `color_mode_data`, empty for most modes |
| 3 | **Image resources**: a length, then `8BIM` blocks, each with a numeric ID | 1005 resolution, optionally 1039 ICC profile |
| 4 | **Layer and mask information**: layer records (bounds, channels, blend mode, opacity, flags, name, *additional layer info* blocks such as `luni`), then each layer's channel pixel data | the layers |
| 5 | **Image data**: the merged ("composite") image, used by readers that ignore layers and for thumbnails | the composite |
@@ -110,7 +110,7 @@ The declarations, with every parameter and return code, are in
| `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 |
-| `minipsd_reader_next_resource(r, &res)` | the image resources in file order, each a raw view: `MINIPSD_MORE` per resource, then `MINIPSD_OK` |
+| `minipsd_reader_next_resource(r, &pos, &res)` | the image resources in file order, each a raw view, the cursor the caller's: `MINIPSD_MORE` per resource, then `MINIPSD_OK` |
| `minipsd_reader_find_resource(r, id, &res)` | the first image resource with an id (`MINIPSD_RES_*`), wherever the walk is: `MINIPSD_OK`, or `MINIPSD_ERR_NOT_FOUND` |
| `minipsd_reader_next_layer(r, &layer)` | the layer records, bottom first, with views of their channel data: `MINIPSD_MORE` per layer, then `MINIPSD_OK` |
| `minipsd_layer_name(layer, utf8, cap, &needed)` | a layer's name as UTF-8 (`luni`, else the legacy name as Latin-1) |
@@ -178,7 +178,7 @@ its layers and composite as stored: no colour is converted.
**Defaults and field meanings**
- A zero-initialised document selects RLE (`MINIPSD_RLE = 0`).
- `opacity = 0` means a fully transparent layer; it is **not** "default opaque".
-- `rgba = NULL` is an empty layer; `name = NULL` is an empty name.
+- `pixels = NULL` is an empty layer; `name = NULL` is an empty name.
- Layers are listed bottom first.
**Names**
@@ -191,6 +191,14 @@ its layers and composite as stored: no colour is converted.
say what they mean to a program rather than repeat Adobe's words: `MINIPSD_SECTION_GROUP_END`
for Adobe's "bounding section divider".
- Functions keep minipsd's verbs (`next_`, `decode_`, `find_`); their nouns follow the fields'.
+- Types follow where a value comes from: what the file stores keeps the width it has there
+ (`uint8_t` flags such as `hidden`, `clipping`, `has_real_merged_data`; `uint16_t` depth and
+ colour mode in `minipsd_info` and `minipsd_doc`); what the caller asks or states is an `int`
+ (`keep_matte`, `scratch_kept`, `stored`, `composite_matted`, `minipsd_decode_opts.depth`).
+- Walks give the caller the cursor (`size_t *pos`, from 0), so a list can be walked again and
+ from several threads: resources, a layer's blocks, guides, alpha names and identifiers.
+ `minipsd_reader_next_layer` keeps its own, since each layer's data offset follows from the
+ layers before it.
- Exceptions, which the file does not store as such and so keep minipsd's names:
- `hidden`: bit 0x02 of the record's flags, which Adobe calls "visible" though it is set when
the layer is hidden;
@@ -214,7 +222,7 @@ its layers and composite as stored: no colour is converted.
| `MINIPSD_ERR_ALLOC` | -4 | `MINIPSD_MALLOC` returned NULL |
| `MINIPSD_ERR_OPEN` | -5 | not returned by minipsd; reserved for the caller's file code (`examples/write_file.c`) |
| `MINIPSD_ERR_FORMAT` | -6 | reader: not a well-formed PSD |
-| `MINIPSD_ERR_UNSUPPORTED` | -7 | reader: a valid file outside what minipsd reads: for decoding, a file of another depth than `opts->depth`, or a ZIP-compressed channel without `opts->inflate`; for `minipsd_read`, a file not 8- or 16-bit RGB; on a 32-bit platform, a PSB whose layer and mask section `size_t` cannot hold |
+| `MINIPSD_ERR_UNSUPPORTED` | -7 | reader: a valid file outside what minipsd reads: for decoding, a file of another depth than `opts->depth`, or a ZIP-compressed channel without `opts->inflate`; for `minipsd_read`, a file the writer could not take back (a mode at a depth it does not write, such as 32-bit CMYK, or layers in a flat mode); writer: a mode at a depth it does not write; on a 32-bit platform, a PSB whose layer and mask section `size_t` cannot hold |
| `MINIPSD_ERR_SHORT` | -8 | reader: the buffer ends inside data the call needs; read more, `minipsd_reader_needed` says how much |
| `MINIPSD_ERR_LIMIT` | -9 | reader: over `minipsd_limits`: the canvas's or a layer's side, the layer count, or the bytes of a decode call; the file is not known to be bad |
| `MINIPSD_ERR_NOT_FOUND` | -10 | a lookup found no such item: `minipsd_reader_find_resource` for an id the file lacks; not a fault of the file |
@@ -228,7 +236,7 @@ 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 (`rgba`) 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 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 |
| `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 |
@@ -274,7 +282,7 @@ interact, whatever the table says.
| `minipsd_writer_total` | yes | reads what `minipsd_writer_init` set; safe beside `minipsd_writer_fill` |
| `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` | no | advances the reader's walk |
+| `minipsd_reader_next_resource` | yes | each thread its own `pos`; only reads the reader |
| `minipsd_reader_next_layer` | no | advances the reader's walk |
| `minipsd_reader_find_resource` | yes | its own walk: only reads the reader; may run beside the walk |
| `minipsd_layer_name` | yes | reads the view; writes only the caller's buffer |
@@ -282,7 +290,7 @@ interact, whatever the table says.
| `minipsd_layer_blending_range` | yes | reads the view |
| `minipsd_resource_*`, `minipsd_layer_block_settings`, `minipsd_utf16be_to_utf8` | yes | read the view; write only the caller's struct, buffer or `pos` (each thread its own); `minipsd_resource_id_name` returns a constant string |
| `minipsd_target_rgba`, `minipsd_target_rgba16`, `minipsd_target_interleaved` | yes | fill only the caller's `minipsd_target` |
-| `minipsd_decode_layer` | yes, with the whole file | each thread its own scratch; may run beside the walk (`next_layer`, `next_resource`) |
+| `minipsd_decode_layer` | yes, with the whole file | each thread its own scratch; may run beside the walk (`next_layer`) |
| `minipsd_decode_mask` | yes, with the whole file | as `minipsd_decode_layer` |
| `minipsd_decode_composite` | yes, with the whole file | as `minipsd_decode_layer` |
| `minipsd_read_size` | yes | only reads the PSD |
@@ -312,12 +320,30 @@ several threads from one reader into one image.
## What goes into the file
-- PSD (version 1) or, with `format` `MINIPSD_PSB`, PSB (version 2), RGB, 4 channels (R, G, B,
- transparency), 8 bits per channel or, with `depth` 16, 16: every sample (`rgba`, mask
- `pixels`, `composite`) is then a `uint16_t` in host order, written big-endian. The layers go
- in the layer info at both depths, or, when `layer_tail` holds an `Lr16` (or `Lr32`) block, in
- that block, where it is, with the layer info empty, as Photoshop's 16-bit files have them; what
- the block held goes, the document's layers win.
+- PSD (version 1) or, with `format` `MINIPSD_PSB`, PSB (version 2), in the document's colour
+ mode (`color_mode`, `MINIPSD_MODE_*`, Adobe's numbers; 0 is RGB unless `depth` is 1, bitmap)
+ and depth, as Photoshop has them: RGB, grey, CMYK, Lab, duotone and multichannel at 8 and 16
+ 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
+ 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
+ (768 bytes), a duotone's inks and curves (required), at 32 bits whatever a file held (psd-tools'
+ 32-bit files hold an `hdrt` block, HDR toning); any other mode has none (`MINIPSD_ERR_ARG`).
+- **Every mode but RGB, and every 32-bit document, brings its composite** (`composite` or
+ `composite_stored`, `MINIPSD_ERR_ARG` otherwise): the writer composes 8- and 16-bit RGB only.
+- **The flat modes** (bitmap, indexed, multichannel) have no layers and no transparency, as
+ Photoshop's files: bitmap's composite is a byte a pixel (0 white, any other black), packed
+ eight pixels a byte by the writer, the first the high bit; indexed's one palette entry a
+ pixel; multichannel's only copied (`composite_stored`, `composite_channels` its channels),
+ since minipsd does not know what its channels are.
+- **Where the layers go:** in the layer info at 8 and 16 bits; at 32 in an `Lr32` block after an
+ empty layer info, an empty `Mt32` before it when the composite has transparency, as
+ psd-tools' 32-bit files have them (`8B64` blocks in a PSB); or, when `layer_tail` holds an
+ `Lr16` (or `Lr32`) block, in that block, where it is, with the layer info empty, as
+ 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`.
@@ -355,7 +381,7 @@ 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 `rgba`, or copied as it is from `stored` (any compression the
+ - 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);
- groups (`section_divider_type`), bottom first: a divider (`lsct` type 3), the group's layers, then the
@@ -375,17 +401,19 @@ several threads from one reader into one image.
- the caller's `composite` when set (the full canvas, straight alpha), 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`: 3 to 56, red, green, blue, then transparency when `composite_alpha`
+ `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) is the first extra channel,
+ - 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;
- - **colour is matted against white:** `c*a + max*(1-a)`, `max` being 255 or 65535. 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 is written byte for byte.
+ - **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
+ is written byte for byte. In RGB only: other modes are written as given, since no file shows
+ how they are matted.
- **Image resources:** the writer's own two first, each with the name of a kept one of the same
id, then the resources kept from a file (`resources`), in order, with their names:
- 1005 ResolutionInfo: dpi as 16.16 fixed point, unit inches; a kept 1005 that says the same
@@ -516,13 +544,15 @@ them; nothing is copied or allocated. Results on real files from other programs:
layers came from included. Global blocks are padded to four bytes past the length they state.
- **The colour mode data:** as one raw view (`minipsd_info.color_mode_data`): an indexed file's
palette, a duotone's inks and curves.
-- **As a document** (`minipsd_read`): 8- and 16-bit RGB files, at their depth; each layer's
+- **As a document** (`minipsd_read`): every file of a mode and depth the writer takes, at its
+ mode and depth, with its colour mode data; each layer's
rectangle, name, opacity, visibility, blend key, clipping, flags, section divider type, mask, blending ranges, decoded pixels
(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` 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 stays
- without, its image the composite, copied. Anything else is reported in `lost` (`MINIPSD_LOST_*`), never
+ 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
+ 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.
- **Round trip:** a file read with `minipsd_read` and written back keeps all its content: what
any program reads as different (pixels, channels, blocks, resources, settings). Layout the
@@ -591,7 +621,7 @@ does, and its memory images are minipsd's buffers, with no conversion:
| colour | sRGB, or the document's ICC profile | `interpretation` sRGB, the profile in `icc-profile-data` |
- **From libvips to minipsd:** `vips_image_write_to_memory` gives a buffer that is a layer's
- `rgba` as it is; a 3-band image takes `vips_bandjoin_const1(image, 255)` first.
+ `pixels` as it is; a 3-band image takes `vips_bandjoin_const1(image, 255)` first.
- **From minipsd to libvips:** decode with `minipsd_target_rgba`, then wrap the buffer with
`vips_image_new_from_memory(buffer, size, width, height, 4, VIPS_FORMAT_UCHAR)`, no copy.
- **Rendering layers:** `vips_composite` takes the layers with their positions (the layer
@@ -615,7 +645,9 @@ does, and its memory images are minipsd's buffers, with no conversion:
- **Memory:** libvips computes lazily, in strips; minipsd takes a whole layer, so a large
pipeline materialises each layer before writing. Reading in bands of rows
(`minipsd_decode_opts`) fits libvips' strips.
-- minipsd never depends on libvips.
+- minipsd never depends on libvips. `examples/vips_composite.c` does the whole round: layers out
+ 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.
## Consumer requirements
diff --git a/docs/USE-CASES.md b/docs/USE-CASES.md
index 8c43c1e..34685de 100644
--- a/docs/USE-CASES.md
+++ b/docs/USE-CASES.md
@@ -105,7 +105,7 @@ The writer turns a project structure into raw bytes, ready to store or send.
| **W3** | **Non-blocking stream:** event loop, async socket, HTTP with `Content-Length` | must never wait | the pull state machine (`fill` into the caller's buffer); total size for the header |
| **W4** | **No heap:** embedded, kernel-ish, strict real-time | fixed memory | workspace size query plus a caller workspace; works with a tiny output buffer |
| **W5** | **Cancel and progress:** long save in a GUI | responsiveness | from the pull model: done/total is progress, stopping the pulls is cancel. With push: return 0 from the callback |
-| **W6** | **Pixels not all in memory:** huge documents, tiles on disk, GPU readback, procedurally generated rows | memory bounded by rows, not layers | a **row provider** callback (layer, y) in place of `rgba` pointers. With RLE every row is asked for twice (sizing pass and data), so the provider must be deterministic. Alternatives: RAW (no sizing pass), or keep the compressed rows in caller-provided storage |
+| **W6** | **Pixels not all in memory:** huge documents, tiles on disk, GPU readback, procedurally generated rows | memory bounded by rows, not layers | a **row provider** callback (layer, y) in place of `pixels` pointers. With RLE every row is asked for twice (sizing pass and data), so the provider must be deterministic. Alternatives: RAW (no sizing pass), or keep the compressed rows in caller-provided storage |
| **W7** | **Caller supplies the composite:** the app already has its flattened render (more exact for blend modes it implements), or wants speed | CPU | an optional composite source (buffer or row provider); the library then skips blending |
| **W8** | **Layer-at-a-time producer:** a render pipeline makes layer 1, saves, frees it, makes layer 2 | one layer in memory | the records need all sizes first, so either all layer metadata and sizes up front (RAW makes sizes trivial), or each layer is compressed into caller storage first. The composite must be built incrementally or supplied (W7) |
| **W9** | **Parallel encoding:** big documents on many cores | speed | a split API: "encode channel *k* of layer *i* into a buffer" (thread-safe, independent), then "assemble the file from these pieces". The caller runs its own thread pool; the library starts no threads |
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 8511181..55414ce 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -11,6 +11,7 @@
# write_nonblocking the pull writer into an O_NONBLOCK pipe, with poll (POSIX only)
# read_file a PSD read whole, described, every layer and the composite decoded
# resave a PSD read as a document, one layer changed, written again (round trip)
+# vips_composite layers flattened by libvips, written with that composite (MINIPSD_VIPS_EXAMPLE)
# ---------------------------------------------------------------------------
set(_examples hello_write hello_read write_file write_memory read_file resave)
@@ -114,3 +115,65 @@ foreach(_psd IN LISTS _other_psds)
COMMAND resave "${_psd}" "${CMAKE_CURRENT_BINARY_DIR}/out-resave/${_name}.psd")
set_tests_properties(example.resave.${_name} PROPERTIES PASS_REGULAR_EXPRESSION "inverted")
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)
+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})
+ set_target_properties(vips_composite PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
+ if(MSVC)
+ target_compile_definitions(vips_composite PRIVATE _CRT_SECURE_NO_WARNINGS) # plain fopen
+ endif()
+
+ # The layers made by the example; then, locally, a Photoshop file's (tests/fixtures). Each run
+ # reads its file back and says whether the composite came back as rendered.
+ file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/out-vips")
+ add_test(NAME example.vips_composite COMMAND vips_composite "${CMAKE_CURRENT_BINARY_DIR}/out-vips/made.psd")
+ set(_vips_tests example.vips_composite)
+ if(_minipsd_fixtures AND EXISTS "${_minipsd_fixtures}/Test-g-example.psd")
+ add_test(NAME example.vips_composite.Test-g-example
+ COMMAND vips_composite "${CMAKE_CURRENT_BINARY_DIR}/out-vips/Test-g-example.psd" "${_minipsd_fixtures}/Test-g-example.psd")
+ list(APPEND _vips_tests example.vips_composite.Test-g-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}")
+ endif()
+endif()
diff --git a/examples/demo.h b/examples/demo.h
index adb3b2f..ede3de8 100644
--- a/examples/demo.h
+++ b/examples/demo.h
@@ -38,8 +38,8 @@ static void demo_init(demo *d)
p[2] = (uint8_t)(70 + (band == 2 ? state >> 16 : 0));
p[3] = (uint8_t)(band == 0 ? 255 : band == 1 ? 255 - x : state >> 8);
}
- d->layers[0].name = "Template"; d->layers[0].rgba = d->pixels; d->layers[0].opacity = 255; d->layers[0].hidden = 0;
- d->layers[1].name = "Layer"; d->layers[1].rgba = NULL; d->layers[1].opacity = 255; d->layers[1].hidden = 0;
+ d->layers[0].name = "Template"; d->layers[0].pixels = d->pixels; d->layers[0].opacity = 255; d->layers[0].hidden = 0;
+ d->layers[1].name = "Layer"; d->layers[1].pixels = NULL; d->layers[1].opacity = 255; d->layers[1].hidden = 0;
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;
diff --git a/examples/hello_write.c b/examples/hello_write.c
index a37a320..58b6e6f 100644
--- a/examples/hello_write.c
+++ b/examples/hello_write.c
@@ -20,7 +20,7 @@ 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", .rgba = rgba, .opacity = 255};
+ minipsd_layer layer = {.name = "Hello", .pixels = rgba, .opacity = 255};
minipsd_doc doc = {.width = 64, .height = 64, .dpi = 72.0, .layers = &layer, .layer_count = 1};
FILE *f;
int i, rc;
diff --git a/examples/read_file.c b/examples/read_file.c
index 18da782..82e6e36 100644
--- a/examples/read_file.c
+++ b/examples/read_file.c
@@ -141,7 +141,10 @@ int main(int argc, char **argv)
rgb = (info.depth == 8 || info.depth == 16) && info.color_mode == 3; /* what minipsd_read takes */
printf("resources:");
- while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE) printf(" %u (%lu)", (unsigned)res.id, (unsigned long)res.data.size);
+ for (pos = 0; minipsd_reader_next_resource(r, &pos, &res) == MINIPSD_MORE;) {
+ const char *res_name = minipsd_resource_id_name(res.id); /* Adobe's name; NULL for others */
+ printf(" %u %s (%lu)", (unsigned)res.id, res_name ? res_name : "?", (unsigned long)res.data.size);
+ }
printf("\n%lu layers, bottom first:\n", (unsigned long)info.layer_count);
while ((rc = minipsd_reader_next_layer(r, &layer)) == MINIPSD_MORE) {
diff --git a/examples/resave.c b/examples/resave.c
index 497d538..15d042d 100644
--- a/examples/resave.c
+++ b/examples/resave.c
@@ -110,7 +110,7 @@ int main(int argc, char **argv)
n = (size_t)(layers[index].right - layers[index].left) * (size_t)(layers[index].bottom - layers[index].top);
for (i = 0; i < n * 4; ++i)
if (i % 4 != 3) pixels[i] = (uint8_t)(255 - pixels[i]); /* invert the colour, keep the alpha */
- layers[index].rgba = pixels;
+ layers[index].pixels = pixels;
layers[index].stored = NULL; /* encode this one */
doc->layers = layers;
for (i = 0; i < doc->layer_count; ++i) copied += layers[i].stored != NULL;
diff --git a/examples/vips_composite.c b/examples/vips_composite.c
new file mode 100644
index 0000000..de4b073
--- /dev/null
+++ b/examples/vips_composite.c
@@ -0,0 +1,282 @@
+/**
+ * \file
+ * \brief Example: layers rendered into a composite by libvips, then written with it by minipsd.
+ *
+ * minipsd writes and reads PSD; it does not render. A program that holds layers as libvips
+ * images lets libvips flatten them (vips_composite) and hands that image to minipsd as the
+ * document's composite, which is what a viewer that ignores layers shows. The two meet in memory
+ * with no conversion: 8-bit RGBA, straight alpha, rows top down (the spec's "Working with libvips").
+ *
+ * Usage: vips_composite out.psd [in.psd]. Without in.psd, four layers made here (a gradient, a
+ * disc multiplied, a panel screened at half opacity, a hidden layer); with it, the layers of an
+ * 8-bit RGB file, read with minipsd_read. Either way the composite is rendered by libvips and the
+ * file written with it. Then, as a check, the file is read back and its composite decoded by
+ * minipsd: alpha must match the render exactly, colour too where alpha is full and within the
+ * un-matting bound elsewhere. Exits with 1 when anything fails.
+ *
+ * Rendered as libvips can: each layer at its rectangle, its opacity multiplied into alpha, hidden
+ * layers and group markers left out, the blend modes with a libvips counterpart mapped, the others
+ * drawn "over". Masks, clipping, group modes and effects are left to the caller.
+ *
+ * SPDX-License-Identifier: MIT
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha
+ */
+
+#define MINIPSD_IMPLEMENTATION
+#include "minipsd.h"
+
+#include
+
+#include
+#include
+#include
+
+#define RGBA 4 /* bands: 8-bit red, green, blue, alpha */
+#define FULL 255 /* an 8-bit sample's full value */
+
+/* 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;
+}
+
+static int to_file(void *file, const void *data, size_t size)
+{
+ return fwrite(data, 1, size, (FILE *)file) == size; /* 0 stops the writer */
+}
+
+/* A PSD blend key as a libvips blend mode (the spec's table); *mapped is 0 for a key libvips has
+ no counterpart for, which is then drawn "over". */
+static VipsBlendMode blend_mode(const char key[4], int *mapped)
+{
+ static const struct { char key[5]; 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;
+ *mapped = 1;
+ if (!key[0] && !key[1] && !key[2] && !key[3]) return VIPS_BLEND_MODE_OVER; /* zero: "norm" */
+ for (k = 0; k < sizeof table / sizeof table[0]; ++k)
+ if (memcmp(key, table[k].key, 4) == 0) return table[k].mode;
+ *mapped = 0;
+ return VIPS_BLEND_MODE_OVER;
+}
+
+/* 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);
+ 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);
+ if (!wrapped) return NULL;
+ if (vips_copy(wrapped, &srgb, "interpretation", VIPS_INTERPRETATION_sRGB, NULL)) { /* RGBA, not 4 bands of anything */
+ g_object_unref(wrapped);
+ return NULL;
+ }
+ g_object_unref(wrapped);
+ if (l->opacity == FULL) return srgb;
+ {
+ double scale[RGBA] = { 1, 1, 1, 0 }, offset[RGBA] = { 0, 0, 0, 0 };
+ scale[RGBA - 1] = l->opacity / (double)FULL;
+ if (vips_linear(srgb, &out, scale, offset, RGBA, "uchar", TRUE, NULL)) out = NULL;
+ g_object_unref(srgb);
+ return out;
+ }
+}
+
+/* The document's visible layers flattened by vips_composite onto a transparent canvas, as a new
+ width * height * 4 buffer (g_free it); NULL on failure. */
+static uint8_t *render(const minipsd_doc *d, size_t *unmapped)
+{
+ VipsImage **in = (VipsImage **)calloc(d->layer_count + 1, sizeof *in), *canvas, *out = NULL, *flat = NULL;
+ int *modes = (int *)calloc(d->layer_count + 1, sizeof *modes);
+ int *x = (int *)calloc(d->layer_count + 1, sizeof *x), *y = (int *)calloc(d->layer_count + 1, sizeof *y);
+ int mapped, n = 0, k;
+ uint8_t *pixels = NULL;
+ size_t i, size = 0;
+
+ *unmapped = 0;
+ if (!in || !modes || !x || !y) goto done;
+ /* The canvas, transparent, makes the composite the document's size; sRGB, as every layer. */
+ if (vips_black(&canvas, (int)d->width, (int)d->height, "bands", RGBA, NULL)) goto done;
+ k = vips_copy(canvas, &in[n], "interpretation", VIPS_INTERPRETATION_sRGB, NULL);
+ g_object_unref(canvas);
+ if (k) goto done;
+ ++n;
+ for (i = 0; i < d->layer_count; ++i) { /* bottom first, as minipsd and vips_composite both list them */
+ const minipsd_layer *l = &d->layers[i];
+ if (l->hidden || l->section_divider_type != MINIPSD_SECTION_LAYER || !l->pixels
+ || 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);
+ *unmapped += !mapped;
+ x[n - 1] = (int)l->left; /* where the layer goes; it may reach past the canvas */
+ y[n - 1] = (int)l->top;
+ ++n;
+ }
+ if (n == 1) { /* nothing visible: the transparent canvas */
+ out = in[0];
+ g_object_ref(out);
+ } else {
+ VipsArrayInt *xs = vips_array_int_new(x, n - 1), *ys = vips_array_int_new(y, n - 1);
+ k = vips_composite(in, &out, n, modes, "x", xs, "y", ys, NULL);
+ vips_area_unref(VIPS_AREA(xs));
+ vips_area_unref(VIPS_AREA(ys));
+ if (k) goto done;
+ }
+ if (vips_cast_uchar(out, &flat, NULL) == 0) { /* vips_composite may give a wider format */
+ pixels = (uint8_t *)vips_image_write_to_memory(flat, &size);
+ if (pixels && size != (size_t)d->width * d->height * RGBA) {
+ g_free(pixels);
+ pixels = NULL;
+ }
+ }
+done:
+ for (k = 0; in && k < n; ++k) g_object_unref(in[k]);
+ if (out) g_object_unref(out);
+ if (flat) g_object_unref(flat);
+ free(in);
+ free(modes);
+ free(x);
+ free(y);
+ return pixels;
+}
+
+/* The written file's composite, decoded by minipsd, against the render: alpha exactly; colour
+ exactly where alpha is full, elsewhere within the un-matting bound, max / (2 * alpha) levels
+ (the spec's API, "Un-matting"). Returns the samples outside it, or -1 when it cannot decode. */
+static long check(const uint8_t *file, size_t size, const uint8_t *render, uint32_t width, uint32_t height)
+{
+ uint8_t workspace[256];
+ minipsd_reader *r;
+ minipsd_info info;
+ minipsd_target t;
+ uint8_t *got;
+ size_t i, n = (size_t)width * height;
+ long off = 0;
+ if (minipsd_reader_init(&r, file, size, workspace, sizeof workspace, NULL, &info) != MINIPSD_OK) return -1;
+ got = (uint8_t *)malloc(n * RGBA);
+ if (!got) return -1;
+ minipsd_target_rgba(&t, got, (size_t)width * RGBA);
+ if (minipsd_decode_composite(r, &t, NULL) != MINIPSD_OK) { free(got); return -1; }
+ for (i = 0; i < n * RGBA; ++i) {
+ const int a = render[i - i % RGBA + RGBA - 1], diff = abs((int)got[i] - (int)render[i]);
+ if (i % RGBA == RGBA - 1 || a == FULL) off += diff != 0;
+ else if (a) off += diff > FULL / (2 * a) + 1; /* + 1: rounding on both sides */
+ }
+ free(got);
+ return off;
+}
+
+/* --- the layers made here, for a run without an input file --- */
+
+#define W 256
+#define H 192
+
+static uint8_t background[W * H * RGBA], disc[128 * 128 * RGBA], panel[120 * 80 * RGBA], hidden[W * H * RGBA];
+
+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;
+ 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;
+ 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;
+ }
+ for (x = 0; x < 120 * 80; ++x) { /* a blue panel */
+ panel[x * RGBA] = 40; panel[x * RGBA + 1] = 90; panel[x * RGBA + 2] = 220; panel[x * RGBA + 3] = FULL;
+ }
+ for (x = 0; x < W * H; ++x) hidden[x * RGBA] = hidden[x * RGBA + 3] = FULL; /* red, never shown */
+
+ memset(layers, 0, 4 * sizeof *layers); /* the structs grow at the end: zero is every default */
+ layers[0].name = "Background"; layers[0].pixels = background; layers[0].opacity = FULL;
+ layers[0].right = W; layers[0].bottom = H;
+ layers[1].name = "Disc"; layers[1].pixels = disc; layers[1].opacity = FULL;
+ memcpy(layers[1].blend_mode, "mul ", 4);
+ layers[1].left = -32; layers[1].top = 96; layers[1].right = 96; layers[1].bottom = 224; /* part off the canvas */
+ layers[2].name = "Panel"; layers[2].pixels = panel; layers[2].opacity = 128;
+ memcpy(layers[2].blend_mode, "scrn", 4);
+ layers[2].left = 100; layers[2].top = 40; layers[2].right = 220; layers[2].bottom = 120;
+ layers[3].name = "Hidden"; layers[3].pixels = hidden; layers[3].opacity = FULL; layers[3].hidden = 1;
+ layers[3].right = W; layers[3].bottom = H;
+}
+
+int main(int argc, char **argv)
+{
+ minipsd_layer layers[4];
+ minipsd_doc made, *doc = &made, *read = NULL;
+ uint8_t *input = NULL, *flat = NULL, *written = NULL;
+ size_t size = 0, unmapped = 0;
+ uint32_t lost = 0;
+ long off = -1;
+ FILE *f;
+ int rc = MINIPSD_OK, failed = 1;
+
+ if (argc < 2) { fputs("usage: vips_composite out.psd [in.psd]\n", stderr); return 2; }
+ if (VIPS_INIT(argv[0])) { fputs("vips_composite: libvips did not start\n", stderr); return 1; }
+
+ if (argc > 2) { /* the layers of a file */
+ input = read_all(argv[2], &size);
+ rc = input ? minipsd_read(input, size, NULL, NULL, &read, &lost) : MINIPSD_ERR_OPEN;
+ free(input);
+ if (rc != MINIPSD_OK) { fprintf(stderr, "vips_composite: %s: %s\n", argv[2], minipsd_error_string(rc)); goto done; }
+ if (read->depth == 16) { fputs("vips_composite: an 8-bit file, please\n", stderr); goto done; }
+ if (lost) printf("warning: the file holds content minipsd cannot carry yet (lost %lu)\n", (unsigned long)lost);
+ doc = read;
+ } else {
+ make_layers(layers);
+ memset(&made, 0, sizeof made);
+ made.width = W; made.height = H; made.dpi = 72.0; made.layers = layers; made.layer_count = 4;
+ }
+
+ flat = render(doc, &unmapped); /* libvips flattens the layers ... */
+ 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 */
+
+ f = fopen(argv[1], "wb");
+ if (!f) rc = MINIPSD_ERR_OPEN;
+ else {
+ rc = minipsd_write(doc, NULL, to_file, f);
+ if (fclose(f) != 0 && rc == MINIPSD_OK) rc = MINIPSD_ERR_OPEN;
+ }
+ if (rc != MINIPSD_OK) { fprintf(stderr, "vips_composite: %s: %s\n", argv[1], minipsd_error_string(rc)); goto done; }
+
+ written = read_all(argv[1], &size);
+ off = written ? check(written, size, flat, doc->width, doc->height) : -1;
+ printf("%lu layers, %lu blend modes drawn as normal; composite read back: %s (%ld samples off)\n",
+ (unsigned long)doc->layer_count, (unsigned long)unmapped, off == 0 ? "as rendered" : "DIFFERENT", off);
+ fflush(stdout); /* now: with libvips' prebuilt Windows build, exit does not always flush it */
+ failed = off != 0;
+done:
+ free(written);
+ g_free(flat);
+ if (read) minipsd_doc_free(read);
+ vips_shutdown();
+ return failed;
+}
diff --git a/include/minipsd.h b/include/minipsd.h
index ca2b8b7..1dfd6f3 100644
--- a/include/minipsd.h
+++ b/include/minipsd.h
@@ -11,6 +11,7 @@
* - Document
* - Writer
* - Reader
+ * - Typed views
* - Document from a file
*
* \par Usage
@@ -23,7 +24,7 @@
*
* \code
* minipsd_layer layers[2] = {
- * { .name = "Template", .rgba = rgba, .opacity = 255 }, // bottom, the full canvas
+ * { .name = "Template", .pixels = rgba, .opacity = 255 }, // bottom, the full canvas
* { .name = "Layer", .opacity = 255 }, // empty, above it
* };
* minipsd_doc doc = { .width = width, .height = height, .dpi = 300.0, .layers = layers, .layer_count = 2 };
@@ -45,8 +46,9 @@
* nothing.
*
* \par What it writes
- * An RGB document, as a PSD or (minipsd_doc.format) a PSB, 8 or 16 bits a channel, with a
- * transparency channel:
+ * 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,
+ * 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;
* names go out both as a legacy Pascal string (ASCII) and as Unicode ('luni'), followed by any
@@ -61,10 +63,12 @@
* - 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.
*
- * Channel data is PackBits RLE by default (small for flat art), or raw.
+ * Channel data is PackBits RLE by default (small for flat art), raw, or ZIP through the caller's
+ * deflate.
*
* Groups (minipsd_layer.section_divider_type) and layer masks (minipsd_layer.mask) are written;
- * text and effects only as blocks kept from a file. Not written: 32-bit, other colour modes.
+ * text and effects only as blocks kept from a file. The writer composes 8- and 16-bit RGB only:
+ * other modes and 32 bits bring their composite.
* PSD caps each side at 30000 px, PSB at 300000; larger documents are refused (MINIPSD_ERR_SIZE).
*
* \par Output
@@ -96,12 +100,12 @@
* The single source of the project version: CMakeLists.txt reads these three lines.
*/
#define MINIPSD_VERSION_MAJOR 0
-#define MINIPSD_VERSION_MINOR 2
+#define MINIPSD_VERSION_MINOR 3
#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.2.0". */
+/** \brief The version as a string literal, e.g. "0.3.0". */
#define MINIPSD_VERSION_STRING \
MPSD__XSTR(MINIPSD_VERSION_MAJOR) "." MPSD__XSTR(MINIPSD_VERSION_MINOR) "." MPSD__XSTR(MINIPSD_VERSION_PATCH)
@@ -126,8 +130,8 @@
enum {
MINIPSD_OK = 0, /*!< Success. */
MINIPSD_MORE = 1, /*!< Not an error: the work is not finished, call again. */
- MINIPSD_ERR_ARG = -1, /*!< A NULL document, no layers nor composite, or a dpi
- outside (0, 65536). */
+ MINIPSD_ERR_ARG = -1, /*!< An invalid argument: a NULL pointer, a value out of its
+ range; each function lists its cases. */
MINIPSD_ERR_SIZE = -2, /*!< A side is 0 or above the format's (MINIPSD_MAX_SIDE, or
MINIPSD_MAX_SIDE_PSB), or the document is too large for
PSD's 32-bit section lengths (about 4 GiB). */
@@ -135,15 +139,16 @@ enum {
MINIPSD_ERR_ALLOC = -4, /*!< MINIPSD_MALLOC returned NULL. */
MINIPSD_ERR_OPEN = -5, /*!< Not returned by minipsd; kept for the caller's own file
code (examples/): a file not opened or closed. */
- MINIPSD_ERR_FORMAT = -6, /*!< Reader: the bytes are not a well-formed PSD. */
+ MINIPSD_ERR_FORMAT = -6, /*!< Reader and typed views: the bytes are not well-formed. */
MINIPSD_ERR_UNSUPPORTED = -7, /*!< Valid, but outside what minipsd handles yet: reading,
samples of another depth than the file's, ZIP-compressed
- without the caller's inflate, or a document of a mode
- other than 8- or 16-bit RGB;
- writing in the other format, kept content that cannot
- change format yet. */
+ without the caller's inflate, or a document the writer
+ could not take back;
+ writing a mode at a depth the writer does not take, in
+ the other format kept content that cannot change format
+ yet. */
MINIPSD_ERR_SHORT = -8, /*!< Reader: the buffer ends inside data the call needs;
- the file may be fine. minipsd_reader_needed says
+ the file may be fine. minipsd_reader_needed says
how much more. */
MINIPSD_ERR_LIMIT = -9, /*!< Reader: a value is over a limit (minipsd_limits). */
MINIPSD_ERR_NOT_FOUND = -10 /*!< A lookup found no such item (a resource of that id);
@@ -188,6 +193,24 @@ enum {
counts of four; sides up to MINIPSD_MAX_SIDE_PSB. */
};
+/**
+ * \brief Colour modes, numbered as the file's header numbers them (minipsd_info.color_mode).
+ *
+ * minipsd converts no colour: samples are what the mode stores, and the ICC profile or the colour
+ * mode data says what they mean. In a document, color_mode 0 is RGB, the default, unless depth is
+ * 1: bitmap is the only 1-bit mode, so 0 then means bitmap.
+ */
+enum {
+ MINIPSD_MODE_BITMAP = 0, /*!< 1 bit: black and white; composite only, no layers. */
+ MINIPSD_MODE_GREY = 1, /*!< One channel. */
+ MINIPSD_MODE_INDEXED = 2, /*!< One channel of palette entries (color_mode_data); no layers. */
+ MINIPSD_MODE_RGB = 3, /*!< Red, green, blue. */
+ MINIPSD_MODE_CMYK = 4, /*!< Cyan, magenta, yellow, black, stored inverted (255: no ink). */
+ MINIPSD_MODE_MULTICHANNEL = 7, /*!< N channels of no stated meaning; no layers. */
+ MINIPSD_MODE_DUOTONE = 8, /*!< One channel; the inks and curves in color_mode_data. */
+ MINIPSD_MODE_LAB = 9 /*!< L, a, b. */
+};
+
/**
* \brief Channel compression. Zero is RLE, so a zero-initialised document gets the compact one.
*
@@ -250,85 +273,6 @@ enum {
MINIPSD_RES_XMP_METADATA = 1060 /*!< XMP metadata, UTF-8 XML: bytes. */
};
-/**
- * \brief Resource 1005, ResolutionInfo, as stored.
- *
- * Both resolutions are in pixels per inch whatever their unit: the unit is only how Photoshop
- * shows them. libvips wants pixels per millimetre (divide by 25.4) and a unit name.
- */
-typedef struct minipsd_resolution_info {
- double horizontal_resolution; /*!< hRes: pixels per inch. */
- uint16_t horizontal_resolution_unit; /*!< hResUnit: 1 pixels per inch, 2 pixels per cm. */
- uint16_t width_unit; /*!< widthUnit: 1 inches, 2 cm, 3 points, 4 picas, 5 columns. */
- double vertical_resolution; /*!< vRes: pixels per inch. */
- uint16_t vertical_resolution_unit; /*!< vResUnit, as hResUnit. */
- uint16_t height_unit; /*!< heightUnit, as widthUnit. */
-} minipsd_resolution_info;
-
-/**
- * \brief Resource 1057, version info, as stored.
- *
- * A file without it (older than Photoshop 6) has a real composite.
- */
-typedef struct minipsd_version_info {
- uint32_t version; /*!< The resource's own version, as stored. */
- uint8_t has_real_merged_data; /*!< Zero: the composite is not the layers' image (saved
- without "Maximize Compatibility"); ImageMagick then
- flattens the layers instead. */
- minipsd_bytes writer_name; /*!< UTF-16BE, without its length ("Adobe Photoshop");
- minipsd_utf16be_to_utf8 converts. */
- minipsd_bytes reader_name; /*!< UTF-16BE, as writer_name. */
- uint32_t file_version; /*!< The file version, as stored. */
-} minipsd_version_info;
-
-/** \brief Resource 1032, grid and guides information: its header; minipsd_resource_next_guide walks the guides. */
-typedef struct minipsd_grid_and_guides {
- uint32_t version; /*!< As stored. */
- uint32_t horizontal_grid; /*!< Adobe's "future implementation of document-specific grids", as */
- uint32_t vertical_grid; /*!< stored, horizontal then vertical. */
- uint32_t guide_count; /*!< How many guides follow. */
-} minipsd_grid_and_guides;
-
-/** \brief One guide of resource 1032. */
-typedef struct minipsd_guide {
- int32_t location; /*!< Where it lies, in document coordinates (pixels) times 32; may lie past
- the canvas. */
- uint8_t direction; /*!< 0: vertical (a column), 1: horizontal (a row). */
-} minipsd_guide;
-
-/** \brief Which of a layer's blocks minipsd_layer_block_settings found: bits of its present field. */
-enum {
- MINIPSD_PRESENT_LAYER_ID = 0x01, /*!< 'lyid'. */
- MINIPSD_PRESENT_SHEET_COLOR = 0x02, /*!< 'lclr'. */
- MINIPSD_PRESENT_FILL_OPACITY = 0x04, /*!< 'iOpa'. */
- MINIPSD_PRESENT_PROTECTION_FLAGS = 0x08, /*!< 'lspf'. */
- MINIPSD_PRESENT_KNOCKOUT = 0x10, /*!< 'knko'. */
- MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS = 0x20, /*!< 'clbl'. */
- MINIPSD_PRESENT_BLEND_INTERIOR_ELEMENTS = 0x40 /*!< 'infx'. */
-};
-
-/**
- * \brief What a layer's fixed-layout blocks say, as stored; the first block of each key counts.
- *
- * A value whose block is missing is 0, its bit in present clear; fill_opacity is then 255, the
- * fill a layer without it has.
- */
-typedef struct minipsd_block_settings {
- uint32_t layer_id; /*!< 'lyid': the layer's id. */
- uint16_t sheet_color; /*!< 'lclr': the colour label in the layers panel, 0 none, 1 red,
- 2 orange, 3 yellow, 4 green, 5 blue, 6 violet, 7 grey, and
- newer ones above (psd-tools' list). */
- uint8_t fill_opacity; /*!< 'iOpa': the fill opacity, 255 opaque; unlike opacity, it
- leaves the layer's effects alone. */
- uint32_t protection_flags; /*!< 'lspf': 0x01 transparency, 0x02 composite (pixels), 0x04
- position locked; 0x08 nesting and 0x80000000 all, by
- psd-tools. */
- uint8_t knockout; /*!< 'knko': 0 none, 1 shallow, 2 deep (psd-tools'). */
- uint8_t blend_clipped_elements; /*!< 'clbl': non-zero blends clipped layers as a group. */
- uint8_t blend_interior_elements; /*!< 'infx': non-zero blends interior effects as a group. */
- uint8_t present; /*!< MINIPSD_PRESENT_*: the blocks the layer has. */
-} minipsd_block_settings;
-
/** \brief One tagged block of a layer's additional information ('luni', 'lsct', 'tySh', ...). */
typedef struct minipsd_block {
char key[5]; /*!< The four-character key, then a NUL. */
@@ -371,7 +315,8 @@ enum {
};
/**
- * \brief A layer mask: one 8-bit channel over its own rectangle, 255 shows the layer, 0 hides it.
+ * \brief A layer mask: one channel over its own rectangle, full (255 at 8 bits) shows the layer,
+ * 0 hides it.
*
* Pointers are borrowed, like the layer's. Fields are only ever added at the end.
*/
@@ -403,13 +348,15 @@ typedef struct minipsd_mask {
*/
typedef struct minipsd_layer { /* NOLINT(clang-analyzer-optin.performance.Padding): fields are only added at the end */
const char *name; /*!< UTF-8; NULL means "". */
- const uint8_t *rgba; /*!< (right - left) * (bottom - top) * 4 samples, rows top down, straight
- (not premultiplied) alpha; NULL means an empty layer. A sample is
- a byte, or with depth 16 a uint16_t in host order (two bytes). */
+ 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). */
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
- minipsd_layer_view; all four zero, with rgba and no stored: the
+ minipsd_layer_view; all four zero, with pixels and no stored: the
full canvas. It may reach past the canvas, or lie wholly
outside it. An empty layer's has no area. */
char blend_mode[4]; /*!< The blend mode's four-character key, such as "mul "; all zero:
@@ -423,12 +370,13 @@ typedef struct minipsd_layer { /* NOLINT(clang-analyzer-optin.performance.Paddi
effects, groups, ...), as minipsd_layer_view.additional_info
holds them: written after the layer's own name, except a
'luni' block, since name wins. */
- const minipsd_bytes *stored; /*!< NULL: rgba is encoded. Otherwise four channels as a file
- stores them (R, G, B, transparency, as minipsd_layer_view.channel:
- the compression marker, then the rows), copied as they are
- instead of encoding rgba: a layer never decoded is never encoded
+ const minipsd_bytes *stored; /*!< NULL: pixels are encoded. Otherwise the mode's colour channels
+ then transparency as a file stores them (as
+ minipsd_layer_view.channel: the compression marker, then the
+ rows), copied as they are
+ instead of encoding pixels: a layer never decoded is never encoded
again. Each is checked against the rectangle; transparency of
- size 0 is none, and written as none. rgba may then be NULL, as long
+ size 0 is none, and written as none. pixels may then be NULL, as long
as the document supplies its composite. */
uint8_t section_divider_type; /*!< MINIPSD_SECTION_*: a layer, or a group's top record or
divider. Groups nest bottom first: a divider, the group's
@@ -466,11 +414,19 @@ typedef struct minipsd_doc {
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. */
- const uint8_t *composite; /*!< The composite (merged) image: width * height * 4
- samples, rows top down, straight alpha, written
+ 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,
+ straight alpha, written
whatever the layers hold. NULL: composite_stored
is copied; without it too, composed from the
- layers, a best effort (see "What it writes"). */
+ layers, a best effort (see "What it writes"),
+ in RGB only: every other mode needs one. Other
+ modes are written as given, never matted. The
+ flat modes have no transparency: indexed one
+ palette entry a pixel, bitmap a byte a pixel (0
+ white, any other black; the writer packs them);
+ multichannel takes composite_stored only. */
int composite_matted; /*!< Non-zero: composite is already matted against
white, as the file stores it (what
minipsd_decode_composite gives with keep_matte),
@@ -489,9 +445,10 @@ typedef struct minipsd_doc {
as Photoshop's 16-bit files have them.
Empty: no global mask, no blocks (an empty global
mask is written). */
- uint16_t depth; /*!< Bits per channel: 0 or 8, or 16, where every sample
- (rgba, mask pixels, composite) is a uint16_t in
- host order, 0 .. 65535. */
+ uint16_t depth; /*!< Bits per channel: 0 or 8; 16, where every sample
+ (pixels, mask pixels, composite) is a uint16_t in
+ host order, 0 .. 65535; 32, a float in host
+ order, 0.0 .. 1.0; 1 for bitmap. */
uint16_t format; /*!< MINIPSD_PSD (or 0) or MINIPSD_PSB: what the
document is. Everything kept from a file
(stored channels, masks' stored and real, additional_info,
@@ -505,8 +462,9 @@ typedef struct minipsd_doc {
count, channel by channel, then the rows, laid
out as format lays it out. minipsd_read sets it
with minipsd_read_opts.stored. Size 0: none. */
- uint16_t composite_channels; /*!< The channels composite_stored holds, 3 to
- 56: red, green, blue, then transparency
+ uint16_t composite_channels; /*!< The channels composite_stored holds, up to
+ 56: the mode's colour channels (each of them in
+ multichannel), then transparency
(composite_alpha) and alpha or spot channels, in
the file's order. Ignored without
composite_stored. */
@@ -517,6 +475,15 @@ typedef struct minipsd_doc {
composite_stored: a supplied or composed
composite always has transparency. 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. */
+ minipsd_bytes color_mode_data; /*!< The colour mode data, written as given: an
+ indexed document's palette (768 bytes: 256 red,
+ 256 green, 256 blue), a duotone's inks
+ and curves (minipsd_info.color_mode_data), at
+ 32 bits whatever the file held (an 'hdrt'
+ block: HDR toning); size 0 otherwise. */
} minipsd_doc;
/**
@@ -731,7 +698,7 @@ typedef struct minipsd_info {
uint32_t width, height; /*!< In pixels. */
uint16_t channels; /*!< Channels of the composite, as stored. */
uint16_t depth; /*!< Bits per channel: 1, 8, 16 or 32. */
- uint16_t color_mode; /*!< As stored: 0 bitmap, 1 grey, 2 indexed, 3 RGB, 4 CMYK,
+ uint16_t color_mode; /*!< MINIPSD_MODE_*, as stored: 0 bitmap, 1 grey, 2 indexed, 3 RGB, 4 CMYK,
7 multichannel, 8 duotone, 9 Lab. */
double dpi; /*!< Horizontal pixels per inch (resource 1005); 0 without it. */
minipsd_bytes icc; /*!< The ICC profile (resource 1039); size 0 when none. */
@@ -887,12 +854,30 @@ MINIPSDDEF uint64_t minipsd_reader_needed(const minipsd_reader *r);
/**
* \brief The next image resource, in file order.
*
+ * The cursor is the caller's, as for a layer's blocks: walk the resources as often as needed, from
+ * any number of threads, each with its own pos; the reader is only read.
+ *
* \param r A reader from minipsd_reader_init.
+ * \param pos The caller's cursor: start at 0; moved past the resource returned.
* \param res Receives the resource.
* \return MINIPSD_MORE when a resource was written to res; MINIPSD_OK when there are no more
- * (res is untouched); or MINIPSD_ERR_ARG when r (never initialised) or res is NULL.
+ * (res is untouched); MINIPSD_ERR_FORMAT for a pos this function did not give; or
+ * MINIPSD_ERR_ARG when r (never initialised), pos or res is NULL.
+ */
+MINIPSDDEF int minipsd_reader_next_resource(const minipsd_reader *r, size_t *pos, minipsd_resource *res);
+
+/**
+ * \brief The first image resource with an id, wherever it is.
+ *
+ * It only reads the reader, so it may run beside any other call (see the spec's Threads).
+ *
+ * \param r A reader from minipsd_reader_init.
+ * \param id The resource's number, such as MINIPSD_RES_VERSION_INFO.
+ * \param res Receives the resource.
+ * \return MINIPSD_OK when found; MINIPSD_ERR_NOT_FOUND when the file has none (res is untouched);
+ * MINIPSD_ERR_ARG when r (never initialised) or res is NULL.
*/
-MINIPSDDEF int minipsd_reader_next_resource(minipsd_reader *r, minipsd_resource *res);
+MINIPSDDEF int minipsd_reader_find_resource(const minipsd_reader *r, uint16_t id, minipsd_resource *res);
/**
* \brief The next layer record, bottom layer first.
@@ -952,137 +937,6 @@ MINIPSDDEF int minipsd_layer_next_block(const minipsd_layer_view *layer, size_t
*/
MINIPSDDEF int minipsd_layer_blending_range(const minipsd_layer_view *layer, uint32_t i, minipsd_blending_range *range);
-/**
- * \brief The first image resource with an id, wherever it is.
- *
- * Its own walk from the first resource: next_resource's place is untouched, and it only reads the
- * reader, so it may run beside any other call (see the spec's Threads).
- *
- * \param r A reader from minipsd_reader_init.
- * \param id The resource's number, such as MINIPSD_RES_VERSION_INFO.
- * \param res Receives the resource.
- * \return MINIPSD_OK when found; MINIPSD_ERR_NOT_FOUND when the file has none (res is untouched);
- * MINIPSD_ERR_ARG when r (never initialised) or res is NULL.
- */
-MINIPSDDEF int minipsd_reader_find_resource(const minipsd_reader *r, uint16_t id, minipsd_resource *res);
-
-/* --- Typed views: what a kept resource or block says, parsed; they need no reader ------------ */
-
-/**
- * \brief UTF-16BE text as UTF-8, as minipsd_layer_name gives a name.
- *
- * An unpaired surrogate becomes U+FFFD, a U+0000 ends the text, and an odd last byte is dropped.
- *
- * \param utf16 The text, such as minipsd_version_info.writer_name.
- * \param utf8 Receives the text and a terminating NUL; may be NULL when cap is 0.
- * \param cap The size of utf8 in bytes.
- * \param needed Receives the bytes the text takes with its NUL; may be NULL.
- * \return MINIPSD_OK when it fits; MINIPSD_MORE when cap is too small (utf8 is then empty);
- * or MINIPSD_ERR_ARG for a NULL utf16, a view with a size and no data, or utf8 NULL with
- * cap above 0.
- */
-MINIPSDDEF int minipsd_utf16be_to_utf8(const minipsd_bytes *utf16, char *utf8, size_t cap, size_t *needed);
-
-/**
- * \brief Resource 1005, ResolutionInfo.
- *
- * \param res A resource from the reader or a document, its id MINIPSD_RES_RESOLUTION_INFO.
- * \param out Receives the values.
- * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data is shorter than its 16 bytes;
- * MINIPSD_ERR_ARG for a NULL argument or another id.
- */
-MINIPSDDEF int minipsd_resource_resolution_info(const minipsd_resource *res, minipsd_resolution_info *out);
-
-/**
- * \brief Resource 1057, version info.
- *
- * \param res A resource from the reader or a document, its id MINIPSD_RES_VERSION_INFO.
- * \param out Receives the values; the names point into res's data.
- * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data ends inside a field or a name;
- * MINIPSD_ERR_ARG for a NULL argument or another id.
- */
-MINIPSDDEF int minipsd_resource_version_info(const minipsd_resource *res, minipsd_version_info *out);
-
-/**
- * \brief Resource 1032, grid and guides information: the header.
- *
- * \param res A resource, its id MINIPSD_RES_GRID_AND_GUIDES.
- * \param out Receives the header.
- * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data ends inside the header or the guides it
- * counts; MINIPSD_ERR_ARG for a NULL argument or another id.
- */
-MINIPSDDEF int minipsd_resource_grid_and_guides(const minipsd_resource *res, minipsd_grid_and_guides *out);
-
-/**
- * \brief The next guide of resource 1032, in stored order.
- *
- * \param res A resource, its id MINIPSD_RES_GRID_AND_GUIDES.
- * \param pos The caller's cursor: start at 0; moved past the guide returned.
- * \param guide Receives the guide.
- * \return MINIPSD_MORE when a guide was written; MINIPSD_OK at the end; MINIPSD_ERR_FORMAT as
- * minipsd_resource_grid_and_guides; MINIPSD_ERR_ARG for a NULL argument, another id, or a
- * pos this function did not give.
- */
-MINIPSDDEF int minipsd_resource_next_guide(const minipsd_resource *res, size_t *pos, minipsd_guide *guide);
-
-/**
- * \brief The next alpha channel's name: resource 1045 (Unicode) or 1006 (Pascal strings).
- *
- * Photoshop writes both; find 1045 first, 1006 for files without it.
- *
- * \param res A resource, its id MINIPSD_RES_UNICODE_ALPHA_NAMES or MINIPSD_RES_ALPHA_NAMES.
- * \param pos The caller's cursor: start at 0; moved past the name returned.
- * \param name Receives the name: UTF-16BE for 1045 (minipsd_utf16be_to_utf8 converts), raw bytes
- * in the writer's code page for 1006.
- * \return MINIPSD_MORE when a name was written; MINIPSD_OK at the end; MINIPSD_ERR_FORMAT when a
- * name runs past the data; MINIPSD_ERR_ARG for a NULL argument or another id.
- */
-MINIPSDDEF int minipsd_resource_next_alpha_name(const minipsd_resource *res, size_t *pos, minipsd_bytes *name);
-
-/**
- * \brief The next alpha channel's identifier, resource 1053.
- *
- * \param res A resource, its id MINIPSD_RES_ALPHA_IDENTIFIERS.
- * \param pos The caller's cursor: start at 0; moved past the identifier returned.
- * \param id Receives the identifier.
- * \return MINIPSD_MORE when one was written; MINIPSD_OK at the end; MINIPSD_ERR_FORMAT when the
- * data is not whole identifiers (four bytes each); MINIPSD_ERR_ARG for a NULL argument or
- * another id.
- */
-MINIPSDDEF int minipsd_resource_next_alpha_identifier(const minipsd_resource *res, size_t *pos, uint32_t *id);
-
-/**
- * \brief Resource 1047: which palette entry of an indexed file is transparent.
- *
- * \param res A resource, its id MINIPSD_RES_TRANSPARENCY_INDEX.
- * \param index Receives the entry, 0 to 255.
- * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data is shorter than two bytes; MINIPSD_ERR_ARG
- * for a NULL argument or another id.
- */
-MINIPSDDEF int minipsd_resource_transparency_index(const minipsd_resource *res, uint16_t *index);
-
-/**
- * \brief Adobe's name for an image resource id, for listing what a file holds.
- *
- * Every id of Adobe's "Image Resource IDs" table has one, its ranges included (2000 to 2997 path
- * information, 4000 to 4999 plug-in resources). The text is a constant, in English.
- *
- * \param id A resource's number, such as minipsd_resource.id.
- * \return The name, such as "Resolution info"; NULL for an id Adobe does not list.
- */
-MINIPSDDEF const char *minipsd_resource_id_name(uint16_t id);
-
-/**
- * \brief What a layer's fixed-layout blocks say: id, colour label, fill, locks, knockout, blending
- * of clipped and interior elements.
- *
- * \param layer A record from minipsd_reader_next_layer, or a view built by hand.
- * \param out Receives the values.
- * \return MINIPSD_OK; MINIPSD_ERR_FORMAT for a malformed run of blocks or one of these shorter than
- * its value; MINIPSD_ERR_ARG for a NULL argument.
- */
-MINIPSDDEF int minipsd_layer_block_settings(const minipsd_layer_view *layer, minipsd_block_settings *out);
-
/**
* \brief Where one channel's samples go: the first sample, and the steps to the next one.
*
@@ -1275,6 +1129,202 @@ MINIPSDDEF int minipsd_decode_composite(minipsd_reader *r, const minipsd_target
MINIPSDDEF int minipsd_decode_mask(minipsd_reader *r, const minipsd_layer_view *layer, const minipsd_plane *plane,
const minipsd_decode_opts *opts);
+/* --- Typed views: what a kept resource or block says, parsed; they need no reader ------------ */
+
+/**
+ * \brief Resource 1005, ResolutionInfo, as stored.
+ *
+ * Both resolutions are in pixels per inch whatever their unit: the unit is only how Photoshop
+ * shows them. libvips wants pixels per millimetre (divide by 25.4) and a unit name.
+ */
+typedef struct minipsd_resolution_info {
+ double horizontal_resolution; /*!< hRes: pixels per inch. */
+ uint16_t horizontal_resolution_unit; /*!< hResUnit: 1 pixels per inch, 2 pixels per cm. */
+ uint16_t width_unit; /*!< widthUnit: 1 inches, 2 cm, 3 points, 4 picas, 5 columns. */
+ double vertical_resolution; /*!< vRes: pixels per inch. */
+ uint16_t vertical_resolution_unit; /*!< vResUnit, as hResUnit. */
+ uint16_t height_unit; /*!< heightUnit, as widthUnit. */
+} minipsd_resolution_info;
+
+/**
+ * \brief Resource 1057, version info, as stored.
+ *
+ * A file without it (older than Photoshop 6) has a real composite.
+ */
+typedef struct minipsd_version_info {
+ uint32_t version; /*!< The resource's own version, as stored. */
+ uint8_t has_real_merged_data; /*!< Zero: the composite is not the layers' image (saved
+ without "Maximize Compatibility"); ImageMagick then
+ flattens the layers instead. */
+ minipsd_bytes writer_name; /*!< UTF-16BE, without its length ("Adobe Photoshop");
+ minipsd_utf16be_to_utf8 converts. */
+ minipsd_bytes reader_name; /*!< UTF-16BE, as writer_name. */
+ uint32_t file_version; /*!< The file version, as stored. */
+} minipsd_version_info;
+
+/** \brief Resource 1032, grid and guides information: its header; minipsd_resource_next_guide walks the guides. */
+typedef struct minipsd_grid_and_guides {
+ uint32_t version; /*!< As stored. */
+ uint32_t horizontal_grid; /*!< Adobe's "future implementation of document-specific grids", as */
+ uint32_t vertical_grid; /*!< stored, horizontal then vertical. */
+ uint32_t guide_count; /*!< How many guides follow. */
+} minipsd_grid_and_guides;
+
+/** \brief One guide of resource 1032. */
+typedef struct minipsd_guide {
+ int32_t location; /*!< Where it lies, in document coordinates (pixels) times 32; may lie past
+ the canvas. */
+ uint8_t direction; /*!< 0: vertical (a column), 1: horizontal (a row). */
+} minipsd_guide;
+
+/** \brief Which of a layer's blocks minipsd_layer_block_settings found: bits of its present field. */
+enum {
+ MINIPSD_PRESENT_LAYER_ID = 0x01, /*!< 'lyid'. */
+ MINIPSD_PRESENT_SHEET_COLOR = 0x02, /*!< 'lclr'. */
+ MINIPSD_PRESENT_FILL_OPACITY = 0x04, /*!< 'iOpa'. */
+ MINIPSD_PRESENT_PROTECTION_FLAGS = 0x08, /*!< 'lspf'. */
+ MINIPSD_PRESENT_KNOCKOUT = 0x10, /*!< 'knko'. */
+ MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS = 0x20, /*!< 'clbl'. */
+ MINIPSD_PRESENT_BLEND_INTERIOR_ELEMENTS = 0x40 /*!< 'infx'. */
+};
+
+/**
+ * \brief What a layer's fixed-layout blocks say, as stored; the first block of each key counts.
+ *
+ * A value whose block is missing is 0, its bit in present clear; fill_opacity is then 255, the
+ * fill a layer without it has.
+ */
+typedef struct minipsd_block_settings {
+ uint32_t layer_id; /*!< 'lyid': the layer's id. */
+ uint16_t sheet_color; /*!< 'lclr': the colour label in the layers panel, 0 none, 1 red,
+ 2 orange, 3 yellow, 4 green, 5 blue, 6 violet, 7 grey, and
+ newer ones above (psd-tools' list). */
+ uint8_t fill_opacity; /*!< 'iOpa': the fill opacity, 255 opaque; unlike opacity, it
+ leaves the layer's effects alone. */
+ uint32_t protection_flags; /*!< 'lspf': 0x01 transparency, 0x02 composite (pixels), 0x04
+ position locked; 0x08 nesting and 0x80000000 all, by
+ psd-tools. */
+ uint8_t knockout; /*!< 'knko': 0 none, 1 shallow, 2 deep (psd-tools'). */
+ uint8_t blend_clipped_elements; /*!< 'clbl': non-zero blends clipped layers as a group. */
+ uint8_t blend_interior_elements; /*!< 'infx': non-zero blends interior effects as a group. */
+ uint8_t present; /*!< MINIPSD_PRESENT_*: the blocks the layer has. */
+} minipsd_block_settings;
+
+/**
+ * \brief UTF-16BE text as UTF-8, as minipsd_layer_name gives a name.
+ *
+ * An unpaired surrogate becomes U+FFFD, a U+0000 ends the text, and an odd last byte is dropped.
+ *
+ * \param utf16 The text, such as minipsd_version_info.writer_name.
+ * \param utf8 Receives the text and a terminating NUL; may be NULL when cap is 0.
+ * \param cap The size of utf8 in bytes.
+ * \param needed Receives the bytes the text takes with its NUL; may be NULL.
+ * \return MINIPSD_OK when it fits; MINIPSD_MORE when cap is too small (utf8 is then empty);
+ * or MINIPSD_ERR_ARG for a NULL utf16, a view with a size and no data, or utf8 NULL with
+ * cap above 0.
+ */
+MINIPSDDEF int minipsd_utf16be_to_utf8(const minipsd_bytes *utf16, char *utf8, size_t cap, size_t *needed);
+
+/**
+ * \brief Resource 1005, ResolutionInfo.
+ *
+ * \param res A resource from the reader or a document, its id MINIPSD_RES_RESOLUTION_INFO.
+ * \param out Receives the values.
+ * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data is shorter than its 16 bytes;
+ * MINIPSD_ERR_ARG for a NULL argument or another id.
+ */
+MINIPSDDEF int minipsd_resource_resolution_info(const minipsd_resource *res, minipsd_resolution_info *out);
+
+/**
+ * \brief Resource 1057, version info.
+ *
+ * \param res A resource from the reader or a document, its id MINIPSD_RES_VERSION_INFO.
+ * \param out Receives the values; the names point into res's data.
+ * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data ends inside a field or a name;
+ * MINIPSD_ERR_ARG for a NULL argument or another id.
+ */
+MINIPSDDEF int minipsd_resource_version_info(const minipsd_resource *res, minipsd_version_info *out);
+
+/**
+ * \brief Resource 1032, grid and guides information: the header.
+ *
+ * \param res A resource, its id MINIPSD_RES_GRID_AND_GUIDES.
+ * \param out Receives the header.
+ * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data ends inside the header or the guides it
+ * counts; MINIPSD_ERR_ARG for a NULL argument or another id.
+ */
+MINIPSDDEF int minipsd_resource_grid_and_guides(const minipsd_resource *res, minipsd_grid_and_guides *out);
+
+/**
+ * \brief The next guide of resource 1032, in stored order.
+ *
+ * \param res A resource, its id MINIPSD_RES_GRID_AND_GUIDES.
+ * \param pos The caller's cursor: start at 0; moved past the guide returned.
+ * \param guide Receives the guide.
+ * \return MINIPSD_MORE when a guide was written; MINIPSD_OK at the end; MINIPSD_ERR_FORMAT as
+ * minipsd_resource_grid_and_guides; MINIPSD_ERR_ARG for a NULL argument, another id, or a
+ * pos this function did not give.
+ */
+MINIPSDDEF int minipsd_resource_next_guide(const minipsd_resource *res, size_t *pos, minipsd_guide *guide);
+
+/**
+ * \brief The next alpha channel's name: resource 1045 (Unicode) or 1006 (Pascal strings).
+ *
+ * Files commonly hold both; find 1045 first, 1006 for files without it.
+ *
+ * \param res A resource, its id MINIPSD_RES_UNICODE_ALPHA_NAMES or MINIPSD_RES_ALPHA_NAMES.
+ * \param pos The caller's cursor: start at 0; moved past the name returned.
+ * \param name Receives the name: UTF-16BE for 1045 (minipsd_utf16be_to_utf8 converts), raw bytes
+ * in the writer's code page for 1006.
+ * \return MINIPSD_MORE when a name was written; MINIPSD_OK at the end; MINIPSD_ERR_FORMAT when a
+ * name runs past the data; MINIPSD_ERR_ARG for a NULL argument or another id.
+ */
+MINIPSDDEF int minipsd_resource_next_alpha_name(const minipsd_resource *res, size_t *pos, minipsd_bytes *name);
+
+/**
+ * \brief The next alpha channel's identifier, resource 1053.
+ *
+ * \param res A resource, its id MINIPSD_RES_ALPHA_IDENTIFIERS.
+ * \param pos The caller's cursor: start at 0; moved past the identifier returned.
+ * \param id Receives the identifier.
+ * \return MINIPSD_MORE when one was written; MINIPSD_OK at the end; MINIPSD_ERR_FORMAT when the
+ * data is not whole identifiers (four bytes each); MINIPSD_ERR_ARG for a NULL argument or
+ * another id.
+ */
+MINIPSDDEF int minipsd_resource_next_alpha_identifier(const minipsd_resource *res, size_t *pos, uint32_t *id);
+
+/**
+ * \brief Resource 1047: which palette entry of an indexed file is transparent.
+ *
+ * \param res A resource, its id MINIPSD_RES_TRANSPARENCY_INDEX.
+ * \param index Receives the entry, 0 to 255.
+ * \return MINIPSD_OK; MINIPSD_ERR_FORMAT when the data is shorter than two bytes; MINIPSD_ERR_ARG
+ * for a NULL argument or another id.
+ */
+MINIPSDDEF int minipsd_resource_transparency_index(const minipsd_resource *res, uint16_t *index);
+
+/**
+ * \brief Adobe's name for an image resource id, for listing what a file holds.
+ *
+ * Every id of Adobe's "Image Resource IDs" table has one, its ranges included (2000 to 2997 path
+ * information, 4000 to 4999 plug-in resources). The text is a constant, in English.
+ *
+ * \param id A resource's number, such as minipsd_resource.id.
+ * \return The name, such as "Resolution info"; NULL for an id Adobe does not list.
+ */
+MINIPSDDEF const char *minipsd_resource_id_name(uint16_t id);
+
+/**
+ * \brief What a layer's fixed-layout blocks say: id, colour label, fill, locks, knockout, blending
+ * of clipped and interior elements.
+ *
+ * \param layer A record from minipsd_reader_next_layer, or a view built by hand.
+ * \param out Receives the values.
+ * \return MINIPSD_OK; MINIPSD_ERR_FORMAT for a malformed run of blocks or one of these shorter than
+ * its value; MINIPSD_ERR_ARG for a NULL argument.
+ */
+MINIPSDDEF int minipsd_layer_block_settings(const minipsd_layer_view *layer, minipsd_block_settings *out);
+
/* --- Document from a file ------------------------------------------------------------------ */
/**
@@ -1334,8 +1384,10 @@ MINIPSDDEF int minipsd_read_size(const void *data, size_t size, const minipsd_li
* document lives as long as the block. Each layer comes with its rectangle, opacity, visibility,
* blend key, clipping, flags, decoded pixels (with opts->stored, its channels as stored) and its
* blocks as they are; the composite as the file stores it, with composite_matted set, so writing
- * the document gives the same image back. The document has the file's depth, 8 or 16 bits. A
- * file without layers gets one, "Background", holding its image. A file without a resolution
+ * the document gives the same image back. The document has the file's colour mode, colour mode
+ * data and depth. A file without layers gets one, "Background", holding its image, but in the
+ * flat modes (bitmap, indexed, multichannel), which have none; multichannel's composite is always
+ * copied as stored. A file without a resolution
* gets 72 dpi. Layers a 16-bit file keeps in its 'Lr16' block become the document's layers; the
* block stays in layer_tail, empty, where the file had it, and the writer puts the layers back
* there. With opts->stored, a file without layers stays without: no layer, the composite copied.
@@ -1352,7 +1404,8 @@ MINIPSDDEF int minipsd_read_size(const void *data, size_t size, const minipsd_li
* \return MINIPSD_OK or an error code.
*
* \retval MINIPSD_ERR_ARG doc or block is NULL, or block_size is too small.
- * \retval MINIPSD_ERR_UNSUPPORTED Not 8- or 16-bit RGB, ZIP-compressed pixels to decode without
+ * \retval MINIPSD_ERR_UNSUPPORTED A mode at a depth the writer does not take (32-bit CMYK), layers
+ * in a flat mode, ZIP-compressed pixels to decode without
* opts->inflate (with opts->stored they are copied), a
* ZIP-compressed composite to decode, or a layer wider or taller than
* the format allows the writer (MINIPSD_MAX_SIDE, _PSB).
diff --git a/include/minipsd/detail/composite.h b/include/minipsd/detail/composite.h
index 7aec1c2..f4770b6 100644
--- a/include/minipsd/detail/composite.h
+++ b/include/minipsd/detail/composite.h
@@ -18,13 +18,34 @@ typedef struct mpsd__composite {
uint8_t *row; /* width * 4 samples, the caller's */
} mpsd__composite;
-/* Is the document 16-bit: two bytes a sample? */
-static unsigned mpsd__doc_wide(const minipsd_doc *d) { return d->depth == MPSD__CH_16BIT; }
+/* The document's bits a sample (0 is 8), and the bytes a sample takes in memory: 1 << shift. */
+static unsigned mpsd__doc_depth(const minipsd_doc *d) { return d->depth ? d->depth : MPSD__CH_8BIT; }
+static unsigned mpsd__doc_shift(const minipsd_doc *d) { return mpsd__shift(mpsd__doc_depth(d)); }
-/* The full sample of the document's depth. */
-static uint32_t mpsd__doc_full(const minipsd_doc *d) { return mpsd__full(mpsd__doc_wide(d) ? MPSD__CH_16BIT : MPSD__CH_8BIT); }
+/* The full sample of the document's depth (1.0f's bits at 32). */
+static uint32_t mpsd__doc_full(const minipsd_doc *d) { return mpsd__full(mpsd__doc_depth(d)); }
-static int mpsd__contributes(const minipsd_layer *l) { return l->rgba && !l->hidden && l->opacity; }
+/* The file's colour mode: color_mode, whose 0 is RGB unless depth is 1 (bitmap, the only 1-bit
+ mode). */
+static unsigned mpsd__doc_mode(const minipsd_doc *d)
+{
+ return d->color_mode || d->depth == 1 ? d->color_mode : (unsigned)MINIPSD_MODE_RGB;
+}
+
+/* Is the mode flat: bitmap, indexed or multichannel, which have no layers and no transparency? */
+static int mpsd__doc_flat(const minipsd_doc *d)
+{
+ const unsigned mode = mpsd__doc_mode(d);
+ return mode == MINIPSD_MODE_BITMAP || mode == MINIPSD_MODE_INDEXED || mode == MINIPSD_MODE_MULTICHANNEL;
+}
+
+/* The document's colour channels (three in RGB and Lab, four in CMYK, one in grey, duotone, bitmap
+ and indexed, the stored composite's in multichannel), and the samples of a pixel: those, then
+ transparency unless the mode is flat. */
+static unsigned mpsd__doc_colors(const minipsd_doc *d) { return mpsd__colors(mpsd__doc_mode(d), d->composite_channels); }
+static unsigned mpsd__doc_pixel(const minipsd_doc *d) { return mpsd__doc_colors(d) + !mpsd__doc_flat(d); }
+
+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
without area. All four sides zero with pixels (rgba or stored channels) is the full canvas.
@@ -38,13 +59,13 @@ static mpsd__rect mpsd__layer_rect(const minipsd_doc *d, const minipsd_layer *l)
{
mpsd__rect r;
r.top = l->top; r.left = l->left; r.bottom = l->bottom; r.right = l->right;
- if (l->rgba && !l->stored && !(r.top | r.left | r.bottom | r.right)) { /* stored: the file's own rectangle */
+ if (l->pixels && !l->stored && !(r.top | r.left | r.bottom | r.right)) { /* stored: the file's own rectangle */
r.bottom = (int32_t)d->height;
r.right = (int32_t)d->width;
}
r.width = (uint32_t)((int64_t)r.right - r.left);
r.height = (uint32_t)((int64_t)r.bottom - r.top);
- if ((!l->rgba && !l->stored) || r.right <= r.left || r.bottom <= r.top) r.width = r.height = 0; /* inverted: empty */
+ if ((!l->pixels && !l->stored) || r.right <= r.left || r.bottom <= r.top) r.width = r.height = 0; /* inverted: empty */
return r;
}
@@ -62,7 +83,7 @@ static void mpsd__composite_init(mpsd__composite *c, const minipsd_doc *d, uint8
/* The visible layers' row y blended "over" into row, straight alpha: the best effort. */
static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint32_t y)
{
- const unsigned wide = mpsd__doc_wide(d), pixel = (unsigned)MPSD__RGBA << wide;
+ const unsigned wide = mpsd__doc_shift(d), pixel = (unsigned)MPSD__RGBA << wide;
const float max = (float)mpsd__doc_full(d);
size_t i;
int64_t x;
@@ -76,7 +97,7 @@ static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint32_t y)
const int64_t x1 = r.right < (int64_t)d->width ? r.right : (int64_t)d->width;
const uint8_t *src;
if (!mpsd__contributes(l) || ly < 0 || ly >= (int64_t)r.height) continue;
- src = l->rgba + (size_t)ly * r.width * pixel;
+ src = l->pixels + (size_t)ly * r.width * pixel;
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;
@@ -96,19 +117,34 @@ static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint32_t y)
}
/* The composite's row y as the file stores it, matted against white: a supplied row that is
- already matted as it is; otherwise the supplied row copied, or the layers composed, into the
- caller's row, then matted there. */
+ 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. */
static const uint8_t *mpsd__composite_row(mpsd__composite *c, uint32_t y)
{
const minipsd_doc *d = c->doc;
- const unsigned wide = mpsd__doc_wide(d), pixel = (unsigned)MPSD__RGBA << wide;
+ const unsigned wide = mpsd__doc_shift(d), pixel = mpsd__doc_pixel(d) << wide;
const uint32_t max = mpsd__doc_full(d);
const size_t size = (size_t)d->width * pixel;
uint32_t x;
- if (!d->composite) mpsd__compose(d, c->row, y);
- else if (d->composite_matted) return d->composite + (size_t)y * size;
+ 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));
+ for (x = 0; x < d->width; ++x)
+ if (in[x]) c->row[x / MPSD__BYTE_BITS] |= (uint8_t)(1u << (MPSD__BYTE_BITS - 1u - x % MPSD__BYTE_BITS));
+ return c->row;
+ }
+ if (!d->composite) mpsd__compose(d, c->row, y); /* RGB only: minipsd_doc_check */
+ 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; ++x) { /* the merged image is stored over white */
+ 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;
diff --git a/include/minipsd/detail/format.h b/include/minipsd/detail/format.h
index 7ea2d20..c8379d4 100644
--- a/include/minipsd/detail/format.h
+++ b/include/minipsd/detail/format.h
@@ -99,6 +99,7 @@ static uint32_t mpsd__max_side(unsigned psb) { return psb ? MINIPSD_MAX_SIDE_PSB
#define MPSD__GUIDES_HEAD 16
#define MPSD__GUIDE_SIZE 5
+#define MPSD__PALETTE_SIZE 768 /* an indexed file's colour mode data: 256 red, then green, then blue */
#define MPSD__TRANSPARENCY_INDEX_SIZE 2 /* one palette entry, two bytes */
#define MPSD__FILL_OPAQUE 255 /* a layer's fill without an 'iOpa' block */
@@ -164,11 +165,9 @@ static uint64_t mpsd__record_size(uint32_t channels, unsigned psb)
/* A layer's channels by id: -1 transparency, 0 .. colours - 1 the colour channels (0 1 2 red green
blue), -2 the mask, -3 the real mask; other ids are alpha and spot channels. The writer writes
- RGB's in this order: MINIPSD_CH_*, then the two masks. */
+ transparency, the colour channels, then the two masks. */
#define MPSD__RGBA 4 /* RGB's colour channels and transparency; samples a pixel */
-#define MPSD__CH_MASK 4
-#define MPSD__CH_REAL 5
-#define MPSD__CHANNELS 6 /* the most channels the writer writes a layer */
+#define MPSD__CHANNELS 7 /* the most channels the writer writes a layer: CMYK's four, transparency, two masks */
/* The reader keeps a layer's channels in slots: the colour channels, then transparency, as a
target's planes (MINIPSD_PLANES), then the two masks. */
diff --git a/include/minipsd/detail/samples.h b/include/minipsd/detail/samples.h
index 35af2a8..47db01e 100644
--- a/include/minipsd/detail/samples.h
+++ b/include/minipsd/detail/samples.h
@@ -16,13 +16,6 @@
#include
-/* --- colour modes, as the header stores them --- */
-
-#define MPSD__MODE_RGB 3
-#define MPSD__MODE_CMYK 4
-#define MPSD__MODE_MULTICHANNEL 7
-#define MPSD__MODE_LAB 9
-
/* --- colour channels count --- */
#define MPSD__SINGLE_COLOR_CHAN 1
@@ -35,13 +28,16 @@
static unsigned mpsd__colors(unsigned mode, unsigned channels)
{
switch (mode) {
- case MPSD__MODE_RGB: case MPSD__MODE_LAB: return MPSD__TRIPLE_COLOR_CHAN;
- case MPSD__MODE_CMYK: return MPSD__QUADRUPLE_COLOR_CHAN;
- case MPSD__MODE_MULTICHANNEL: return channels;
+ case MINIPSD_MODE_RGB: case MINIPSD_MODE_LAB: return MPSD__TRIPLE_COLOR_CHAN;
+ case MINIPSD_MODE_CMYK: return MPSD__QUADRUPLE_COLOR_CHAN;
+ case MINIPSD_MODE_MULTICHANNEL: return channels;
default: return MPSD__SINGLE_COLOR_CHAN;
}
}
+/* Colour modes: bitmap, grey, indexed, RGB, CMYK, multichannel, duotone, Lab. */
+static int mpsd__mode_ok(uint32_t mode) { return mode <= MINIPSD_MODE_CMYK || (mode >= MINIPSD_MODE_MULTICHANNEL && mode <= MINIPSD_MODE_LAB); }
+
/* --- depths: a sample in memory, a row in the file --- */
#define MPSD__CH_1BIT 1 /* the 1-bit channel size: bitmap, eight pixels a byte */
diff --git a/include/minipsd/read.h b/include/minipsd/read.h
index 2983f52..4128bcd 100644
--- a/include/minipsd/read.h
+++ b/include/minipsd/read.h
@@ -87,22 +87,15 @@ static minipsd_bytes mpsd__copy_tail(mpsd__arena *a, const minipsd_reader *r, mi
return out;
}
-/* Every image resource, copied, in file order: counted on one pass, placed on a second, the reader
- started again in between. */
-static int mpsd__read_resources(minipsd_reader **r, const void *data, size_t size, uint8_t *workspace,
- size_t workspace_size, const minipsd_limits *limits, mpsd__arena *a,
- minipsd_doc *d)
+/* Every image resource, copied, in file order: counted on one pass, placed on a second. */
+static void mpsd__read_resources(const minipsd_reader *r, mpsd__arena *a, minipsd_doc *d)
{
minipsd_resource res, *out;
- minipsd_info info;
- size_t count = 0, i;
- int rc;
- while (minipsd_reader_next_resource(*r, &res) == MINIPSD_MORE) ++count;
- if (!count) return MINIPSD_OK;
- rc = minipsd_reader_init(r, data, size, workspace, workspace_size, limits, &info);
- if (rc != MINIPSD_OK) return rc;
+ size_t count = 0, pos = 0, i;
+ while (minipsd_reader_next_resource(r, &pos, &res) == MINIPSD_MORE) ++count;
+ if (!count) return;
out = (minipsd_resource *)mpsd__take(a, (uint64_t)count * sizeof *out, 1);
- for (i = 0; minipsd_reader_next_resource(*r, &res) == MINIPSD_MORE; ++i) {
+ for (i = 0, pos = 0; minipsd_reader_next_resource(r, &pos, &res) == MINIPSD_MORE; ++i) {
const minipsd_bytes copied = mpsd__copy(a, res.data), name = mpsd__copy(a, res.name);
if (!out) continue;
out[i].id = res.id;
@@ -113,7 +106,6 @@ static int mpsd__read_resources(minipsd_reader **r, const void *data, size_t siz
d->resources = out;
d->resource_count = count;
}
- return MINIPSD_OK;
}
/* The document in a, measured or placed; on MINIPSD_OK with a block, *out and *lost are set. */
@@ -135,42 +127,55 @@ 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;
+ unsigned wide, planes;
uint32_t side;
- int rc;
+ int rc, flat_mode;
rc = minipsd_reader_init(&r, data, size, workspace, sizeof workspace, limits, &info);
if (rc != MINIPSD_OK) return rc;
- if (!r->rgb || (r->depth != MPSD__CH_8BIT && r->depth != MPSD__CH_16BIT)) return MINIPSD_ERR_UNSUPPORTED; /* TODO(S6.13): every mode */
- wide = r->shift; /* 16-bit samples, aligned for a uint16_t */
- side = mpsd__max_side(r->psb);
+ { /* the modes and depths the writer takes: a file it would refuse is refused here */
+ minipsd_doc probe;
+ memset(&probe, 0, sizeof probe);
+ probe.depth = info.depth;
+ probe.color_mode = info.color_mode;
+ probe.color_mode_data = info.color_mode_data;
+ probe.composite_stored.size = MPSD__MARKER_SIZE; /* a composite it has */
+ probe.composite_channels = info.channels;
+ flat_mode = mpsd__doc_flat(&probe);
+ if ((info.color_mode == MINIPSD_MODE_BITMAP) != (info.depth == MPSD__CH_1BIT) || mpsd__doc_mode_ok(&probe) != MINIPSD_OK
+ || (flat_mode && info.layer_count))
+ return MINIPSD_ERR_UNSUPPORTED;
+ }
+ wide = r->shift; /* wider samples, aligned for their type */
+ planes = r->colors + !flat_mode; /* a pixel: the colour channels, then transparency */
+ side = mpsd__max_side(r->psb);
memset(&all, 0, sizeof all); /* every row, un-matted; new fields zero */
- all.depth = wide ? MPSD__CH_16BIT : MPSD__CH_8BIT;
+ all.depth = (int)r->depth;
all.inflate = opts ? opts->inflate : NULL; /* ZIP channels: without it, unsupported */
all.inflate_user = opts ? opts->inflate_user : NULL;
all.scratch = a->scratch;
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 * MPSD__RGBA << wide;
- /* Stored: the composite copied as it is, every channel, never decoded. */
- keep_composite = opts && opts->stored && info.channels >= 3;
+ canvas = (uint64_t)info.width * info.height * planes << 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 */
- /* A flat file: its image as one layer; stored, as it is, no layer, the composite copied. */
- count = info.layer_count ? info.layer_count : keep_composite ? 0 : 1;
+ /* A file without layers: its image as one layer; stored, as it is, no layer, the composite
+ copied. The flat modes have none. */
+ count = info.layer_count ? info.layer_count : keep_composite || flat_mode ? 0 : 1;
d = (minipsd_doc *)mpsd__take(a, sizeof *d, 1);
layers = (minipsd_layer *)mpsd__take(a, (uint64_t)count * sizeof *layers, 1);
if (d) {
memset(d, 0, sizeof *d);
memset(layers, 0, count * sizeof *layers);
}
- rc = mpsd__read_resources(&r, data, size, workspace, sizeof workspace, limits, a, d);
- if (rc != MINIPSD_OK) return rc;
+ mpsd__read_resources(r, a, d);
for (i = 0; i < info.layer_count; ++i) {
uint64_t w, h;
char *name;
- minipsd_bytes extra, pascal, copies[MPSD__RGBA], *stored = NULL;
+ minipsd_bytes extra, pascal, copies[MINIPSD_PLANES], *stored = NULL;
minipsd_mask *mask = NULL;
minipsd_blending_range *ranges;
uint8_t *mask_pixels = NULL;
@@ -178,7 +183,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit
int k, keep;
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(S6.8): alpha and spot channels */
+ if (v.other_channels) loss |= MINIPSD_LOST_CHANNELS; /* TODO: alpha and spot channels (ROADMAP, Not scheduled) */
w = (uint64_t)((int64_t)v.right - v.left);
h = (uint64_t)((int64_t)v.bottom - v.top);
if (w > side || h > side) return MINIPSD_ERR_UNSUPPORTED; /* the writer's limit */
@@ -187,17 +192,17 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit
/* Stored: every channel copied as it is, never decoded; one without transparency is
written without one, as the file has it. An empty layer has nothing to copy. */
keep = opts && opts->stored && w && h;
- for (k = 0; k < MINIPSD_CH_A; ++k) keep = keep && v.channel[k].size; /* R, G, B */
+ for (k = 0; k < (int)r->colors; ++k) keep = keep && v.channel[k].size; /* the colour channels */
if (keep) {
- stored = (minipsd_bytes *)mpsd__take(a, MPSD__RGBA * sizeof *stored, 1);
- for (k = 0; k < MPSD__RGBA; ++k) copies[k] = mpsd__copy(a, v.channel[k]);
+ 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 * MPSD__RGBA << wide, (int)wide) : NULL;
+ pixels = w && h && !keep ? (uint8_t *)mpsd__take(a, w * h * planes << 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 * MPSD__RGBA << wide;
- for (k = 0; w && h && !keep && k < MPSD__RGBA; ++k) /* decoded: pixels, NULL while measuring */
+ if (pixels) decoded += w * h * planes << wide;
+ for (k = 0; w && h && !keep && k < (int)planes; ++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);
@@ -222,12 +227,12 @@ 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 * MPSD__RGBA << wide), MPSD__RGBA, wide);
+ mpsd__target(&t, pixels, (size_t)(w * planes << wide), planes, wide);
rc = minipsd_decode_layer(r, &v, &t, &all);
if (rc != MINIPSD_OK) return rc;
}
layers[i].name = name;
- layers[i].rgba = pixels;
+ layers[i].pixels = pixels;
layers[i].opacity = v.opacity;
layers[i].hidden = v.hidden;
layers[i].top = v.top; layers[i].left = v.left;
@@ -251,7 +256,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit
if (rc != MINIPSD_OK) return rc;
}
if (stored) {
- for (k = 0; k < MPSD__RGBA; ++k) stored[k] = copies[k];
+ for (k = 0; k < (int)planes; ++k) stored[k] = copies[k];
layers[i].stored = stored;
}
}
@@ -262,7 +267,7 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit
if (r->image_start > size) return MINIPSD_ERR_SHORT;
composite.data = r->data + (size_t)r->image_start;
composite.size = size - (size_t)r->image_start;
- if (!mpsd__stored_ok(composite, info.width, info.channels * info.height, wide, r->psb)) /* as decoding says it */
+ if (!mpsd__stored_ok(composite, info.width, info.channels * info.height, r->depth, r->psb)) /* as decoding says it */
return composite.size >= MPSD__MARKER_SIZE && mpsd__get16(composite.data) > MPSD__COMP_ZIP_PREDICTED ? MINIPSD_ERR_FORMAT : MINIPSD_ERR_SHORT;
composite = mpsd__copy(a, composite);
}
@@ -272,29 +277,33 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit
decoded += canvas;
if (max && decoded > max) return MINIPSD_ERR_LIMIT;
}
- if (!keep_composite && info.channels > 3u + (info.composite_has_alpha ? 1u : 0u))
- loss |= MINIPSD_LOST_CHANNELS; /* decoded: R, G, B, transparency */
+ if (!keep_composite && info.channels > r->colors + (info.composite_has_alpha ? 1u : 0u))
+ loss |= MINIPSD_LOST_CHANNELS; /* decoded: the colour channels, transparency */
tail = mpsd__copy_tail(a, r, info.layer_tail);
if (info.icc.size) {
uint8_t *icc = (uint8_t *)mpsd__take(a, info.icc.size, 0);
if (icc) memcpy(icc, info.icc.data, info.icc.size);
if (d) { d->icc = icc; d->icc_size = info.icc.size; }
}
+ { /* the mode, and its data (a palette, a duotone's inks) as the file has it */
+ const minipsd_bytes mode_data = mpsd__copy(a, info.color_mode_data);
+ if (d) { d->color_mode = info.color_mode; d->color_mode_data = mode_data; }
+ }
/* reachable with a 32-bit size_t (wasm32, armv7) */
if (a->zip > (uint64_t)(size_t)-1 - MPSD__ALIGN || a->used > (uint64_t)(size_t)-1 - MPSD__ALIGN - a->zip) return MINIPSD_ERR_SIZE;
if (!d) return MINIPSD_OK;
if (pixels) {
- mpsd__target(&t, pixels, (size_t)info.width * MPSD__RGBA << wide, MPSD__RGBA, wide);
+ mpsd__target(&t, pixels, (size_t)info.width * planes << wide, planes, 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 * MPSD__RGBA << wide, MPSD__RGBA, wide);
+ mpsd__target(&t, flat, (size_t)info.width * planes << wide, planes, wide);
rc = minipsd_decode_composite(r, &t, &all);
if (rc != MINIPSD_OK) return rc;
layers[0].name = "Background";
- layers[0].rgba = flat;
+ layers[0].pixels = flat;
layers[0].opacity = 255;
layers[0].bottom = (int32_t)info.height; /* the rectangle a reader then sees */
layers[0].right = (int32_t)info.width;
@@ -310,7 +319,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 && info.composite_has_alpha && info.channels >= MPSD__RGBA);
+ d->composite_alpha = (uint8_t)(keep_composite && !flat_mode && info.composite_has_alpha && info.channels > r->colors);
d->layer_tail = tail;
d->depth = info.depth;
d->format = info.format;
diff --git a/include/minipsd/reader.h b/include/minipsd/reader.h
index 64d027f..9001e88 100644
--- a/include/minipsd/reader.h
+++ b/include/minipsd/reader.h
@@ -41,8 +41,7 @@ struct minipsd_reader {
uint32_t channels; /* the composite: how many channels, 1..MPSD__MAX_CHANNELS */
int composite_alpha; /* the composite: non-zero if it has an alpha channel */
uint64_t image_start; /* the composite: where its channel data starts */
- uint64_t res_start; /* the resources: where the first and the next one start, and */
- uint64_t res_next; /* where they end */
+ uint64_t res_start; /* the resources: where the first one starts, where they end */
uint64_t res_end;
uint64_t li_start; /* the layer info: its first record, and the end of its content */
uint64_t li_end;
@@ -136,9 +135,6 @@ static void mpsd__info_clear(minipsd_info *i)
i->color_mode_data.size = 0;
}
-/* Colour modes: bitmap, grey, indexed, RGB, CMYK, multichannel, duotone, Lab. */
-static int mpsd__mode_ok(uint32_t mode) { return mode <= 4 || (mode >= 7 && mode <= 9); }
-
/* The layer count at `at`, then the records and their channel data up to `end`: the layer info. */
static int mpsd__layers_at(minipsd_reader *r, uint64_t at, uint64_t end, minipsd_info *in)
{
@@ -233,11 +229,11 @@ static int mpsd__scan(minipsd_reader *r, minipsd_info *info)
/* The image resources, all of them: the resolution and the ICC profile are among them. */
rc = mpsd__length(r, at, 0, &length);
if (rc != MINIPSD_OK) return rc;
- r->res_start = r->res_next = at + MPSD__LEN;
- r->res_end = r->res_next + length;
+ r->res_start = at + MPSD__LEN;
+ r->res_end = r->res_start + length;
rc = mpsd__have(r, 0, r->res_end);
if (rc != MINIPSD_OK) return rc;
- for (at = r->res_next; at < r->res_end;) { /* checks every block as well */
+ for (at = r->res_start; at < r->res_end;) { /* checks every block as well */
minipsd_resource res;
rc = mpsd__resource(r, at, &res, &at);
if (rc != MINIPSD_OK) return rc;
@@ -273,7 +269,7 @@ static int mpsd__scan(minipsd_reader *r, minipsd_info *info)
}
r->layer_count = in.layer_count;
r->record_next = r->li_start;
- r->rgb = in.color_mode == MPSD__MODE_RGB;
+ r->rgb = in.color_mode == MINIPSD_MODE_RGB;
r->depth = in.depth;
r->shift = mpsd__shift(in.depth);
r->colors = mpsd__colors(in.color_mode, in.channels - (in.composite_has_alpha ? 1u : 0u));
@@ -322,7 +318,7 @@ MINIPSDDEF int minipsd_reader_init(minipsd_reader **out, const void *data, size_
r->rgb = 0;
r->depth = r->shift = r->colors = 0;
r->psb = 0;
- r->res_start = r->res_next = r->res_end = 0;
+ r->res_start = r->res_end = 0;
r->li_start = r->li_end = 0;
r->layer_count = r->layer_next = 0;
r->record_next = r->data_next = 0;
@@ -339,21 +335,24 @@ MINIPSDDEF uint64_t minipsd_reader_needed(const minipsd_reader *r)
return r ? r->needed : 0;
}
-MINIPSDDEF int minipsd_reader_next_resource(minipsd_reader *r, minipsd_resource *res)
+MINIPSDDEF int minipsd_reader_next_resource(const minipsd_reader *r, size_t *pos, minipsd_resource *res)
{
- if (!r || !res || !r->ready) return MINIPSD_ERR_ARG;
- if (r->res_next >= r->res_end) return MINIPSD_OK;
- (void)mpsd__resource(r, r->res_next, res, &r->res_next); /* init checked every block */
+ uint64_t next;
+ int rc;
+ if (!r || !pos || !res || !r->ready) return MINIPSD_ERR_ARG;
+ if (*pos >= r->res_end - r->res_start) return MINIPSD_OK;
+ rc = mpsd__resource(r, r->res_start + *pos, res, &next); /* a pos it did not give: checked again */
+ if (rc != MINIPSD_OK) return rc;
+ *pos = (size_t)(next - r->res_start); /* the section lies in the buffer */
return MINIPSD_MORE;
}
MINIPSDDEF int minipsd_reader_find_resource(const minipsd_reader *r, uint16_t id, minipsd_resource *res)
{
minipsd_resource found;
- uint64_t at;
+ size_t pos = 0;
if (!r || !res || !r->ready) return MINIPSD_ERR_ARG;
- for (at = r->res_start; at < r->res_end;) {
- (void)mpsd__resource(r, at, &found, &at); /* init checked every block */
+ while (minipsd_reader_next_resource(r, &pos, &found) == MINIPSD_MORE) {
if (found.id == id) {
*res = found;
return MINIPSD_OK;
diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h
index 7518d03..928ba78 100644
--- a/include/minipsd/writer.h
+++ b/include/minipsd/writer.h
@@ -31,6 +31,7 @@
/* The document's format: what its kept bytes are laid out as, 1 for a PSB (detail/format.h). */
static unsigned mpsd__doc_psb(const minipsd_doc *d) { return d->format == MINIPSD_PSB; }
+
/* The format written: opts->format, or else the document's own; 1 for a PSB. */
static unsigned mpsd__out_psb(const minipsd_doc *d, const minipsd_write_opts *opts)
{
@@ -51,12 +52,14 @@ 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. */
+/* One row of one channel, width samples of the document's depth, step bytes apart; at 1 bit
+ already packed, a byte apart. */
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_wide(d);
- if (d->compression == MINIPSD_RAW) mpsd__raw(s, row, (size_t)width << wide, step, wide);
- else mpsd__packbits(s, row, (size_t)width << wide, step, wide);
+ 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);
}
/* The compression marker a channel the writer encodes starts with. */
@@ -69,19 +72,32 @@ static uint64_t mpsd__row_size(const minipsd_doc *d, const uint8_t *row, size_t
return c.count;
}
-/* A layer's channels in the order written: transparency (id -1), R, G, B (RGBA offsets 3, 0, 1,
- 2), then the mask (-2) and the real mask (-3) when there are. */
-static const int mpsd__channel_id[MPSD__CHANNELS] = { -1, 0, 1, 2, -2, -3 };
-static const int mpsd__channel_of[MPSD__RGBA] = { 3, 0, 1, 2 }; /* RGBA offset for each colour channel */
+/* A layer's channels in the order written, k: transparency (id -1), the colour channels (ids 0
+ on), then the mask (-2) and the real mask (-3) when there are. The mask's k follows the pixel's
+ channels. */
+static int mpsd__ch_mask(const minipsd_doc *d) { return (int)mpsd__doc_pixel(d); }
+
+static int mpsd__channel_id(const minipsd_doc *d, int k)
+{
+ const int mask = mpsd__ch_mask(d);
+ return k == 0 ? -1 : k < mask ? k - 1 : k == mask ? -2 : -3;
+}
+
+/* Where pixel channel k lies in a pixel, and in stored (as minipsd_layer_view.channel): the colour
+ 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 R. */
-static int mpsd__channel_count(const minipsd_layer *l)
+ layer without transparency, as its file had it, starts at the first colour channel. */
+static int mpsd__channel_count(const minipsd_doc *d, const minipsd_layer *l)
{
- return MPSD__RGBA + (l->mask ? 1 + (l->mask->real.size != 0) : 0);
+ return mpsd__ch_mask(d) + (l->mask ? 1 + (l->mask->real.size != 0) : 0);
}
-static int mpsd__first_channel(const minipsd_layer *l) { return l->stored && !l->stored[MINIPSD_CH_A].size; }
+static int mpsd__first_channel(const minipsd_doc *d, const minipsd_layer *l)
+{
+ return l->stored && !l->stored[mpsd__doc_colors(d)].size;
+}
/* Where channel k of a layer comes from: bytes to copy as they are, or samples to encode, step
bytes apart, width by height, of the document's depth. A stored channel's height is its
@@ -106,27 +122,28 @@ static uint32_t mpsd__real_rows(const minipsd_mask *m)
static mpsd__source mpsd__channel_source(const minipsd_doc *d, const minipsd_layer *l, int k)
{
- const unsigned wide = mpsd__doc_wide(d);
+ const unsigned wide = mpsd__doc_shift(d);
mpsd__source src;
src.stored = NULL;
src.samples = NULL;
src.step = (size_t)1 << wide;
src.width = src.height = 0;
- if (k < MPSD__RGBA) {
+ if (k < mpsd__ch_mask(d)) {
static const uint8_t full[2] = { MPSD__FULL_8, MPSD__FULL_8 }; /* 255, or 65535 at 16 bits */
- const mpsd__rect r = mpsd__layer_rect(d, l);
+ const mpsd__rect r = mpsd__layer_rect(d, l);
+ const int at = mpsd__channel_of(d, k);
src.height = r.height;
- if (l->stored && l->stored[mpsd__channel_of[k]].size) { src.stored = &l->stored[mpsd__channel_of[k]]; return src; }
+ if (l->stored && l->stored[at].size) { src.stored = &l->stored[at]; return src; }
if (l->stored) { /* no transparency stored: never written (mpsd__first_channel) */
src.samples = r.height ? full : NULL;
src.step = 0;
} else {
- src.samples = r.height ? l->rgba + ((size_t)mpsd__channel_of[k] << wide) : NULL;
- src.step = (size_t)MPSD__RGBA << wide;
+ src.samples = r.height ? l->pixels + ((size_t)at << wide) : NULL;
+ src.step = (size_t)mpsd__doc_pixel(d) << wide;
}
src.width = r.width;
src.height = r.height;
- } else if (k == MPSD__CH_MASK) {
+ } else if (k == mpsd__ch_mask(d)) {
const minipsd_mask *m = l->mask;
const uint32_t w = (uint32_t)((int64_t)m->right - m->left), h = (uint32_t)((int64_t)m->bottom - m->top);
src.height = h;
@@ -165,9 +182,9 @@ static uint64_t mpsd__zip_plane(const minipsd_doc *d)
size_t i;
int k;
for (i = 0; mpsd__zip(d) && i < d->layer_count; ++i)
- for (k = 0; k <= MPSD__CH_MASK && k < mpsd__channel_count(&d->layers[i]); ++k) {
+ for (k = 0; 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_wide(d) : 0;
+ const uint64_t n = src.samples ? (uint64_t)src.width * src.height << mpsd__doc_shift(d) : 0;
if (n > max) max = n;
}
return max;
@@ -182,7 +199,7 @@ static int mpsd__copies_composite(const minipsd_doc *d) { return !d->composite &
/* The composite's channels in the file: the stored one's, or R, G, B and transparency. */
static unsigned mpsd__composite_channels(const minipsd_doc *d)
{
- return mpsd__copies_composite(d) ? d->composite_channels : MPSD__RGBA;
+ 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
@@ -379,7 +396,7 @@ static uint64_t mpsd__layer_channel_size(const minipsd_doc *d, const minipsd_lay
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_wide(d));
+ if (d->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);
return size;
@@ -397,13 +414,13 @@ static uint64_t mpsd__layer_extra_size(const minipsd_layer *l, unsigned from, un
static void mpsd__layer_record(mpsd__sink *s, const minipsd_doc *d, const minipsd_layer *l, const uint64_t *sizes, unsigned out)
{
const mpsd__rect r = mpsd__layer_rect(d, l);
- const int n = mpsd__channel_count(l);
+ const int n = mpsd__channel_count(d, l);
int k;
mpsd__u32(s, (uint32_t)r.top); mpsd__u32(s, (uint32_t)r.left);
mpsd__u32(s, (uint32_t)r.bottom); mpsd__u32(s, (uint32_t)r.right);
- mpsd__u16(s, (unsigned)(n - mpsd__first_channel(l)));
- for (k = mpsd__first_channel(l); k < n; ++k) {
- mpsd__u16(s, (unsigned)(uint16_t)(int16_t)mpsd__channel_id[k]);
+ mpsd__u16(s, (unsigned)(n - mpsd__first_channel(d, l)));
+ for (k = mpsd__first_channel(d, l); k < n; ++k) {
+ mpsd__u16(s, (unsigned)(uint16_t)(int16_t)mpsd__channel_id(d, k));
mpsd__putlen(s, sizes[k], out);
}
mpsd__put(s, "8BIM", 4);
@@ -500,10 +517,10 @@ static uint64_t mpsd__resources_size(const minipsd_doc *d)
return c.count;
}
-/* A stored channel that fits a width x height rectangle of one-byte or (wide) two-byte samples:
- the compression marker, then raw rows, or (RLE) a byte count per row (four bytes in a PSB) and
- rows no longer than they add up to. ZIP passes as it is. */
-static int mpsd__stored_ok(minipsd_bytes c, uint32_t width, uint32_t height, unsigned wide, unsigned psb)
+/* A stored channel that fits a width x height rectangle of samples of depth bits: the compression
+ marker, then raw rows, or (RLE) a byte count per row (four bytes in a PSB) and rows no longer
+ than they add up to. ZIP passes as it is. */
+static int mpsd__stored_ok(minipsd_bytes c, uint32_t width, uint32_t height, unsigned depth, unsigned psb)
{
const uint64_t counts = (uint64_t)height * mpsd__count_size(psb);
uint64_t left, sum = 0;
@@ -512,7 +529,7 @@ static int mpsd__stored_ok(minipsd_bytes c, uint32_t width, uint32_t height, uns
left = c.size - MPSD__MARKER_SIZE;
switch (mpsd__get16(c.data)) {
case MPSD__COMP_RAW:
- return ((uint64_t)width * height << wide) <= left;
+ return mpsd__row_bytes(width, depth) * height <= left;
case MPSD__COMP_RLE:
if (counts > left) return 0;
for (y = 0; y < height; ++y) sum += mpsd__count(c.data + MPSD__MARKER_SIZE, y, psb);
@@ -588,6 +605,25 @@ static int mpsd__tail_range(mpsd__sink *s, const minipsd_doc *d, size_t lo, size
return MINIPSD_OK;
}
+/* Does the writer put the layers in a block of its own, 'Lr32', the layer info left empty, as
+ Photoshop keeps a 32-bit file's? For a 32-bit document whose kept tail has no layers' block: after
+ the tail, with an empty 'Mt32' before it when the composite has transparency (the layer count in
+ the block is then positive, as Photoshop writes it). */
+static int mpsd__own_layers_block(const minipsd_doc *d)
+{
+ size_t end;
+ return mpsd__doc_depth(d) == MPSD__CH_32BIT && d->layer_count && mpsd__layers_block(d, &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)
+{
+ mpsd__put(s, out ? "8B64" : "8BIM", 4);
+ mpsd__put(s, key, 4);
+ mpsd__putlen(s, size, out);
+}
+
/* The kept tail whole, but for the layers' block. */
static int mpsd__tail_out(mpsd__sink *s, const minipsd_doc *d, unsigned out)
{
@@ -604,8 +640,9 @@ static uint64_t mpsd__section_size(const minipsd_doc *d, uint64_t info, unsigned
mpsd__sink c = { NULL, NULL, 0, 0 };
size_t end;
(void)mpsd__tail_out(&c, d, out);
- if (mpsd__layers_block(d, &end) < d->layer_tail.size)
+ 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' */
return mpsd__len_size(out) + info + c.count;
}
@@ -649,7 +686,7 @@ static int mpsd__table_ok(const mpsd__source *src, unsigned from, unsigned out)
{
uint32_t y;
if (!mpsd__retable(src, from, out)) return MINIPSD_OK;
- if (!mpsd__stored_ok(*src->stored, 0, src->height, 0, from))
+ 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;
@@ -659,19 +696,21 @@ static int mpsd__table_ok(const mpsd__source *src, unsigned from, unsigned out)
static int mpsd__retable_ok(const minipsd_doc *d, const minipsd_layer *l, int k, unsigned out)
{
const mpsd__source src = mpsd__channel_source(d, l, k);
- if (k == MPSD__CH_REAL && mpsd__retable(&src, mpsd__doc_psb(d), out) && l->mask->rest.size < MPSD__REAL_RECT + MPSD__RECT_SIZE)
+ if (k == mpsd__ch_mask(d) + 1 && mpsd__retable(&src, mpsd__doc_psb(d), out) && l->mask->rest.size < MPSD__REAL_RECT + MPSD__RECT_SIZE)
return MINIPSD_ERR_UNSUPPORTED; /* no real rectangle to count rows by */
return mpsd__table_ok(&src, mpsd__doc_psb(d), out);
}
-/* A stored composite: R, G, B and up to the format's limit, the fourth transparency only when
- there is one, fitting the canvas, its table the out format's (or one that can become it). */
+/* A stored composite: the mode's colour channels and up to the format's limit, the one after them
+ transparency only when there is one, fitting the canvas, its table the out format's (or one that
+ can become it). */
static int mpsd__composite_ok(const minipsd_doc *d, unsigned out)
{
const mpsd__source src = mpsd__composite_source(d);
if (!mpsd__copies_composite(d)) return MINIPSD_OK;
- if (d->composite_channels < 3 || d->composite_channels > MPSD__MAX_CHANNELS || (d->composite_alpha && d->composite_channels < MPSD__RGBA)
- || !mpsd__stored_ok(d->composite_stored, d->width, src.height, mpsd__doc_wide(d), mpsd__doc_psb(d)))
+ if (d->composite_channels < mpsd__doc_colors(d) || d->composite_channels > MPSD__MAX_CHANNELS
+ || (d->composite_alpha && d->composite_channels < mpsd__doc_pixel(d))
+ || !mpsd__stored_ok(d->composite_stored, d->width, src.height, mpsd__doc_depth(d), mpsd__doc_psb(d)))
return MINIPSD_ERR_ARG;
return mpsd__table_ok(&src, mpsd__doc_psb(d), out);
}
@@ -697,12 +736,36 @@ static int mpsd__groups_ok(const minipsd_doc *d)
static int mpsd__mask_ok(const minipsd_doc *d, const minipsd_mask *m, unsigned out)
{
const int64_t w = (int64_t)m->right - m->left, h = (int64_t)m->bottom - m->top;
- const unsigned wide = mpsd__doc_wide(d), psb = mpsd__doc_psb(d);
+ const unsigned psb = mpsd__doc_psb(d);
if (w < 0 || h < 0 || (m->rest.size && !m->rest.data)) return MINIPSD_ERR_ARG;
if (w > mpsd__max_side(out) || h > mpsd__max_side(out)) return MINIPSD_ERR_SIZE;
- if (m->stored.size ? !mpsd__stored_ok(m->stored, (uint32_t)w, (uint32_t)h, wide, psb) : (w && h && !m->pixels))
+ if (m->stored.size ? !mpsd__stored_ok(m->stored, (uint32_t)w, (uint32_t)h, mpsd__doc_depth(d), psb) : (w && h && !m->pixels))
+ return MINIPSD_ERR_ARG;
+ if (m->real.size && !mpsd__stored_ok(m->real, 0, 0, MPSD__CH_8BIT, psb)) return MINIPSD_ERR_ARG; /* its rectangle is in rest */
+ return MINIPSD_OK;
+}
+
+/* The modes the writer takes, at the depths Photoshop has them: bitmap at 1 bit, indexed at 8,
+ the others at 8 and 16; RGB and grey at 32 too. Duotone brings its colour mode data (inks and
+ curves), indexed its palette; a 32-bit document may bring some (psd-tools' 32-bit files hold an
+ 'hdrt' block, HDR toning), the others none. Every mode but RGB, and every 32-bit document,
+ brings its composite, supplied or stored: the writer composes 8- and 16-bit RGB only. The flat modes (bitmap, indexed, multichannel) have no layers and no
+ transparency, and multichannel's composite is only copied: minipsd does not know its channels. */
+static int mpsd__doc_mode_ok(const minipsd_doc *d)
+{
+ const unsigned mode = mpsd__doc_mode(d), depth = mpsd__doc_depth(d);
+ const int byte = mode == MINIPSD_MODE_BITMAP || mode == MINIPSD_MODE_INDEXED; /* at most 8 bits */
+ if (!mpsd__mode_ok(mode) || (mode == MINIPSD_MODE_BITMAP) != (depth == MPSD__CH_1BIT)) return MINIPSD_ERR_ARG;
+ if ((depth == MPSD__CH_16BIT && byte)
+ || (depth == MPSD__CH_32BIT && mode != MINIPSD_MODE_RGB && mode != MINIPSD_MODE_GREY))
+ return MINIPSD_ERR_UNSUPPORTED;
+ if (mode == MINIPSD_MODE_INDEXED ? d->color_mode_data.size != MPSD__PALETTE_SIZE
+ : mode == MINIPSD_MODE_DUOTONE ? !d->color_mode_data.size
+ : d->color_mode_data.size && depth != MPSD__CH_32BIT)
return MINIPSD_ERR_ARG;
- if (m->real.size && !mpsd__stored_ok(m->real, 0, 0, 0, psb)) return MINIPSD_ERR_ARG; /* its rectangle is in rest */
+ 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 (mode == MINIPSD_MODE_MULTICHANNEL && (d->composite || !d->composite_channels)) return MINIPSD_ERR_ARG;
return MINIPSD_OK;
}
@@ -717,9 +780,12 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *d, const minipsd_write_opts
/* The dpi is stored as 16.16 fixed point: it must round to 1 .. 2^32 - 1. NaN fails both. */
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_8BIT && d->depth != MPSD__CH_16BIT) || d->format > MINIPSD_PSB
- || d->compression < MINIPSD_RLE || d->compression > MINIPSD_ZIP_PREDICTED || (mpsd__zip(d) && !(opts && opts->deflate)))
+ || (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->color_mode_data.size && !d->color_mode_data.data))
return MINIPSD_ERR_ARG;
+ rc = mpsd__doc_mode_ok(d);
+ if (rc != MINIPSD_OK) return rc;
rc = mpsd__kept_ok(d, out);
if (rc != MINIPSD_OK) return rc;
if (!mpsd__groups_ok(d)) return MINIPSD_ERR_ARG;
@@ -733,21 +799,21 @@ 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->rgba && !l->stored && w && h)) return MINIPSD_ERR_ARG;
+ if (w < 0 || h < 0 || (!l->pixels && !l->stored && w && h)) 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; rc == MINIPSD_OK && k < mpsd__channel_count(l); ++k) rc = mpsd__retable_ok(d, l, k, out);
+ for (k = mpsd__ch_mask(d); rc == MINIPSD_OK && k < mpsd__channel_count(d, l); ++k) rc = mpsd__retable_ok(d, l, k, out);
if (rc != MINIPSD_OK) return rc;
if (mpsd__layer_extra_size(l, mpsd__doc_psb(d), out) > MPSD__MAX_SECTION) return MINIPSD_ERR_SIZE;
if (!l->stored) continue;
- if (!l->rgba && !d->composite && !d->composite_stored.size) return MINIPSD_ERR_ARG; /* nothing to compose it from */
- for (k = 0; k < MPSD__RGBA; ++k) { /* transparency may be missing */
+ if (!l->pixels && !d->composite && !d->composite_stored.size) return MINIPSD_ERR_ARG; /* nothing to compose it from */
+ for (k = 0; k < (int)mpsd__doc_pixel(d); ++k) { /* transparency, the last, may be missing */
const mpsd__rect r = mpsd__layer_rect(d, l);
- if ((k != MINIPSD_CH_A || l->stored[k].size)
- && !mpsd__stored_ok(l->stored[k], r.width, r.height, mpsd__doc_wide(d), mpsd__doc_psb(d)))
+ if ((k != (int)mpsd__doc_colors(d) || l->stored[k].size)
+ && !mpsd__stored_ok(l->stored[k], r.width, r.height, mpsd__doc_depth(d), mpsd__doc_psb(d)))
return MINIPSD_ERR_ARG;
}
- for (k = 0; k < MPSD__RGBA; ++k) {
+ for (k = 0; k < mpsd__ch_mask(d); ++k) {
rc = mpsd__retable_ok(d, l, k, out);
if (rc != MINIPSD_OK) return rc;
}
@@ -766,25 +832,31 @@ static void mpsd__head(mpsd__sink *s, const minipsd_doc *d, uint64_t info, unsig
mpsd__u16(s, mpsd__composite_channels(d)); /* R, G, B, alpha, ... */
mpsd__u32(s, d->height);
mpsd__u32(s, d->width);
- mpsd__u16(s, mpsd__doc_wide(d) ? 16 : 8); /* bits per channel */
- mpsd__u16(s, MPSD__MODE_RGB);
+ mpsd__u16(s, mpsd__doc_depth(d)); /* bits per channel */
+ mpsd__u16(s, mpsd__doc_mode(d));
- mpsd__u32(s, 0); /* colour mode data */
+ mpsd__u32(s, (uint32_t)d->color_mode_data.size); /* colour mode data: a palette, inks */
+ mpsd__put(s, d->color_mode_data.data, d->color_mode_data.size);
mpsd__u32(s, (uint32_t)mpsd__resources_size(d));
mpsd__resources(s, d);
size_t end;
- const size_t at = mpsd__layers_block(d, &end);
+ const size_t at = mpsd__layers_block(d, &end);
+ const int own = mpsd__own_layers_block(d);
mpsd__putlen(s, mpsd__section_size(d, info, out), out); /* layer and mask information */
- mpsd__putlen(s, at < d->layer_tail.size ? 0 : info, out);
+ mpsd__putlen(s, at < d->layer_tail.size || own ? 0 : info, out);
if (at < d->layer_tail.size) {
(void)mpsd__tail_range(s, d, 0, at, out);
mpsd__put(s, d->layer_tail.data + at, MPSD__BLOCK_LEN); /* '8BIM', 'Lr16' */
mpsd__putlen(s, info, out);
+ } else if (own) { /* the tail, then the writer's blocks */
+ (void)mpsd__tail_out(s, d, out);
+ if (mpsd__composite_alpha(d)) mpsd__own_block(s, "Mt32", 0, out);
+ mpsd__own_block(s, "Lr32", info, out);
}
- if (d->layer_count)
- mpsd__u16(s, (unsigned)(uint16_t)(mpsd__composite_alpha(d) ? -(int16_t)d->layer_count : (int16_t)d->layer_count)); /* negative: composite has alpha */
+ if (d->layer_count) /* negative: the composite has transparency */
+ mpsd__u16(s, (unsigned)(uint16_t)(mpsd__composite_alpha(d) && !own ? -(int16_t)d->layer_count : (int16_t)d->layer_count));
}
/* After the last layer channel: the layer info's padding, the kept tail or an empty global layer
@@ -797,6 +869,8 @@ static void mpsd__tail(mpsd__sink *s, const minipsd_doc *d, uint64_t content, ui
if (at < d->layer_tail.size) { /* the block's padding, then the tail after it */
mpsd__zeros(s, mpsd__pad((size_t)info, 4) - (size_t)info);
(void)mpsd__tail_range(s, d, end, d->layer_tail.size, out);
+ } else if (mpsd__own_layers_block(d)) { /* the writer's block: its padding; the tail came before it */
+ 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 */
}
@@ -827,8 +901,8 @@ enum {
struct minipsd_writer {
const minipsd_doc *doc;
uint64_t *channel_sizes; /* layer_count * MPSD__CHANNELS */
- uint32_t *composite_sizes; /* MPSD__RGBA * height, channel-major (RLE) */
- uint8_t *composite_row; /* width * MPSD__RGBA samples */
+ uint32_t *composite_sizes; /* pixel * height, channel-major (RLE) */
+ uint8_t *composite_row; /* width * pixel samples */
uint64_t content, info; /* the layer info's length, unpadded and padded */
uint64_t total; /* the file size */
/* the cursor */
@@ -861,8 +935,8 @@ 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__RGBA * sizeof(uint32_t);
- const size_t row = (size_t)d->width * MPSD__RGBA << mpsd__doc_wide(d);
+ const size_t rows = d->compression == 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) {
uint8_t *p = (uint8_t *)workspace;
@@ -881,7 +955,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 && mpsd__channel_source(d, &d->layers[i], k).samples;
+ return mpsd__zip(d) && 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
@@ -891,7 +965,7 @@ 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_wide(d);
+ const unsigned wide = mpsd__doc_shift(d);
const size_t bytes = (size_t)src.width << wide;
size_t x, y, size = w->zip_room;
if (w->zip_layer == i && w->zip_channel == k) return MINIPSD_OK;
@@ -899,10 +973,13 @@ static int mpsd__zip_channel(minipsd_writer *w, size_t i, int k)
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) out[x] = row[mpsd__at(x, (ptrdiff_t)src.step, wide)];
- if (d->compression == MINIPSD_ZIP_PREDICTED) mpsd__predict(out, src.width, wide ? MPSD__CH_16BIT : MPSD__CH_8BIT);
+ 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]
+ = 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 (w->deflate(w->deflate_user, w->zip_plane, bytes * src.height, w->zip_out, &size) || size > w->zip_room)
+ 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;
w->zip_size = size;
w->zip_layer = i;
@@ -923,7 +1000,7 @@ static int mpsd__measure_layers(minipsd_writer *w)
int k, rc;
w->content = d->layer_count ? MPSD__LAYER_COUNT : 0; /* no layers: no count either */
for (i = 0; i < d->layer_count; ++i) {
- const int n = mpsd__channel_count(&d->layers[i]), first = mpsd__first_channel(&d->layers[i]);
+ const int n = mpsd__channel_count(d, &d->layers[i]), first = mpsd__first_channel(d, &d->layers[i]);
w->content += mpsd__record_size((uint32_t)(n - first), psb) + mpsd__layer_extra_size(&d->layers[i], mpsd__doc_psb(d), psb);
for (k = first; k < n; ++k) {
if (mpsd__zips(d, i, k)) { /* deflated once here, once more to write */
@@ -967,7 +1044,7 @@ MINIPSDDEF int minipsd_writer_init(minipsd_writer **out, const minipsd_doc *d, c
mpsd__writer_layout(d, workspace, &w);
w->doc = d;
w->out = mpsd__out_psb(d, opts);
- wide = mpsd__doc_wide(d);
+ wide = mpsd__doc_shift(d);
w->deflate = opts ? opts->deflate : NULL; /* copied: opts may be gone by the first fill */
w->deflate_user = opts ? opts->deflate_user : NULL;
w->zip_layer = (size_t)-1;
@@ -977,7 +1054,7 @@ MINIPSDDEF int minipsd_writer_init(minipsd_writer **out, const minipsd_doc *d, c
if (rc != MINIPSD_OK) return rc;
total = MPSD__HEADER_SIZE
- + MPSD__LEN /* colour mode data: none */
+ + MPSD__LEN + d->color_mode_data.size /* colour mode data */
+ MPSD__LEN + mpsd__resources_size(d)
+ mpsd__len_size(w->out) + mpsd__section_size(d, w->info, w->out) /* the section's length, the section */
+ MPSD__MARKER_SIZE; /* the composite's */
@@ -985,13 +1062,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 += (uint64_t)d->width * d->height * MPSD__RGBA << wide;
+ total += mpsd__row_bytes(d->width, mpsd__doc_depth(d)) * d->height * mpsd__doc_pixel(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 < MPSD__RGBA; ++k) {
- const uint64_t n = mpsd__row_size(d, row + ((size_t)k << wide), (size_t)MPSD__RGBA << wide, d->width);
+ 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);
w->composite_sizes[(size_t)k * d->height + y] = (uint32_t)n;
total += mpsd__count_size(w->out) + n;
}
@@ -1079,7 +1156,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_wide(d)), (size_t)MPSD__RGBA << mpsd__doc_wide(d), d->width);
+ 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);
break;
}
}
@@ -1114,7 +1191,7 @@ static void mpsd__writer_next(minipsd_writer *w)
case MPSD__RECORD:
if (++w->layer < d->layer_count) return;
w->layer = 0;
- w->channel = mpsd__first_channel(&d->layers[0]);
+ w->channel = mpsd__first_channel(d, &d->layers[0]);
w->piece = mpsd__channel_piece(w);
return;
case MPSD__COMPRESSION: /* rows follow, unless the layer is empty */
@@ -1153,17 +1230,17 @@ 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 < MPSD__RGBA) return;
+ if (++w->channel < (int)mpsd__doc_pixel(d)) return;
w->channel = 0;
w->piece = w->piece == MPSD__COMPOSITE_COUNT ? MPSD__COMPOSITE_ROW : MPSD__DONE;
return;
}
if (w->copying) { w->piece = MPSD__DONE; return; } /* the stored composite is complete */
/* A layer channel is complete: the next channel, the next layer, or the tail. */
- if (++w->channel < mpsd__channel_count(&d->layers[w->layer])) { w->piece = mpsd__channel_piece(w); return; }
+ if (++w->channel < mpsd__channel_count(d, &d->layers[w->layer])) { w->piece = mpsd__channel_piece(w); return; }
w->channel = 0;
if (++w->layer < d->layer_count) {
- w->channel = mpsd__first_channel(&d->layers[w->layer]);
+ w->channel = mpsd__first_channel(d, &d->layers[w->layer]);
w->piece = mpsd__channel_piece(w);
return;
}
diff --git a/tests/fuzz/fuzz_reader.c b/tests/fuzz/fuzz_reader.c
index 9ad6093..ca2bff1 100644
--- a/tests/fuzz/fuzz_reader.c
+++ b/tests/fuzz/fuzz_reader.c
@@ -128,7 +128,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
check_short(r, rc, size);
if (rc != MINIPSD_OK) return 0;
- while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE) {}
+ for (pos = 0; minipsd_reader_next_resource(r, &pos, &res) == MINIPSD_MORE;) {}
while ((rc = minipsd_reader_next_layer(r, &layer)) == MINIPSD_MORE) {
(void)minipsd_layer_name(&layer, name, sizeof name, &need); /* often too small: truncation too */
for (pos = 0; minipsd_layer_next_block(&layer, &pos, &block) == MINIPSD_MORE;) {}
diff --git a/tests/minipsd_c_smoke.c b/tests/minipsd_c_smoke.c
index f2e0c2d..eccea49 100644
--- a/tests/minipsd_c_smoke.c
+++ b/tests/minipsd_c_smoke.c
@@ -41,8 +41,8 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size)
for (i = 0; i < sizeof px; ++i) px[i] = (uint8_t)(i * 17);
memset(layers, 0, sizeof layers); /* the structs grow at the end: zero is every default */
- layers[0].name = "Template"; layers[0].rgba = px; layers[0].opacity = 255; layers[0].hidden = 0;
- layers[1].name = "Layer"; layers[1].rgba = NULL; layers[1].opacity = 255; layers[1].hidden = 0;
+ layers[0].name = "Template"; layers[0].pixels = px; layers[0].opacity = 255; layers[0].hidden = 0;
+ 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;
@@ -88,13 +88,13 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size)
uint8_t got[sizeof px];
uint8_t rws[256];
char name[16];
- size_t rneed, pos = 0, resources = 0, blocks = 0;
+ size_t rneed, pos = 0, rpos = 0, resources = 0, blocks = 0;
if (minipsd_reader_workspace_size(&rneed) != MINIPSD_OK || rneed > sizeof rws
|| minipsd_reader_init(&rd, b.data, b.size, rws, rneed, NULL, &info) != MINIPSD_OK
|| info.width != 3 || info.height != 2 || info.layer_count != 2 || minipsd_reader_needed(rd) != 0)
return -103;
- while (minipsd_reader_next_resource(rd, &res) == MINIPSD_MORE) ++resources;
+ while (minipsd_reader_next_resource(rd, &rpos, &res) == MINIPSD_MORE) ++resources;
if (resources != 1)
return -104;
{ /* the resource the writer wrote, found and parsed; a version info built by hand */
@@ -208,7 +208,7 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size)
minipsd_decode_opts deep;
smoke_buf c;
for (i = 0; i < sizeof px16 / sizeof px16[0]; ++i) px16[i] = (uint16_t)(px[i] * 257u);
- layers[0].rgba = (const uint8_t *)px16;
+ layers[0].pixels = (const uint8_t *)px16;
doc.depth = 16;
c.data = file; c.size = 0; c.capacity = sizeof file;
memset(&deep, 0, sizeof deep);
diff --git a/tests/psd_tools/roundtrip.py b/tests/psd_tools/roundtrip.py
index f517e9f..ece4fb2 100644
--- a/tests/psd_tools/roundtrip.py
+++ b/tests/psd_tools/roundtrip.py
@@ -105,7 +105,7 @@ class BlendingRange(ctypes.Structure):
class Layer(ctypes.Structure):
- _fields_ = [("name", ctypes.c_char_p), ("rgba", ctypes.c_void_p),
+ _fields_ = [("name", ctypes.c_char_p), ("pixels", ctypes.c_void_p),
("opacity", ctypes.c_uint8), ("hidden", ctypes.c_uint8),
("top", ctypes.c_int32), ("left", ctypes.c_int32),
("bottom", ctypes.c_int32), ("right", ctypes.c_int32),
@@ -123,7 +123,8 @@ class Doc(ctypes.Structure):
("composite", ctypes.c_void_p), ("composite_matted", ctypes.c_int),
("resources", ctypes.c_void_p), ("resource_count", ctypes.c_size_t), ("layer_tail", Bytes),
("depth", ctypes.c_uint16), ("format", ctypes.c_uint16),
- ("composite_stored", Bytes), ("composite_channels", ctypes.c_uint16)]
+ ("composite_stored", Bytes), ("composite_channels", ctypes.c_uint16),
+ ("composite_alpha", ctypes.c_uint8), ("color_mode", ctypes.c_uint16), ("color_mode_data", Bytes)]
DEFLATE_FN = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p,
@@ -436,7 +437,7 @@ def pixels(lw, lh, colour):
("Outer", None, SECTION_GROUP, b"\0\0\0\0", None),
]
for i, (name, rgba, kind, blend, m) in enumerate(spec):
- records[i] = Layer(name=name.encode(), rgba=rgba, opacity=255, blend_mode=blend, section_divider_type=kind, mask=m)
+ 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)
out = bytearray()
@@ -498,8 +499,8 @@ def samples(values):
mask_px = [x * 65535 // (lw - 1) for y in range(lh) for x in range(lw)]
mask = Mask(top=5, left=10, bottom=5 + lh, right=10 + lw, pixels=samples(mask_px))
records = (Layer * 2)()
- records[0] = Layer(name=b"Gradient", rgba=samples(base), opacity=255)
- records[1] = Layer(name=b"Masked", rgba=samples(top), opacity=255, top=5, left=10, bottom=5 + lh, right=10 + lw,
+ records[0] = Layer(name=b"Gradient", pixels=samples(base), opacity=255)
+ 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
@@ -555,6 +556,172 @@ def plane(values, k, step):
return errors
+# --- every colour mode the writer takes -----------------------------------------------------------
+
+MODE_GREY, MODE_RGB, MODE_CMYK, MODE_DUOTONE, MODE_LAB = 1, 3, 4, 8, 9
+MODE_COLORS = {MODE_GREY: 1, MODE_RGB: 3, MODE_CMYK: 4, MODE_DUOTONE: 1, MODE_LAB: 3}
+MODES = [(mode, depth, compression) for mode in (MODE_GREY, MODE_CMYK, MODE_LAB, MODE_DUOTONE)
+ for depth, compression in ((8, MINIPSD_RLE), (16, MINIPSD_RLE), (16, MINIPSD_ZIP_PREDICTED))] + [
+ (mode, 32, compression) for mode in (MODE_RGB, MODE_GREY) for compression in (MINIPSD_RLE, MINIPSD_ZIP_PREDICTED)]
+DUOTONE_DATA = bytes(range(40)) # opaque to minipsd and psd-tools: written and read as given
+
+
+def mode_document(lib, path, mode, depth, compression):
+ """A document of the mode and depth: a full-canvas gradient, above it a half-transparent layer at
+ an offset, and a composite of its own (the writer composes RGB only), every sample distinct.
+ Returns the samples, interleaved: the mode's colour channels then transparency."""
+ w, h, lw, lh = 12, 9, 5, 4
+ pixel = MODE_COLORS[mode] + 1
+ full = 1.0 if depth == 32 else (1 << depth) - 1
+ keep = []
+
+ def samples(values):
+ buf = ({8: ctypes.c_uint8, 16: ctypes.c_uint16, 32: ctypes.c_float}[depth] * len(values))(*values)
+ keep.append(buf)
+ return ctypes.cast(buf, ctypes.c_void_p)
+
+ def value(n): # distinct samples; at 32 bits floats in 0 .. 1 that a float holds exactly
+ return n % 256 / 256 if depth == 32 else n * (full // 255) % (full + 1)
+
+ def image(width, height, seed, alpha):
+ return [full if k == pixel - 1 and alpha is None else alpha if k == pixel - 1 else
+ value(seed * 37 + y * 11 + x * 7 + k * 53)
+ for y in range(height) for x in range(width) for k in range(pixel)]
+
+ half = 0.5 if depth == 32 else full // 2
+ base, top, flat = image(w, h, 1, None), image(lw, lh, 2, half), image(w, h, 3, None)
+ records = (Layer * 2)()
+ records[0] = Layer(name=b"Base", pixels=samples(base), opacity=255)
+ 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
+ if mode == MODE_DUOTONE:
+ data = ctypes.create_string_buffer(DUOTONE_DATA, len(DUOTONE_DATA))
+ keep.append(data)
+ doc.color_mode_data = Bytes(ctypes.cast(data, ctypes.c_void_p), len(DUOTONE_DATA))
+ out = bytearray()
+
+ def collect(_user, data, size):
+ out.extend(ctypes.string_at(data, size))
+ return 1
+
+ rc = lib.minipsd_write(ctypes.byref(doc), ctypes.byref(WriteOpts(0, deflate)), WRITE_FN(collect), None)
+ if rc == MINIPSD_OK:
+ path.write_bytes(out)
+ return rc, (w, h, lw, lh, pixel, base, top, flat)
+
+
+def check_mode_document(psd, name, mode, depth, expected):
+ """psd-tools reads the header's mode, depth and channels, the mode data, and every channel of
+ both layers and of the composite, sample for sample: written as given, the composite unmatted."""
+ w, h, lw, lh, pixel, base, top, flat = expected
+ fmt = {8: ">B", 16: ">H", 32: ">f"}[depth]
+ errors = []
+
+ def expect(what, got, want):
+ if got != want:
+ errors.append(f"{name}: {what} is {got!r}, expected {want!r}")
+
+ def plane(values, k):
+ return b"".join(struct.pack(fmt, values[i]) for i in range(k, len(values), pixel))
+
+ header = psd._record.header
+ expect("mode", int(header.color_mode), mode)
+ expect("depth", header.depth, depth)
+ expect("channels", header.channels, pixel)
+ expect("mode data", bytes(psd._record.color_mode_data.value), DUOTONE_DATA if mode == MODE_DUOTONE else b"")
+ lmi = psd._record.layer_and_mask_information
+ li = lmi.layer_info
+ 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"])
+ 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))):
+ for info, data in zip(record.channel_info, channels):
+ k = pixel - 1 if info.id.value == -1 else info.id.value
+ expect(f"'{record.name}' channel {info.id.value}", data.get_data(rw, rh, depth) == plane(values, k), True)
+ merged = psd._record.image_data.get_data(header)
+ for k in range(pixel):
+ expect(f"composite channel {k}", bytes(merged[k]) == plane(flat, k), True)
+ return errors
+
+
+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.
+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)]
+PALETTE = bytes(i * 5 % 256 for i in range(768))
+MULTICHANNELS = 3
+
+
+def flat_document(lib, path, mode, depth, compression):
+ """A document of a flat mode, its composite every sample distinct (bitmap: a pattern, its width
+ not a multiple of eight). Returns the composite's channels as the file stores them, packed at 1
+ bit, big-endian at 16."""
+ w, h = 13, 5
+ sample = max(depth // 8, 1)
+ keep = []
+
+ def buffer(data):
+ buf = ctypes.create_string_buffer(bytes(data), len(data))
+ keep.append(buf)
+ return buf
+
+ doc = Doc(w, h, 72.0, None, 0, None, 0, compression)
+ doc.depth, doc.color_mode = depth, mode
+ if mode == MODE_BITMAP:
+ values = [(x * 3 + y * 5) % 7 * 40 for y in range(h) for x in range(w)] # 0 white, the rest black
+ doc.composite = ctypes.cast(buffer(values), ctypes.c_void_p)
+ rows = [values[y * w:(y + 1) * w] for y in range(h)]
+ want = [b"".join(bytes(sum(1 << (7 - i) for i, v in enumerate(row[x:x + 8]) if v) for x in range(0, w, 8))
+ for row in rows)]
+ elif mode == MODE_INDEXED:
+ values = [(y * w + x) * 7 % 256 for y in range(h) for x in range(w)]
+ doc.composite = ctypes.cast(buffer(values), ctypes.c_void_p)
+ palette = buffer(PALETTE)
+ doc.color_mode_data = Bytes(ctypes.cast(palette, ctypes.c_void_p), len(PALETTE))
+ 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
+ out = bytearray()
+
+ def collect(_user, data, size):
+ out.extend(ctypes.string_at(data, size))
+ return 1
+
+ rc = lib.minipsd_write(ctypes.byref(doc), None, WRITE_FN(collect), None)
+ if rc == MINIPSD_OK:
+ path.write_bytes(out)
+ return rc, want
+
+
+def check_flat_document(psd, name, mode, depth, want):
+ """psd-tools reads the header's mode, depth and channels, the palette, no layers, and the
+ composite's channels as written."""
+ errors = []
+
+ def expect(what, got, wanted):
+ if got != wanted:
+ errors.append(f"{name}: {what} is {got!r}, expected {wanted!r}")
+
+ header = psd._record.header
+ expect("mode", int(header.color_mode), mode)
+ expect("depth", header.depth, depth)
+ expect("channels", header.channels, len(want))
+ expect("mode data", bytes(psd._record.color_mode_data.value), PALETTE if mode == MODE_INDEXED else b"")
+ expect("layers", len(psd), 0)
+ merged = psd._record.image_data.get_data(header)
+ for k, plane in enumerate(want):
+ expect(f"composite channel {k}", bytes(merged[k]), plane)
+ return errors
+
+
# --- files from other programs: read, written back, what is kept compared --------------------------
# Files with text, effects, groups and resources of every kind; read whole, written back as they are.
@@ -568,6 +735,13 @@ def plane(values, k, step):
KEPT_PSB = ["text.psb", "group.psb", "hidden-groups.psb", "layer_mask_data.psb", "unicode_pathname.psb",
"placedLayer.psb", "layer_comps.psb", "masks.psb"]
KEPT_PSB_16 = ["16bit5x5.psb"]
+# Every colour mode and depth: grey, CMYK, Lab, duotone, 32-bit with layers in 'Lr32'; bitmap,
+# indexed and multichannel without layers (multichannel's composite always copied).
+KEPT_MODES = ["colormodes/" + n for n in (
+ "4x4_8bit_grayscale.psd", "4x4_16bit_grayscale.psd", "4x4_32bit_grayscale.psd", "4x4_32bit_rgb.psd",
+ "4x4_8bit_cmyk.psd", "4x4_16bit_cmyk.psd", "4x4_8bit_lab.psd", "4x4_16bit_lab.psd", "4x4_8bit_duotone.psd",
+ "4x4_1bit_bitmap.psd", "100x20_1bit_bitmap_rle.psd", "4x4_8bit_index_color.psd",
+ "4x4_16bit_multichannel.psd")] + ["32bit.psd"]
def resave(lib, source, path, stored=False, fmt=0, zip_=False):
@@ -662,9 +836,10 @@ def blocks(tagged):
def resources(psd):
return {key: resource.tobytes() for key, resource in psd.image_resources.items()}
- def header(psd): # decoded, the composite is written R, G, B and transparency
+ def header(psd): # decoded, the composite is written with transparency, but in a flat mode
h = psd._record.header
- return h.channels if stored else None, h.height, h.width, h.depth, h.color_mode
+ flat = int(h.color_mode) in (MODE_BITMAP, MODE_INDEXED, MODE_MULTICHANNEL)
+ return h.channels if stored or flat else None, h.height, h.width, h.depth, h.color_mode
def rect(rec):
empty = rec.bottom <= rec.top or rec.right <= rec.left
@@ -681,9 +856,16 @@ def mask(lmi):
same("global layer mask", mask(gb), mask(ga))
(rb, cb), (ra, ca) = records(gb), records(ga)
same("layer count", len(rb), len(ra))
- if stored:
- same("composite", before._record.image_data.get_data(before._record.header),
- after._record.image_data.get_data(after._record.header))
+ def composite(psd): # at 1 bit, a row's padding bits are not content: written as zeros
+ h = psd._record.header
+ planes = [bytes(c) for c in psd._record.image_data.get_data(h)]
+ if h.depth != 1 or h.width % 8 == 0:
+ return planes
+ row, keep = (h.width + 7) // 8, (0xFF << (8 - h.width % 8)) & 0xFF
+ return [bytes(v & keep if i % row == row - 1 else v for i, v in enumerate(c)) for c in planes]
+
+ if stored or int(before._record.header.color_mode) in (MODE_BITMAP, MODE_INDEXED, MODE_MULTICHANNEL): # flat: as it was
+ same("composite", composite(before), composite(after))
vb, va, depth = before.version, after.version, before.depth
def data(rec, c, d, version):
@@ -1037,6 +1219,24 @@ def main():
errors += found
print(f"{name}: {'FAILED' if found else 'ok'}")
+ for mode, depth, compression in MODES:
+ name = f"mode-{mode}-{depth}{'-zip' if compression else ''}"
+ path = out / f"{name}.psd"
+ rc, expected = mode_document(lib, path, mode, depth, compression)
+ found = [f"{name}: minipsd_write returned {rc}"] if rc != MINIPSD_OK else (
+ check_mode_document(PSDImage.open(path), name, mode, depth, expected) + check_decoded(lib, path))
+ errors += found
+ print(f"{name}: {'FAILED' if found else 'ok'}")
+
+ for mode, depth, compression in FLAT:
+ name = f"flat-{mode}-{depth}{'-raw' if compression else ''}"
+ path = out / f"{name}.psd"
+ rc, want = flat_document(lib, path, mode, depth, compression)
+ found = [f"{name}: minipsd_write returned {rc}"] if rc != MINIPSD_OK else (
+ check_flat_document(PSDImage.open(path), name, mode, depth, want) + check_decoded(lib, path))
+ errors += found
+ print(f"{name}: {'FAILED' if found else 'ok'}")
+
for source in (out / "groups-masks.psd", out / "deep-16.psd"):
found = check_zip(lib, source, out)
errors += found
@@ -1046,7 +1246,8 @@ def main():
if files is None:
print("kept content: skipped, no psd-tools files (MINIPSD_PSD_TOOLS_FILES)")
# Decoded (ZIP through zlib), and 16-bit ones also as stored.
- kept = [(n, False) for n in KEPT_FILES + KEPT_PSB + KEPT_16 + KEPT_PSB_16] + [(n, True) for n in KEPT_16 + KEPT_PSB_16]
+ kept = [(n, False) for n in KEPT_FILES + KEPT_PSB + KEPT_16 + KEPT_PSB_16 + KEPT_MODES] + [
+ (n, True) for n in KEPT_16 + KEPT_PSB_16 + KEPT_MODES]
# As it is, then in the other format: PSD as PSB, PSB as PSD, every channel stored, so that its
# RLE count table is what changes width.
converted = [((n, True), True) for n in dict.fromkeys(n for n, _ in kept)]
diff --git a/tests/support.h b/tests/support.h
index fe576e4..b96f592 100644
--- a/tests/support.h
+++ b/tests/support.h
@@ -51,7 +51,7 @@ struct Layer {
std::uint8_t opacity = 0, flags = 0;
std::vector channelIds;
std::vector compression; // per channel
- Bytes rgba; // decoded, interleaved; empty for an empty layer
+ Bytes pixels; // decoded, interleaved RGBA; empty for an empty layer
};
struct Psd {
@@ -176,14 +176,14 @@ Psd read(const Bytes &b)
for (int i = 0; i < count; ++i) {
Layer &l = p.layers[i];
const auto w = static_cast(l.right - l.left), h = static_cast(l.bottom - l.top);
- if (w && h) l.rgba.assign(std::size_t(w) * h * 4, 0);
+ if (w && h) l.pixels.assign(std::size_t(w) * h * 4, 0);
for (std::size_t c = 0; c < l.channelIds.size(); ++c) {
const std::size_t start = r.pos();
const int comp = r.u16();
l.compression.push_back(comp);
std::vector counts;
if (comp == 1) for (std::uint32_t y = 0; y < h; ++y) counts.push_back(r.u16());
- if (w && h) readPlane(r, comp, counts, w, h, l.rgba, rgbaOffset(l.channelIds[c]));
+ if (w && h) readPlane(r, comp, counts, w, h, l.pixels, rgbaOffset(l.channelIds[c]));
if (r.pos() - start != lengths[i][c]) throw std::runtime_error("channel length does not match its record");
}
}
diff --git a/tests/test_composite.cpp b/tests/test_composite.cpp
index 2168772..5f373df 100644
--- a/tests/test_composite.cpp
+++ b/tests/test_composite.cpp
@@ -95,12 +95,12 @@ TEST(MinipsdCompositeTest, ALayerBlendsAtItsRectangleClippedToTheCanvas)
// A 2 x 2 layer at x 3, y -1 on a 4 x 3 canvas: only its bottom-left pixel lands, at (3, 0).
const Bytes px = {1, 2, 3, 255, 4, 5, 6, 255,
7, 8, 9, 255, 10, 11, 12, 255};
- const minipsd_layer edge{.name = "Edge", .rgba = px.data(), .opacity = 255, .top = -1, .left = 3, .bottom = 1, .right = 5};
+ const minipsd_layer edge{.name = "Edge", .pixels = px.data(), .opacity = 255, .top = -1, .left = 3, .bottom = 1, .right = 5};
Bytes want(std::size_t(4) * 3 * 4, 0);
std::copy(px.begin() + 8, px.begin() + 12, want.begin() + std::ptrdiff_t(3) * 4);
EXPECT_EQ(composite(4, 3, &edge, 1), matted(want));
- const minipsd_layer outside{.name = "Out", .rgba = px.data(), .opacity = 255, .top = 3, .left = -2, .bottom = 5, .right = 0};
+ const minipsd_layer outside{.name = "Out", .pixels = px.data(), .opacity = 255, .top = 3, .left = -2, .bottom = 5, .right = 0};
EXPECT_EQ(composite(4, 3, &outside, 1), matted(Bytes(std::size_t(4) * 3 * 4, 0))) << "wholly off the canvas: nothing";
}
@@ -133,8 +133,8 @@ TEST(MinipsdCompositeTest, UnmattingUndoesTheMatteWithinItsRounding)
TEST(MinipsdCompositeTest, SixteenBitLayersComposeAndMatte)
{
const std::uint16_t red[4] = {65535, 0, 0, 65535}, blue[4] = {0, 0, 65535, 32768};
- const minipsd_layer layers[2] = {{.name = "Red", .rgba = reinterpret_cast(red), .opacity = 255},
- {.name = "Blue", .rgba = reinterpret_cast(blue), .opacity = 255}};
+ const minipsd_layer layers[2] = {{.name = "Red", .pixels = reinterpret_cast(red), .opacity = 255},
+ {.name = "Blue", .pixels = reinterpret_cast(blue), .opacity = 255}};
minipsd_doc d = docOf(1, 1, layers, 2);
d.depth = 16;
std::uint16_t buffer[4];
@@ -148,7 +148,7 @@ TEST(MinipsdCompositeTest, SixteenBitLayersComposeAndMatte)
EXPECT_EQ(got[3], 65535);
const std::uint16_t half[4] = {0, 65535, 0, 32768}; // over white: 65535 * (1 - a) + c * a
- const minipsd_layer one{.name = "Half", .rgba = reinterpret_cast(half), .opacity = 255};
+ const minipsd_layer one{.name = "Half", .pixels = reinterpret_cast(half), .opacity = 255};
d = docOf(1, 1, &one, 1);
d.depth = 16;
std::memcpy(got, mpsd__composite_row(&c, 0), sizeof got);
diff --git a/tests/test_document.cpp b/tests/test_document.cpp
index 2fb8d0c..66a29dd 100644
--- a/tests/test_document.cpp
+++ b/tests/test_document.cpp
@@ -26,9 +26,9 @@ TEST(MinipsdDocumentTest, AcceptsAPlainDocument)
// A rectangle may lie anywhere, on the canvas or off it; an empty layer's has no area.
const minipsd_layer anywhere[3] = {
- {.name = "Off", .rgba = px.data(), .opacity = 255, .top = -2147483647 - 1, .left = 9, .bottom = -2147483647 + 1, .right = 11},
- {.name = "Edge", .rgba = px.data(), .opacity = 255, .top = 1, .left = 1, .bottom = 3, .right = 3},
- {.name = "Empty", .rgba = nullptr, .opacity = 255, .top = 5, .left = 5, .bottom = 5, .right = 9}};
+ {.name = "Off", .pixels = px.data(), .opacity = 255, .top = -2147483647 - 1, .left = 9, .bottom = -2147483647 + 1, .right = 11},
+ {.name = "Edge", .pixels = px.data(), .opacity = 255, .top = 1, .left = 1, .bottom = 3, .right = 3},
+ {.name = "Empty", .pixels = nullptr, .opacity = 255, .top = 5, .left = 5, .bottom = 5, .right = 9}};
const minipsd_doc r = docOf(2, 2, anywhere, 3);
EXPECT_EQ(minipsd_doc_check(&r, nullptr), MINIPSD_OK);
}
@@ -53,6 +53,62 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments)
}
d = docOf(2, 2, &layer, 1); d.icc_size = 4;
EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "ICC size without data";
+ // The colour mode: 0 and RGB are RGB; grey, CMYK, Lab and duotone bring their composite, which
+ // only RGB's writer can compose; duotone, and only it of these, its mode data.
+ const std::uint8_t inks[3] = {1, 2, 3};
+ const Bytes cmyk(std::size_t{2} * 2 * 5, 0);
+ d = docOf(2, 2, &layer, 1); d.color_mode = MINIPSD_MODE_RGB;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "RGB named";
+ d.color_mode = MINIPSD_MODE_CMYK;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "CMYK without its composite";
+ d.composite = cmyk.data();
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "CMYK with it";
+ d.color_mode = MINIPSD_MODE_DUOTONE;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "duotone without its inks";
+ d.color_mode_data = {inks, sizeof inks};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "duotone with them";
+ d.color_mode = MINIPSD_MODE_GREY;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "mode data for grey";
+ d.color_mode = 5;
+ d.color_mode_data = {};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "no such mode";
+ // The flat modes: no layers, no transparency. Indexed brings its palette and is 8-bit; bitmap
+ // is 1-bit, the only 1-bit mode; multichannel's composite is copied, never supplied.
+ const Bytes palette(768, 0);
+ const Bytes stored = {0, 0, 1, 2, 3, 4, 5, 6, 7, 8}; // raw, two channels of 2 x 2
+ d = docOf(2, 2, nullptr, 0); d.composite = cmyk.data(); d.color_mode = MINIPSD_MODE_INDEXED;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "indexed without its palette";
+ d.color_mode_data = {palette.data(), palette.size()};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "indexed with it";
+ d.depth = 16;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_UNSUPPORTED) << "16-bit indexed";
+ d.depth = 8; d.layers = &layer; d.layer_count = 1;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "indexed with layers";
+ d = docOf(2, 2, nullptr, 0); d.composite = cmyk.data(); d.depth = 1;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "1 bit: bitmap";
+ d.color_mode = MINIPSD_MODE_GREY;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "1-bit grey";
+ d = docOf(2, 2, nullptr, 0); d.color_mode = MINIPSD_MODE_MULTICHANNEL; d.composite_channels = 2;
+ d.composite_stored = {stored.data(), stored.size()};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "multichannel, its composite stored";
+ d.composite_alpha = 1;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "multichannel with transparency";
+ d.composite_alpha = 0; d.composite = cmyk.data();
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "multichannel supplied";
+ // 32 bits: RGB and grey, as Photoshop has them, with their composite (the writer composes none).
+ d = docOf(2, 2, &layer, 1); d.depth = 32;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "32-bit RGB without its composite";
+ d.composite = cmyk.data();
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "32-bit RGB with it";
+ d.color_mode_data = {inks, sizeof inks};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "32-bit mode data (HDR toning), as given";
+ d.color_mode_data = {};
+ d.color_mode = MINIPSD_MODE_CMYK;
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_UNSUPPORTED) << "32-bit CMYK";
+ d = docOf(2, 2, &layer, 1); d.color_mode_data = {inks, sizeof inks};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "mode data for RGB";
+ d.color_mode_data = {nullptr, 3};
+ EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "mode data size without data";
for (const auto &[w, h] : {std::pair{0, 2}, {2, 0},
{MINIPSD_MAX_SIDE + 1, 2}, {2, MINIPSD_MAX_SIDE + 1}}) {
d = docOf(w, h, &layer, 1);
@@ -60,13 +116,13 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments)
}
// Rectangles: inverted, an area without pixels, a side PSD's 16-bit row counts cannot hold.
- struct Case { std::int32_t top, left, bottom, right; const std::uint8_t *rgba; int rc; };
+ struct Case { std::int32_t top, left, bottom, right; const std::uint8_t *pixels; int rc; };
for (const Case &c : {Case{2, 0, 1, 2, px.data(), MINIPSD_ERR_ARG}, Case{0, 2, 2, 1, px.data(), MINIPSD_ERR_ARG},
Case{0, 0, 2, 2, nullptr, MINIPSD_ERR_ARG},
Case{0, 0, 1, MINIPSD_MAX_SIDE + 1, px.data(), MINIPSD_ERR_SIZE},
Case{-MINIPSD_MAX_SIDE, 0, 1, 1, px.data(), MINIPSD_ERR_SIZE},
Case{-2147483647 - 1, 0, 2147483647, 1, px.data(), MINIPSD_ERR_SIZE}}) {
- const minipsd_layer bad{.name = "R", .rgba = c.rgba, .opacity = 255,
+ const minipsd_layer bad{.name = "R", .pixels = c.pixels, .opacity = 255,
.top = c.top, .left = c.left, .bottom = c.bottom, .right = c.right};
d = docOf(2, 2, &bad, 1);
EXPECT_EQ(minipsd_doc_check(&d, nullptr), c.rc) << c.top << ", " << c.left << ", " << c.bottom << ", " << c.right;
@@ -86,7 +142,7 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments)
d = docOf(2, 2, &layer, 1); d.layer_tail = {nullptr, 4};
EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "a layer tail size without data";
for (const minipsd_bytes extra : {minipsd_bytes{nullptr, 12}, minipsd_bytes{one, 1}}) {
- const minipsd_layer bad{.name = "L", .rgba = px.data(), .opacity = 255, .additional_info = extra};
+ const minipsd_layer bad{.name = "L", .pixels = px.data(), .opacity = 255, .additional_info = extra};
d = docOf(2, 2, &bad, 1);
EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "extra of " << extra.size << " bytes";
}
@@ -98,7 +154,7 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments)
static minipsd_bytes four[4];
static minipsd_layer l;
for (minipsd_bytes &c : four) c = {ch.data(), ch.size()};
- l = {.name = "S", .rgba = rgba, .opacity = 255, .top = 0, .left = 0, .bottom = 2, .right = 2, .stored = four};
+ l = {.name = "S", .pixels = rgba, .opacity = 255, .top = 0, .left = 0, .bottom = 2, .right = 2, .stored = four};
minipsd_doc s = docOf(2, 2, &l, 1);
s.composite = composite;
return minipsd_doc_check(&s, nullptr);
@@ -114,7 +170,7 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments)
// Groups nest bottom first: a divider, the layers, the top record.
auto groups = [&](std::initializer_list kinds) {
std::vector ls;
- for (std::uint8_t k : kinds) ls.push_back({.name = "G", .rgba = nullptr, .opacity = 255, .section_divider_type = k});
+ for (std::uint8_t k : kinds) ls.push_back({.name = "G", .pixels = nullptr, .opacity = 255, .section_divider_type = k});
const minipsd_doc g = docOf(2, 2, ls.data(), ls.size());
return minipsd_doc_check(&g, nullptr);
};
@@ -127,7 +183,7 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments)
// Masks: a rectangle not inverted, pixels for an area, a side PSD can count.
auto masked = [&](const minipsd_mask &m) {
- const minipsd_layer ml{.name = "M", .rgba = px.data(), .opacity = 255, .mask = &m};
+ const minipsd_layer ml{.name = "M", .pixels = px.data(), .opacity = 255, .mask = &m};
const minipsd_doc md = docOf(2, 2, &ml, 1);
return minipsd_doc_check(&md, nullptr);
};
diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp
index cef955e..215b102 100644
--- a/tests/test_reader.cpp
+++ b/tests/test_reader.cpp
@@ -501,22 +501,30 @@ TEST(MinipsdReaderTest, ResourcesAreListedInFileOrderWithTheirBytes)
ASSERT_EQ(o.rc, MINIPSD_OK);
minipsd_resource res;
- ASSERT_EQ(minipsd_reader_next_resource(o.r, &res), MINIPSD_MORE);
+ std::size_t pos = 0;
+ ASSERT_EQ(minipsd_reader_next_resource(o.r, &pos, &res), MINIPSD_MORE);
EXPECT_EQ(res.id, 1005);
EXPECT_EQ(res.data.size, 16u);
EXPECT_TRUE(inside(o.blob, res.data));
- ASSERT_EQ(minipsd_reader_next_resource(o.r, &res), MINIPSD_MORE);
+ const std::size_t second = pos;
+ ASSERT_EQ(minipsd_reader_next_resource(o.r, &pos, &res), MINIPSD_MORE);
EXPECT_EQ(res.id, 1039);
EXPECT_EQ(Bytes(res.data.data, res.data.data + res.data.size), icc) << "without the pad byte";
res.id = 7777;
- EXPECT_EQ(minipsd_reader_next_resource(o.r, &res), MINIPSD_OK);
+ EXPECT_EQ(minipsd_reader_next_resource(o.r, &pos, &res), MINIPSD_OK);
EXPECT_EQ(res.id, 7777) << "untouched at the end";
- // init again: the list starts over.
- Open again(file);
- ASSERT_EQ(again.rc, MINIPSD_OK);
- EXPECT_EQ(minipsd_reader_next_resource(again.r, &res), MINIPSD_MORE);
+ // The cursor is the caller's: the list starts over from 0, or from any place it gave, as often
+ // as asked; a place it did not give is refused.
+ std::size_t again = 0;
+ EXPECT_EQ(minipsd_reader_next_resource(o.r, &again, &res), MINIPSD_MORE);
EXPECT_EQ(res.id, 1005);
+ again = second;
+ EXPECT_EQ(minipsd_reader_next_resource(o.r, &again, &res), MINIPSD_MORE);
+ EXPECT_EQ(res.id, 1039);
+ again = 1;
+ EXPECT_EQ(minipsd_reader_next_resource(o.r, &again, &res), MINIPSD_ERR_FORMAT) << "inside a resource";
+ EXPECT_EQ(again, 1u) << "the cursor stays";
}
TEST(MinipsdReaderTest, ResourcesWithNamesAndOddSizesAreWalkedCorrectly)
@@ -577,9 +585,10 @@ TEST(MinipsdReaderTest, ResourcesWithNamesAndOddSizesAreWalkedCorrectly)
const std::tuple expected[] = {
{1036, 7, "ab"}, {2000, 4, "abc"}, {1005, 16, ""}, {1039, 3, ""}, {1005, 16, ""}, {1039, 9, ""}};
+ std::size_t pos = 0;
for (const auto &[id, size, name] : expected) {
minipsd_resource res;
- ASSERT_EQ(minipsd_reader_next_resource(o.r, &res), MINIPSD_MORE);
+ ASSERT_EQ(minipsd_reader_next_resource(o.r, &pos, &res), MINIPSD_MORE);
EXPECT_EQ(res.id, id);
EXPECT_EQ(res.data.size, size);
EXPECT_EQ(std::string(reinterpret_cast(res.name.data), res.name.size), name);
@@ -596,7 +605,8 @@ TEST(MinipsdReaderTest, AFileWithoutResourcesHasNoDpiAndNoProfile)
EXPECT_EQ(o.info.dpi, 0.0);
EXPECT_EQ(o.info.icc.size, 0u);
minipsd_resource res;
- EXPECT_EQ(minipsd_reader_next_resource(o.r, &res), MINIPSD_OK);
+ std::size_t pos = 0;
+ EXPECT_EQ(minipsd_reader_next_resource(o.r, &pos, &res), MINIPSD_OK);
EXPECT_EQ(minipsd_reader_find_resource(o.r, MINIPSD_RES_RESOLUTION_INFO, &res), MINIPSD_ERR_NOT_FOUND);
}
@@ -1260,14 +1270,16 @@ TEST(MinipsdReaderTest, ACallOnAReaderThatNeverStartedIsRefused)
minipsd_layer_view v;
minipsd_resource res;
EXPECT_EQ(minipsd_reader_next_layer(cut.r, &v), MINIPSD_ERR_ARG);
- EXPECT_EQ(minipsd_reader_next_resource(cut.r, &res), MINIPSD_ERR_ARG);
+ std::size_t pos = 0;
+ EXPECT_EQ(minipsd_reader_next_resource(cut.r, &pos, &res), MINIPSD_ERR_ARG);
EXPECT_EQ(minipsd_reader_find_resource(cut.r, MINIPSD_RES_RESOLUTION_INFO, &res), MINIPSD_ERR_ARG);
Open ok(f.bytes);
EXPECT_EQ(minipsd_reader_next_layer(nullptr, &v), MINIPSD_ERR_ARG);
EXPECT_EQ(minipsd_reader_next_layer(ok.r, nullptr), MINIPSD_ERR_ARG);
- EXPECT_EQ(minipsd_reader_next_resource(nullptr, &res), MINIPSD_ERR_ARG);
- EXPECT_EQ(minipsd_reader_next_resource(ok.r, nullptr), MINIPSD_ERR_ARG);
+ EXPECT_EQ(minipsd_reader_next_resource(nullptr, &pos, &res), MINIPSD_ERR_ARG);
+ EXPECT_EQ(minipsd_reader_next_resource(ok.r, nullptr, &res), MINIPSD_ERR_ARG);
+ EXPECT_EQ(minipsd_reader_next_resource(ok.r, &pos, nullptr), MINIPSD_ERR_ARG);
EXPECT_EQ(minipsd_reader_find_resource(nullptr, MINIPSD_RES_RESOLUTION_INFO, &res), MINIPSD_ERR_ARG);
EXPECT_EQ(minipsd_reader_find_resource(ok.r, MINIPSD_RES_RESOLUTION_INFO, nullptr), MINIPSD_ERR_ARG);
}
@@ -1309,11 +1321,11 @@ Chan zipChan(int id, const std::vector &samples, std::uint32_t w, bool
}
// A 3 x 2 layer, every channel ZIP, two of them predicted, and a ZIP mask the size of the layer
-// holding the red samples; values that wrap when predicted. rgba is what it decodes to.
+// holding the red samples; values that wrap when predicted. pixels is what it decodes to.
struct ZipLayer {
static constexpr std::uint32_t w = 3, h = 2;
Rec rec;
- Bytes rgba, red; // interleaved RGBA and the mask, samples in host order
+ Bytes pixels, red; // interleaved RGBA and the mask, samples in host order
explicit ZipLayer(int depth)
{
@@ -1332,7 +1344,7 @@ struct ZipLayer {
else out.push_back(static_cast(v));
};
for (std::size_t i = 0; i < std::size_t(w) * h; ++i) {
- for (const auto &c : s) put(rgba, c[i]);
+ for (const auto &c : s) put(pixels, c[i]);
put(red, s[0][i]);
}
}
@@ -1725,12 +1737,12 @@ TEST(MinipsdReaderTest, ZipChannelsDecodeThroughTheCallersInflate)
const std::size_t bytes = depth / 8;
const ZipLayer z(depth);
OneLayer l(z.rec, static_cast(depth));
- Bytes scratch(std::size_t(w) * h * bytes), out(z.rgba.size(), 0xEE);
+ Bytes scratch(std::size_t(w) * h * bytes), out(z.pixels.size(), 0xEE);
int calls = 0;
minipsd_decode_opts opts{0, 0, 0, depth, countingInflate, &calls, scratch.data(), scratch.size()};
minipsd_target t = targetOf(out, w, depth);
ASSERT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_OK);
- EXPECT_EQ(out, z.rgba) << "prediction undone, 16-bit samples in host order";
+ EXPECT_EQ(out, z.pixels) << "prediction undone, 16-bit samples in host order";
EXPECT_EQ(calls, 4) << "one channel at a time, through the user pointer";
// A band: the whole channel is inflated, its rows taken from there.
@@ -1738,13 +1750,13 @@ TEST(MinipsdReaderTest, ZipChannelsDecodeThroughTheCallersInflate)
t = targetOf(band, w, depth);
opts.y_first = opts.y_count = 1;
ASSERT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_OK);
- EXPECT_EQ(band, Bytes(z.rgba.begin() + std::ptrdiff_t(band.size()), z.rgba.end()));
+ EXPECT_EQ(band, Bytes(z.pixels.begin() + std::ptrdiff_t(band.size()), z.pixels.end()));
// Bands of one row over a scratch of every channel: the first inflates them, each in its
// own part, the next take them from there.
opts.scratch_kept = 1;
EXPECT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_ERR_ARG) << "kept, but one channel's room shares one part";
- Bytes all(scratch.size() * 4), bands(z.rgba.size(), 0xEE);
+ Bytes all(scratch.size() * 4), bands(z.pixels.size(), 0xEE);
opts.scratch = all.data();
opts.scratch_size = all.size();
calls = 0;
@@ -1755,7 +1767,7 @@ TEST(MinipsdReaderTest, ZipChannelsDecodeThroughTheCallersInflate)
ASSERT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_OK);
std::copy(band.begin(), band.end(), bands.begin() + std::ptrdiff_t(y * band.size()));
}
- EXPECT_EQ(bands, z.rgba);
+ EXPECT_EQ(bands, z.pixels);
EXPECT_EQ(calls, 4) << "one inflate a channel, not one a band";
opts.scratch = scratch.data();
@@ -1776,7 +1788,7 @@ TEST(MinipsdReaderTest, ZipChannelsDecodeThroughTheCallersInflate)
opts.scratch_kept = 0;
// Refused before a sample is written: no room to inflate into.
- Bytes untouched(z.rgba.size(), 0xEE);
+ Bytes untouched(z.pixels.size(), 0xEE);
t = targetOf(untouched, w, depth);
opts.scratch_size = scratch.size() - 1;
EXPECT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_ERR_ARG);
@@ -1784,7 +1796,7 @@ TEST(MinipsdReaderTest, ZipChannelsDecodeThroughTheCallersInflate)
opts.scratch_size = scratch.size();
opts.scratch = nullptr;
EXPECT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_ERR_ARG);
- EXPECT_EQ(untouched, Bytes(z.rgba.size(), 0xEE));
+ EXPECT_EQ(untouched, Bytes(z.pixels.size(), 0xEE));
// A stream that does not inflate, or not to the channel's size, is malformed.
opts.scratch = scratch.data();
@@ -2195,9 +2207,9 @@ TEST(MinipsdReadTest, AFileBecomesADocumentThatStandsAlone)
{
const Bytes px = testImage(5, 3), icc = {1, 2, 3, 4, 5};
const minipsd_layer layers[3] = {
- {.name = "\xC5\xBB\xC3\xB3\xC5\x82w", .rgba = px.data(), .opacity = 128, .hidden = 1, .top = -1, .left = 4, .bottom = 2, .right = 9},
- {.name = "Empty", .rgba = nullptr, .opacity = 255, .top = 3, .left = 3, .bottom = 3, .right = 6},
- {.name = nullptr, .rgba = px.data(), .opacity = 255, .top = 0, .left = 0, .bottom = 3, .right = 5}};
+ {.name = "\xC5\xBB\xC3\xB3\xC5\x82w", .pixels = px.data(), .opacity = 128, .hidden = 1, .top = -1, .left = 4, .bottom = 2, .right = 9},
+ {.name = "Empty", .pixels = nullptr, .opacity = 255, .top = 3, .left = 3, .bottom = 3, .right = 6},
+ {.name = nullptr, .pixels = px.data(), .opacity = 255, .top = 0, .left = 0, .bottom = 3, .right = 5}};
minipsd_doc d = docOf(7, 4, layers, 3);
d.dpi = 72.5; d.icc = icc.data(); d.icc_size = icc.size(); d.compression = MINIPSD_RAW;
const Bytes original = write(d);
@@ -2221,6 +2233,8 @@ TEST(MinipsdReadTest, AFileBecomesADocumentThatStandsAlone)
EXPECT_EQ(r->height, 4u);
EXPECT_EQ(r->dpi, 72.5);
EXPECT_EQ(Bytes(r->icc, r->icc + r->icc_size), icc);
+ EXPECT_EQ(r->color_mode, MINIPSD_MODE_RGB) << "the file's mode, named";
+ EXPECT_EQ(r->color_mode_data.size, 0u);
EXPECT_EQ(r->compression, MINIPSD_RAW);
ASSERT_EQ(r->layer_count, 3u);
EXPECT_STREQ(r->layers[0].name, layers[0].name);
@@ -2231,8 +2245,8 @@ TEST(MinipsdReadTest, AFileBecomesADocumentThatStandsAlone)
EXPECT_EQ(a.opacity, b.opacity);
EXPECT_EQ(a.hidden, b.hidden);
EXPECT_EQ(std::tie(a.top, a.left, a.bottom, a.right), std::tie(b.top, b.left, b.bottom, b.right));
- EXPECT_EQ(a.rgba == nullptr, b.rgba == nullptr);
- if (a.rgba) { EXPECT_EQ(Bytes(a.rgba, a.rgba + px.size()), px); }
+ EXPECT_EQ(a.pixels == nullptr, b.pixels == nullptr);
+ if (a.pixels) { EXPECT_EQ(Bytes(a.pixels, a.pixels + px.size()), px); }
}
EXPECT_EQ(r->composite_matted, 1);
EXPECT_EQ(Bytes(r->composite, r->composite + std::ptrdiff_t(7) * 4 * 4), read(original).composite) << "as stored";
@@ -2258,11 +2272,20 @@ TEST(MinipsdReadTest, ItRefusesWhatTheReaderRefusesAndWhatItCannotHold)
lim.max_decoded_bytes += 1;
readDoc(file, &lost, MINIPSD_OK, &lim);
+ // A mode and depth the writer could not take back: 32-bit CMYK; layers in a flat mode (indexed).
Spec s;
s.depth = 32;
+ s.mode = MINIPSD_MODE_CMYK;
+ s.channels = 5;
s.layers = {simpleRec()};
s.count = -1;
readDoc(build(s).bytes, &lost, MINIPSD_ERR_UNSUPPORTED);
+ s.depth = 8;
+ s.mode = MINIPSD_MODE_INDEXED;
+ s.channels = 1;
+ s.colorModeData = Bytes(768, 0);
+ 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}) {
@@ -2347,7 +2370,7 @@ TEST(MinipsdReadTest, ZipLayersAndMasksDecodeThroughTheCallersInflate)
const minipsd_read_opts zip{0, zlib_stored_inflate, nullptr};
const Doc d = readDoc(file, nullptr, MINIPSD_OK, nullptr, &zip);
ASSERT_TRUE(d);
- EXPECT_EQ(Bytes(d->layers[0].rgba, d->layers[0].rgba + z.rgba.size()), z.rgba);
+ EXPECT_EQ(Bytes(d->layers[0].pixels, d->layers[0].pixels + z.pixels.size()), z.pixels);
ASSERT_TRUE(d->layers[0].mask);
EXPECT_EQ(Bytes(d->layers[0].mask->pixels, d->layers[0].mask->pixels + z.red.size()), z.red);
@@ -2369,7 +2392,7 @@ TEST(MinipsdReadTest, ZipLayersAndMasksDecodeThroughTheCallersInflate)
s.layers[0].mask.clear();
const Doc m = readDoc(build(s).bytes, nullptr, MINIPSD_OK, nullptr, &zip);
ASSERT_TRUE(m);
- EXPECT_EQ(Bytes(m->layers[0].rgba, m->layers[0].rgba + z.rgba.size()), z.rgba);
+ EXPECT_EQ(Bytes(m->layers[0].pixels, m->layers[0].pixels + z.pixels.size()), z.pixels);
}
// Photoshop's empty fill layer, its rectangle inverted, in a file that ends the layer and mask
@@ -2477,7 +2500,7 @@ TEST(MinipsdReadTest, AFlatFileBecomesOneBackgroundLayer)
EXPECT_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].rgba, d->layers[0].rgba + 16), (Bytes{0x40, 0x40, 0x40, 255, 0x40, 0x40, 0x40, 255,
+ 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(minipsd_doc_check(d.get(), nullptr), MINIPSD_OK) << "the writer takes it as it is";
@@ -2517,11 +2540,50 @@ TEST(MinipsdReadTest, AFlat16BitFileIsOpaqueAt65535)
ASSERT_TRUE(d);
EXPECT_EQ(d->depth, 16);
std::uint16_t px[4];
- std::memcpy(px, d->layers[0].rgba, sizeof px);
+ std::memcpy(px, d->layers[0].pixels, sizeof px);
EXPECT_EQ(px[0], 0x4040);
EXPECT_EQ(px[3], 65535);
}
+// Modes beyond 8- and 16-bit RGB come back as written: a 32-bit document's layers in 'Lr32', its
+// float composite matted on the way out (c * a + 1 - a); a bitmap's bytes packed eight to a byte,
+// a width that is not a multiple of eight. Written back, the same content; a second pass, the
+// same bytes.
+TEST(MinipsdReadTest, EveryModeComesBackAsWritten)
+{
+ const std::vector layer = {0.25f, 0.5f, 0.75f, 1.0f, 0.0f, 1.0f, 0.5f, 0.5f}; // 2 x 1, R G B A
+ const minipsd_layer deep{.name = "Deep", .pixels = reinterpret_cast(layer.data()), .opacity = 255};
+ minipsd_doc d = docOf(2, 1, &deep, 1);
+ d.depth = 32;
+ d.composite = deep.pixels;
+ const Bytes floats = write(d);
+ const Doc f = readDoc(floats, nullptr);
+ ASSERT_TRUE(f);
+ EXPECT_EQ(f->depth, 32);
+ ASSERT_EQ(f->layer_count, 1u);
+ EXPECT_EQ(std::memcmp(f->layers[0].pixels, layer.data(), layer.size() * sizeof(float)), 0);
+ float composite[8];
+ std::memcpy(composite, f->composite, sizeof composite);
+ EXPECT_EQ(composite[0], 0.25f) << "opaque: as given";
+ EXPECT_EQ(composite[4], 0.5f) << "half transparent black over white";
+ const Bytes again = write(*f); // the layers now in the kept 'Lr32'
+ const Doc g = readDoc(again, nullptr);
+ ASSERT_TRUE(g);
+ EXPECT_EQ(write(*g), again) << "a second pass changes no byte";
+
+ 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;
+ b.composite = bits.data();
+ const Bytes bitmap = write(b);
+ const Doc r = readDoc(bitmap, nullptr);
+ ASSERT_TRUE(r);
+ EXPECT_EQ(r->color_mode, MINIPSD_MODE_BITMAP);
+ EXPECT_EQ(r->layer_count, 0u) << "a flat mode: no Background layer";
+ EXPECT_EQ(Bytes(r->composite, r->composite + bits.size()), (Bytes{0, 1, 0, 0, 1, 0, 0, 0, 1, 1}));
+ EXPECT_EQ(write(*r), bitmap);
+}
+
// Layers kept in a global block, as Photoshop keeps them at 16 and 32 bits: found after the global
// layer mask, behind other blocks, each padded to four past its length; the walk stops quietly at
// what is not a block. A document read from them leaves that block out of its tail.
@@ -2881,7 +2943,8 @@ void expectKeptAlike(const Bytes &before, const Bytes &after)
auto resources = [](minipsd_reader *r) {
std::vector> out;
minipsd_resource res;
- while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE)
+ std::size_t pos = 0;
+ while (minipsd_reader_next_resource(r, &pos, &res) == MINIPSD_MORE)
if (res.id != 1005 && res.id != 1039)
out.emplace_back(res.id, Bytes(res.data.data, res.data.data + res.data.size),
Bytes(res.name.data, res.name.data + res.name.size));
@@ -3104,8 +3167,8 @@ TEST(MinipsdReaderTest, FilesFromOtherProgramsAreReadWholeAndInParts)
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.rgba == nullptr, b.rgba == nullptr);
- if (a.rgba && b.rgba) { EXPECT_TRUE(std::equal(a.rgba, a.rgba + n, b.rgba)); }
+ 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_TRUE(std::equal(d->composite, d->composite + composite.size(), e->composite));
expectKeptAlike(file, again);
@@ -3124,8 +3187,8 @@ TEST(MinipsdReaderTest, FilesFromOtherProgramsAreReadWholeAndInParts)
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.rgba == nullptr, b.rgba == nullptr);
- if (a.rgba && b.rgba) { EXPECT_TRUE(std::equal(a.rgba, a.rgba + n, b.rgba)); }
+ EXPECT_EQ(a.pixels == nullptr, b.pixels == nullptr);
+ if (a.pixels && b.pixels) { EXPECT_TRUE(std::equal(a.pixels, a.pixels + n, b.pixels)); }
}
}
diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp
index 3f8fe9c..d9b9ddd 100644
--- a/tests/test_writer.cpp
+++ b/tests/test_writer.cpp
@@ -58,7 +58,7 @@ TEST(MinipsdWriterTest, OneLayerRoundTripsThroughRle)
EXPECT_EQ(l.flags, 0);
EXPECT_EQ(l.pascalName, "Template");
EXPECT_EQ(l.unicodeName, u"Template");
- EXPECT_EQ(l.rgba, px);
+ EXPECT_EQ(l.pixels, px);
EXPECT_EQ(p.compositeCompression, 1);
EXPECT_EQ(p.composite, matted(px)) << "one layer at full opacity: the composite is the layer, over white";
}
@@ -73,7 +73,7 @@ TEST(MinipsdWriterTest, RawCompressionRoundTrips)
const Psd p = read(write(d));
EXPECT_EQ(p.layers[0].compression, (std::vector{0, 0, 0, 0}));
EXPECT_EQ(p.compositeCompression, 0);
- EXPECT_EQ(p.layers[0].rgba, px) << "width " << w;
+ EXPECT_EQ(p.layers[0].pixels, px) << "width " << w;
EXPECT_EQ(p.composite, matted(px)) << "width " << w;
}
}
@@ -88,7 +88,7 @@ TEST(MinipsdWriterTest, EmptyLayerHasNoPixelsAndLeavesTheComposite)
EXPECT_EQ(p.layers[1].pascalName, "Layer");
EXPECT_EQ(p.layers[1].bottom, 0);
EXPECT_EQ(p.layers[1].right, 0);
- EXPECT_TRUE(p.layers[1].rgba.empty());
+ EXPECT_TRUE(p.layers[1].pixels.empty());
EXPECT_EQ(p.composite, matted(px));
}
@@ -98,10 +98,10 @@ TEST(MinipsdWriterTest, ALayerIsWrittenAtItsRectangle)
// rectangle has no area: each record keeps its rectangle, and its channels its own pixels.
const Bytes small = testImage(3, 2), edge = testImage(4, 5), outside = testImage(2, 2);
const minipsd_layer layers[4] = {
- {.name = "Small", .rgba = small.data(), .opacity = 255, .top = 1, .left = 2, .bottom = 3, .right = 5},
- {.name = "Edge", .rgba = edge.data(), .opacity = 255, .top = -2, .left = 6, .bottom = 3, .right = 10},
- {.name = "Outside", .rgba = outside.data(), .opacity = 255, .top = 9, .left = -5, .bottom = 11, .right = -3},
- {.name = "Empty", .rgba = nullptr, .opacity = 255, .top = 4, .left = 4, .bottom = 4, .right = 7}};
+ {.name = "Small", .pixels = small.data(), .opacity = 255, .top = 1, .left = 2, .bottom = 3, .right = 5},
+ {.name = "Edge", .pixels = edge.data(), .opacity = 255, .top = -2, .left = 6, .bottom = 3, .right = 10},
+ {.name = "Outside", .pixels = outside.data(), .opacity = 255, .top = 9, .left = -5, .bottom = 11, .right = -3},
+ {.name = "Empty", .pixels = nullptr, .opacity = 255, .top = 4, .left = 4, .bottom = 4, .right = 7}};
for (int compression : {MINIPSD_RLE, MINIPSD_RAW}) {
minipsd_doc d = docOf(8, 4, layers, 4);
d.compression = compression;
@@ -115,10 +115,10 @@ TEST(MinipsdWriterTest, ALayerIsWrittenAtItsRectangle)
EXPECT_EQ(p.layers[i].bottom, l.bottom);
EXPECT_EQ(p.layers[i].right, l.right);
}
- EXPECT_EQ(p.layers[0].rgba, small);
- EXPECT_EQ(p.layers[1].rgba, edge);
- EXPECT_EQ(p.layers[2].rgba, outside);
- EXPECT_TRUE(p.layers[3].rgba.empty());
+ EXPECT_EQ(p.layers[0].pixels, small);
+ EXPECT_EQ(p.layers[1].pixels, edge);
+ EXPECT_EQ(p.layers[2].pixels, outside);
+ EXPECT_TRUE(p.layers[3].pixels.empty());
EXPECT_EQ(p.composite.size(), 8u * 4 * 4) << "the composite stays the canvas";
}
@@ -136,7 +136,7 @@ TEST(MinipsdWriterTest, ASuppliedCompositeIsMattedAndWrittenWithoutComposing)
{
const Bytes px = testImage(37, 5);
const minipsd_layer layers[2] = {{"Template", px.data(), 255, 0},
- {.name = "Offset", .rgba = px.data(), .opacity = 128, .top = 2, .left = -9, .bottom = 7, .right = 28}};
+ {.name = "Offset", .pixels = px.data(), .opacity = 128, .top = 2, .left = -9, .bottom = 7, .right = 28}};
for (int compression : {MINIPSD_RLE, MINIPSD_RAW}) {
SCOPED_TRACE(compression);
minipsd_doc d = docOf(37, 5, layers, 2);
@@ -233,7 +233,7 @@ TEST(MinipsdWriterTest, KeptContentIsWrittenBackAsGiven)
+ tagged("luni", {0, 0, 0, 1, 0, 'Y', 0, 0}) + tysh; // another stale name, a block
const minipsd_blending_range ranges[2] = {{{{0, 0}, {255, 255}}, {{59, 60}, {210, 220}}}, // grey, then red
{{{10, 10}, {200, 200}}, {{0, 0}, {255, 255}}}};
- const minipsd_layer layer{.name = "Mine", .rgba = px.data(), .opacity = 200, .hidden = 1,
+ const minipsd_layer layer{.name = "Mine", .pixels = px.data(), .opacity = 200, .hidden = 1,
.blend_mode = {'m', 'u', 'l', ' '}, .clipping = 1, .flags = 0x09, .additional_info = view(extra),
.blending_ranges = ranges, .blending_range_count = 2};
const Bytes cm = {0, 150, 0, 0, 0, 1, 0, 2, 0, 150, 0, 0, 0, 1, 0, 2}; // 150 dpi, shown in centimetres
@@ -256,7 +256,8 @@ TEST(MinipsdWriterTest, KeptContentIsWrittenBackAsGiven)
EXPECT_EQ(Bytes(info.layer_tail.data, info.layer_tail.data + info.layer_tail.size), tail);
minipsd_resource res{};
std::vector ids;
- while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE) {
+ std::size_t at = 0;
+ while (minipsd_reader_next_resource(r, &at, &res) == MINIPSD_MORE) {
ids.push_back(res.id);
if (res.id == 1005) {
EXPECT_EQ(Bytes(res.data.data, res.data.data + 16), cm) << "the same dpi: kept, units too";
@@ -285,8 +286,7 @@ TEST(MinipsdWriterTest, KeptContentIsWrittenBackAsGiven)
const Bytes other = write(d);
ASSERT_EQ(minipsd_reader_init(&r, other.data(), other.size(), workspace.data(), workspace.size(), nullptr, &info), MINIPSD_OK);
EXPECT_EQ(info.dpi, 72.0);
- while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE && res.id != 1005) {}
- ASSERT_EQ(res.id, 1005);
+ ASSERT_EQ(minipsd_reader_find_resource(r, MINIPSD_RES_RESOLUTION_INFO, &res), MINIPSD_OK);
EXPECT_EQ(res.data.data[7], 1) << "shown in inches";
// An 'Lr32' block says where the layers go too; a tail that is not blocks after its mask
@@ -311,7 +311,7 @@ TEST(MinipsdWriterTest, KeptContentIsWrittenBackAsGiven)
ASSERT_EQ(minipsd_reader_init(&r, noIcc.data(), noIcc.size(), workspace.data(), workspace.size(), nullptr, &info), MINIPSD_OK);
EXPECT_EQ(info.icc.size, 0u);
ids.clear();
- while (minipsd_reader_next_resource(r, &res) == MINIPSD_MORE) ids.push_back(res.id);
+ for (at = 0; minipsd_reader_next_resource(r, &at, &res) == MINIPSD_MORE;) ids.push_back(res.id);
EXPECT_EQ(ids, (std::vector{1005, 1036}));
}
@@ -324,7 +324,7 @@ TEST(MinipsdWriterTest, GroupsAndMasksAreWritten)
const Bytes kept = tagged("lsct", {0, 0, 0, 2, '8', 'B', 'I', 'M', 'n', 'o', 'r', 'm', 0, 0, 0, 1}); // with a sub-type
const minipsd_layer layers[5] = {
{.name = "", .opacity = 255, .section_divider_type = MINIPSD_SECTION_GROUP_END},
- {.name = "Masked", .rgba = px.data(), .opacity = 255, .mask = &mask},
+ {.name = "Masked", .pixels = px.data(), .opacity = 255, .mask = &mask},
{.name = "", .opacity = 255, .section_divider_type = MINIPSD_SECTION_GROUP_END},
{.name = "Inner", .opacity = 255, .additional_info = view(kept), .section_divider_type = MINIPSD_SECTION_GROUP_CLOSED},
{.name = "Outer", .opacity = 255, .blend_mode = {'s', 'c', 'r', 'n'}, .section_divider_type = MINIPSD_SECTION_GROUP}};
@@ -432,7 +432,7 @@ TEST(MinipsdWriterTest, GroupsAndMasksAreWritten)
const Bytes realRaw = {0, 0};
minipsd_mask withReal = mask;
withReal.real = view(realRaw);
- const minipsd_layer one{.name = "Real", .rgba = px.data(), .opacity = 255, .mask = &withReal};
+ const minipsd_layer one{.name = "Real", .pixels = px.data(), .opacity = 255, .mask = &withReal};
const Bytes file = write(docOf(6, 4, &one, 1));
Bytes workspace(256);
minipsd_reader *r = nullptr;
@@ -453,7 +453,7 @@ TEST(MinipsdWriterTest, ChannelsOverNoAreaHaveNoRows)
const minipsd_mask noArea{.pixels = two};
const minipsd_layer layers[2] = {
{.name = "Stored", .opacity = 255, .top = 0, .left = 0, .bottom = 0, .right = 2, .stored = stored},
- {.name = "Masked", .rgba = px.data(), .opacity = 255, .mask = &noArea}};
+ {.name = "Masked", .pixels = px.data(), .opacity = 255, .mask = &noArea}};
minipsd_doc d = docOf(2, 2, layers, 2);
d.composite = canvas.data();
const Bytes file = write(d);
@@ -511,7 +511,7 @@ TEST(MinipsdWriterTest, SixteenBitDocumentsAreWrittenAndReadBack)
const auto bytes = [](const std::vector &v) { return reinterpret_cast(v.data()); };
const minipsd_mask mask{.top = 1, .left = 1, .bottom = 3, .right = 5, .default_color = 255, .pixels = bytes(ramp)};
const minipsd_layer layers[2] = {
- {.name = "Deep", .rgba = bytes(px), .opacity = 200, .top = 1, .left = 1, .bottom = 4, .right = 6, .mask = &mask},
+ {.name = "Deep", .pixels = bytes(px), .opacity = 200, .top = 1, .left = 1, .bottom = 4, .right = 6, .mask = &mask},
{.name = "Empty", .opacity = 255}};
// ZIP through the stored-block deflate and inflate: each written as given, decoded back.
const minipsd_write_opts zipped{0, zlib_stored_compress, nullptr};
@@ -589,7 +589,7 @@ 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);
- const auto *got = reinterpret_cast(opaque->layers[0].rgba);
+ 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;
minipsd_doc_free(opaque);
}
@@ -632,7 +632,7 @@ TEST(MinipsdWriterTest, PsbDocumentsAreWrittenAndReadBack)
Bytes lnk2 = {'8', 'B', 'I', 'M', 'l', 'n', 'k', '2', 0, 0, 0, 0, 0, 0, 0, 2, 7, 7}; // eight bytes of length
const minipsd_mask mask{.top = 1, .left = 1, .bottom = 3, .right = 5, .default_color = 255, .pixels = ramp.data()};
const minipsd_layer layers[3] = {
- {.name = "Pic", .rgba = px.data(), .opacity = 200, .top = 1, .left = 1, .bottom = 4, .right = 6,
+ {.name = "Pic", .pixels = px.data(), .opacity = 200, .top = 1, .left = 1, .bottom = 4, .right = 6,
.additional_info = {lnk2.data(), lnk2.size()}, .mask = &mask},
{.name = "", .opacity = 255, .section_divider_type = MINIPSD_SECTION_GROUP_END},
{.name = "Group", .opacity = 255, .section_divider_type = MINIPSD_SECTION_GROUP}};
@@ -695,7 +695,7 @@ TEST(MinipsdWriterTest, APsbTakesSidesAPsdCannot)
const std::uint32_t w = 30001;
Bytes px(std::size_t(w) * 4);
for (std::size_t i = 0; i < px.size(); ++i) px[i] = static_cast(i % 4 == 3 ? 255 : i * 7);
- const minipsd_layer layer{.name = "Wide", .rgba = px.data(), .opacity = 255};
+ const minipsd_layer layer{.name = "Wide", .pixels = px.data(), .opacity = 255};
minipsd_doc d = docOf(w, 1, &layer, 1);
EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_SIZE) << "a PSD";
d.format = MINIPSD_PSB;
@@ -703,7 +703,7 @@ TEST(MinipsdWriterTest, APsbTakesSidesAPsdCannot)
minipsd_doc *back = nullptr;
ASSERT_EQ(minipsd_read(file.data(), file.size(), nullptr, nullptr, &back, nullptr), MINIPSD_OK);
EXPECT_EQ(back->width, w);
- EXPECT_TRUE(std::equal(px.begin(), px.end(), back->layers[0].rgba));
+ EXPECT_TRUE(std::equal(px.begin(), px.end(), back->layers[0].pixels));
minipsd_doc_free(back);
d.width = MINIPSD_MAX_SIDE_PSB + 1;
@@ -734,7 +734,7 @@ TEST(MinipsdWriterTest, AnotherFormatTranslatesWhatTheDocumentKeeps)
const Bytes extra = tagged("cinf", {1, 2, 3, 4}) + tagged("Patt", {9, 9});
const Bytes tail = be32(0) + tagged("FMsk", Bytes(12, 7)) + tagged("Patt", {5}); // the last one unpadded
const minipsd_blending_range ranges[1] = {{{{5, 6}, {250, 251}}, {{0, 0}, {255, 255}}}};
- const minipsd_layer layer{.name = "Pic", .rgba = px.data(), .opacity = 255, .top = 1, .left = 1, .bottom = 4, .right = 6,
+ const minipsd_layer layer{.name = "Pic", .pixels = px.data(), .opacity = 255, .top = 1, .left = 1, .bottom = 4, .right = 6,
.additional_info = {extra.data(), extra.size()}, .mask = &mask, .blending_ranges = ranges,
.blending_range_count = 1};
minipsd_doc d = docOf(6, 4, &layer, 1);
@@ -788,7 +788,7 @@ TEST(MinipsdWriterTest, AnotherFormatRefusesWhatItCannotTranslate)
{
const Bytes px = testImage(2, 2);
const minipsd_write_opts asPsb{MINIPSD_PSB}, asPsd{MINIPSD_PSD}, bad{3};
- minipsd_layer layer{.name = "L", .rgba = px.data(), .opacity = 255, .bottom = 2, .right = 2};
+ minipsd_layer layer{.name = "L", .pixels = px.data(), .opacity = 255, .bottom = 2, .right = 2};
minipsd_doc d = docOf(2, 2, &layer, 1);
EXPECT_EQ(minipsd_doc_check(&d, &bad), MINIPSD_ERR_ARG);
EXPECT_EQ(minipsd_doc_check(&d, &asPsb), MINIPSD_OK) << "nothing kept: nothing to translate";
@@ -858,7 +858,7 @@ TEST(MinipsdWriterTest, PassThroughEncodesOnlyTheLayerThatChanged)
const Bytes a = testImage(37, 5), b = testImage(20, 3), c = testImage(37, 5);
const Bytes fresh(std::size_t(20) * 3 * 4, 0x55);
const minipsd_layer layers[3] = {{"A", a.data(), 255, 0},
- {.name = "B", .rgba = b.data(), .opacity = 255, .top = 1, .left = 2, .bottom = 4, .right = 22},
+ {.name = "B", .pixels = b.data(), .opacity = 255, .top = 1, .left = 2, .bottom = 4, .right = 22},
{"C", c.data(), 128, 1}};
// Each layer's four channels as the file stores them, through the reader.
auto channels = [](const Bytes &file) {
@@ -884,14 +884,14 @@ TEST(MinipsdWriterTest, PassThroughEncodesOnlyTheLayerThatChanged)
minipsd_doc *r = nullptr;
ASSERT_EQ(minipsd_read(file.data(), file.size(), nullptr, &keep, &r, nullptr), MINIPSD_OK);
ASSERT_NE(r->layers[1].stored, nullptr);
- EXPECT_EQ(r->layers[1].rgba, nullptr) << "nothing decoded";
+ EXPECT_EQ(r->layers[1].pixels, nullptr) << "nothing decoded";
encodedLayers.clear();
EXPECT_EQ(write(*r), file) << "copied as stored: the same bytes";
EXPECT_TRUE(encodedLayers.empty()) << "and nothing encoded";
// Layer B changes: only it is encoded; the others' channels are the input's, byte for byte.
std::vector edited(r->layers, r->layers + 3);
- edited[1].rgba = fresh.data();
+ edited[1].pixels = fresh.data();
edited[1].stored = nullptr;
minipsd_doc e = *r;
e.layers = edited.data();
@@ -904,7 +904,7 @@ TEST(MinipsdWriterTest, PassThroughEncodesOnlyTheLayerThatChanged)
ASSERT_EQ(after.size(), 3u);
EXPECT_EQ(after[0], before[0]);
EXPECT_EQ(after[2], before[2]);
- EXPECT_EQ(read(out).layers[1].rgba, fresh);
+ EXPECT_EQ(read(out).layers[1].pixels, fresh);
minipsd_doc_free(r);
}
}
@@ -921,7 +921,7 @@ TEST(MinipsdWriterTest, RecordsKeepOpacityVisibilityAndPixelsAsGiven)
EXPECT_EQ(p.layers[1].flags, 0x02) << "hidden";
EXPECT_EQ(p.layers[1].opacity, 128);
EXPECT_EQ(p.layers[2].opacity, 0);
- EXPECT_EQ(p.layers[1].rgba, pink) << "opacity is a layer setting, not baked into its pixels";
+ EXPECT_EQ(p.layers[1].pixels, pink) << "opacity is a layer setting, not baked into its pixels";
EXPECT_EQ(p.composite, red) << "the composite reaches the file: neither hidden nor clear shows";
}
@@ -945,7 +945,7 @@ TEST(MinipsdWriterTest, UnicodeNameGoesToLuniAndAsciiToThePascalString)
// NUL: written as they are while the name is theirs, the writer's own once it changes.
const Bytes legacy = {0xAF, 'o', 'l', 'w'}; // "Zolw" with a Z dot, Windows-1250
const Bytes luni = tagged("luni", {0, 0, 0, 5, 0x01, 0x7B, 0, 'o', 0, 'l', 0, 'w', 0, 0, 0, 0});
- minipsd_layer kept{.name = "\xC5\xBBolw", .rgba = px.data(), .opacity = 255, .additional_info = view(luni), .name_pascal = view(legacy)};
+ minipsd_layer kept{.name = "\xC5\xBBolw", .pixels = px.data(), .opacity = 255, .additional_info = view(luni), .name_pascal = view(legacy)};
Psd k = read(write(docOf(2, 2, &kept, 1)));
EXPECT_EQ(k.layers[0].pascalName, std::string(legacy.begin(), legacy.end())) << "the kept legacy name";
EXPECT_EQ(k.layers[0].unicodeName, (std::u16string{u'\u017B', u'o', u'l', u'w', 0})) << "the kept 'luni', its NUL too";
@@ -956,7 +956,7 @@ TEST(MinipsdWriterTest, UnicodeNameGoesToLuniAndAsciiToThePascalString)
// Without a 'luni', a legacy name read as Latin-1: kept, and no 'luni' while it is the name.
const Bytes cafe = {'C', 'a', 'f', 0xE9};
- minipsd_layer latin{.name = "Caf\xC3\xA9", .rgba = px.data(), .opacity = 255, .name_pascal = view(cafe)};
+ minipsd_layer latin{.name = "Caf\xC3\xA9", .pixels = px.data(), .opacity = 255, .name_pascal = view(cafe)};
k = read(write(docOf(2, 2, &latin, 1)));
EXPECT_EQ(k.layers[0].pascalName, "Caf\xE9");
EXPECT_TRUE(k.layers[0].unicodeName.empty()) << "the file had none";
@@ -977,7 +977,7 @@ TEST(MinipsdWriterTest, UnicodeNameGoesToLuniAndAsciiToThePascalString)
};
for (const auto &n : names) {
SCOPED_TRACE(n.name ? n.name : "(null)");
- const minipsd_layer one{.name = n.name, .rgba = px.data(), .opacity = 255, .additional_info = view(n.luni)};
+ const minipsd_layer one{.name = n.name, .pixels = px.data(), .opacity = 255, .additional_info = view(n.luni)};
const Psd q = read(write(docOf(2, 2, &one, 1)));
const std::string mine = n.name ? n.name : "";
if (n.kept) { EXPECT_EQ(q.layers[0].unicodeName.size(), mine.size() + (n.name ? 1u : 0u)) << "the kept 'luni', its NUL too"; }
@@ -1087,7 +1087,7 @@ TEST(MinipsdWriterTest, PullTotalIsTheFileSize)
const Bytes icc = {1, 2, 3, 4, 5};
const minipsd_layer layers[4] = {{"Template", px.data(), 255, 0}, {"Layer", nullptr, 255, 0},
{"Half \xC5\xBC", px.data(), 128, 1},
- {.name = "Offset", .rgba = px.data(), .opacity = 255, .top = -2, .left = 20, .bottom = 3, .right = 57}};
+ {.name = "Offset", .pixels = px.data(), .opacity = 255, .top = -2, .left = 20, .bottom = 3, .right = 57}};
for (int compression : {MINIPSD_RLE, MINIPSD_RAW})
for (bool withIcc : {false, true}) {
minipsd_doc d = docOf(37, 5, layers, 4);
@@ -1106,7 +1106,7 @@ TEST(MinipsdWriterTest, PullWriterFeedsANonBlockingTarget)
// it did not take and pulls more only once that is gone.
const Bytes px = testImage(37, 5);
const minipsd_layer layers[3] = {{"Template", px.data(), 255, 0}, {"Layer", nullptr, 255, 0},
- {.name = "Offset", .rgba = px.data(), .opacity = 255, .top = 4, .left = -30, .bottom = 9, .right = 7}};
+ {.name = "Offset", .pixels = px.data(), .opacity = 255, .top = 4, .left = -30, .bottom = 9, .right = 7}};
for (int compression : {MINIPSD_RLE, MINIPSD_RAW}) {
minipsd_doc d = docOf(37, 5, layers, 3);
d.compression = compression;