diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 281adb6..f31d8f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # Builds and tests minipsd on every supported toolchain: the unit tests (C99 + C++20), the C++17 # compile check, the psd-tools round trip (required here, optional locally) and the consumer tests. # Then, on Linux, the same tests under sanitizers, clang-tidy and coverage (job "analysis"). -# release.yml calls this workflow before it publishes anything. +# release.yml publishes only a commit this workflow has passed on master. # Branches run nothing until a pull request is ready for review (drafts are skipped); after that, # each push to it runs again and cancels the run it replaces. name: CI @@ -12,7 +12,6 @@ on: pull_request: types: [ opened, synchronize, reopened, ready_for_review ] workflow_dispatch: - workflow_call: permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63bea5b..a2a8475 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ -# Release: a pushed tag X.Y.Z (bare semver) runs the full CI, checks the tag against the header -# version and the changelog, and publishes minipsd.h (include/ joined into one file by +# Release: a pushed tag X.Y.Z (bare semver) checks that CI already passed on its commit on master, +# checks the tag against the header version and the changelog, and publishes minipsd.h (include/ joined into one file by # tools/amalgamate.cmake) + minipsd.h.sha256 as a GitHub Release with a build-provenance attestation. "Run workflow" (workflow_dispatch) does everything except publish, # so it is a dry run. See docs/GITHUB-RELEASE.md. name: Release @@ -11,18 +11,34 @@ on: permissions: contents: write # create the Release + actions: read # find CI's runs on the commit id-token: write # sign the attestation attestations: write # store it jobs: - ci: - uses: ./.github/workflows/ci.yml - release: - needs: ci runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 # master's history, to find the commit in it + + # CI runs on every push to master, so the commit is tested already: the release checks + # that rather than testing it again. + - name: Check the commit is on master and CI passed on it + env: + GH_TOKEN: ${{ github.token }} + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_SHA is not on master: a release is made from what master has" + exit 1 + fi + passed=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ci.yml --commit "$GITHUB_SHA" \ + --status success --json databaseId --jq length) + if [ "$passed" -eq 0 ]; then + echo "::error::no successful CI run on $GITHUB_SHA: wait for CI to finish, then re-run this job" + exit 1 + fi - name: Check the tag, the header version and the changelog agree run: | diff --git a/README.md b/README.md index 0e2303e..1b1fd71 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Procreate open as layered documents. # Status -**release: 0.3.0** +**release: 0.3.1** The API is [include/minipsd.h](include/minipsd.h); the implementation is in [include/minipsd/](include/minipsd/). @@ -62,7 +62,8 @@ 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 -they are. +they are; guides, alpha channel names, version info and a layer's id, colour label, fill and locks +made from plain values. Channels are PackBits RLE or raw, or ZIP through your deflate (zlib's `compress2`: minipsd implements none). Each side is limited to 30 000 px, or 300 000 px written as a PSB (Photoshop's large document format). @@ -95,9 +96,11 @@ Define before the implementation include: | Macro | Effect | |---|---| -| `MINIPSD_STATIC` | make every function `static` (nothing exported) | +| `MINIPSD_STATIC` | make every function `static` (nothing exported); those a file does not call do not warn | | `MINIPSD_MALLOC` / `MINIPSD_FREE` | replace `malloc` / `free` | | `MINIPSD_NO_MALLOC` | leave out every allocation (`minipsd_write`); the pull writer stays | +| `MINIPSD_NO_READER` | leave out the reader, the typed views and `minipsd_read`: a program that only writes | +| `MINIPSD_NO_WRITER` | leave out the writer, the typed setters and `minipsd_read`: a program that only reads | ## Documentation diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3ba54a1..5e2cd87 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,29 @@ Changes between releases, for users of minipsd. Versioning follows the pre-1.0 rules in [TODO.md](TODO.md): breaking changes bump the MINOR, additions and fixes the PATCH. +## [0.3.1] — 2026-10-24 + +### Added +- `MINIPSD_NO_READER` and `MINIPSD_NO_WRITER`: a build with only the writer or only the reader; + `minipsd_read` needs both. +- Typed setters, the mirror of the typed views: a struct in, the bytes of a resource out, into the + caller's buffer, measured first: `minipsd_resource_make_version_info`, `_grid_and_guides`, + `_alpha_names` (1045 or 1006, from UTF-8), `_alpha_identifiers`, `_transparency_index`, and + `minipsd_utf8_to_utf16be`. Resolution info is `dpi`: `minipsd_resource_make_resolution_info` + returns the new `MINIPSD_ERR_USE_DOC_FIELD`. `minipsd_layer_make_block_settings` sets a layer's + id, colour label, fill, locks and knockout in the blocks it has: each replaced where it is, + added only when missing, the others kept. Size constants (`MINIPSD_GRID_AND_GUIDES_SIZE(n)`, + `MINIPSD_BLOCK_SETTINGS_MAX`, ...) make one call with a fixed buffer enough. +- `minipsd_blocks_settings`: a layer's settings from its blocks alone, such as a document's + `minipsd_layer.additional_info`. +- The pixel aspect ratio (resource 1064): `minipsd_info.pixel_aspect_ratio` (1.0 without one), to + find or refuse non-square pixels; `minipsd_doc.pixel_aspect_ratio` writes one (0: none of the + writer's own), and `minipsd_read` keeps the file's. `MINIPSD_RES_PIXEL_ASPECT_RATIO`. + +### Fixed +- Under `MINIPSD_STATIC`, the functions a file does not call no longer warn (`-Wunused-function` + in gcc and clang, C4505 in MSVC): each is marked possibly unused. + ## [0.3.0] — 2026-10-23 **Breaking:** `minipsd_write`, `minipsd_writer_workspace_size`, `minipsd_writer_init` and diff --git a/docs/CODE-ARCH.md b/docs/CODE-ARCH.md index 2eaa8e7..5b86972 100644 --- a/docs/CODE-ARCH.md +++ b/docs/CODE-ARCH.md @@ -94,6 +94,7 @@ include/ base.h return-code texts writer.h the encoder (pull), plus the 0.1-compatible minipsd_write (push) views.h typed views: a kept resource or a layer's blocks parsed into plain values + builders.h typed setters: the mirror, plain values made into a resource's or blocks' bytes reader.h the decoder: views into the caller's bytes read.h minipsd_read: a whole file into one standalone document, on the reader detail/ @@ -103,6 +104,7 @@ include/ utf.h UTF-8 <-> UTF-16 for layer names and other text packbits.h PackBits encode, decode, size bound; strided source and target composite.h blending one row "over", white matte and un-matte + floats.h floating-point values compared within a tolerance, finite checks; no libm format.h the file's layout: each structure's fields and sizes, and what PSD and PSB differ in (length and row-count widths, long keys, blocks) examples/ the I/O implementations; built and run in CI, never installed @@ -141,10 +143,15 @@ Rules for the split: - `MINIPSD_IMPLEMENTATION` is defined once, as today. - Each implementation file is guarded by its own `MPSD___DONE`, so it is built once however often `minipsd.h` is included. - - `MINIPSD_STATIC` keeps working unchanged. + - `MINIPSD_STATIC` keeps working; its functions are marked possibly unused, since a file calls + only some. - **No unused internals.** The full implementation, compiled with extern linkage, leaves no - `static` function unused: that is a warning, and warnings are errors. -- **Optional subsystems hide behind macros in `minipsd.h`**, the way `MINIPSD_NO_MALLOC` does. + `static` function unused: that is a warning, and warnings are errors. Without the reader or + the writer (`MINIPSD_NO_READER`, `_NO_WRITER`) the helpers in `detail/` are marked possibly + unused (`MPSD__SHARED`), so this check still holds for the full build. `minipsd.h` includes + `detail/` before either part, so the single file keeps them whichever part is left out. +- **Optional subsystems hide behind macros in `minipsd.h`**, the way `MINIPSD_NO_MALLOC`, + `MINIPSD_NO_READER` and `MINIPSD_NO_WRITER` do. - **Promotion is additive.** `packbits` and `composite` stay internal until a consumer needs them. Making one public means adding a declaration to `minipsd.h`, a PATCH change, never a rename of anything existing. diff --git a/docs/CODING_STYLE_C.md b/docs/CODING_STYLE_C.md index 266e804..68c2567 100644 --- a/docs/CODING_STYLE_C.md +++ b/docs/CODING_STYLE_C.md @@ -34,6 +34,11 @@ together, under a table of the structure's fields and sizes, in the module the r share. An offset read once, right where the table shows it, may stay a number. Plain arithmetic and alignment (`pad(n, 4)`, `* 2` for UTF-16 units) are not facts to name. +Floating-point values are never compared with `==` or `!=`: within a tolerance, a named epsilon, +through `detail/floats.h` (`mpsd__floats_equal`), which also says whether a value is finite +(`mpsd__floats_positive`, `mpsd__floats_not_negative`), all without libm. The tests do the same +(`EXPECT_FLOAT_EQ`, `EXPECT_DOUBLE_EQ`). Byte identity of copied data is not a float comparison. + ## Naming | Kind | Form | diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 4ef15ef..86011a7 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -8,7 +8,7 @@ output is covered by the golden test and the `psd-tools` round trip (see the spe [Verification](SPECIFICATION.md#verification)); this page is about everyone else's files. This page is generated by [tools/compat_report.py](../tools/compat_report.py) with minipsd -0.3.0, over 315 files of psd-tools v1.25.0. +0.3.1, over 315 files of psd-tools v1.25.0. ## Credits diff --git a/docs/GITHUB-RELEASE.md b/docs/GITHUB-RELEASE.md index 800a4be..222b3eb 100644 --- a/docs/GITHUB-RELEASE.md +++ b/docs/GITHUB-RELEASE.md @@ -5,8 +5,10 @@ that tag (FetchContent, submodule), so a tag is never moved once pushed. Pushing the tag starts the [`Release`](../.github/workflows/release.yml) workflow: -1. the full [`CI`](../.github/workflows/ci.yml) on every toolchain, including the `psd-tools` - round trip; +1. a check that the tagged commit is on `master` and that [`CI`](../.github/workflows/ci.yml) + (every toolchain, the `psd-tools` round trip) passed on it: CI runs on every push to + `master`, so the release does not test it again. A tag pushed before that run ends fails + here; re-run the job once CI is green; 2. a check that the tag equals the `MINIPSD_VERSION_*` macros in `include/minipsd.h` and that `docs/CHANGELOG.md` has a section for that version; 3. `minipsd.h` as a single file: `include/` joined by `tools/amalgamate.cmake`; diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 4d560c4..21253a1 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -13,7 +13,7 @@ complete programs here are in [examples/](../examples/), which CI builds and run | [Round trip](#round-trip) | a file read into a document, changed, written back; what is kept, what is lost | | [Memory and ownership](#memory-and-ownership) | what you allocate, how long it must live | | [Return codes](#return-codes) | `MINIPSD_OK`, `MINIPSD_MORE`, errors | -| [Configuration](#configuration) | `MINIPSD_STATIC`, the allocator, a build without allocation | +| [Configuration](#configuration) | `MINIPSD_STATIC`, the allocator, a build without allocation, the reader or the writer | --- @@ -300,6 +300,30 @@ doc.composite = flattened; /* width * height * 4, straight alpha; NULL: compos Without one, the writer composes the visible layers with "over" at their opacity: enough for a preview, with no guarantee for anything else. +Guides, the alpha channels' names, version info, and a layer's id, colour label, fill and locks go +in as resources and blocks. A typed setter makes their bytes from the same structs the typed +views read ([Metadata](#metadata)), into your buffer; the size constants make one call enough: + +```c +static const minipsd_guide guides[] = { { 100 * 32, 0 }, { 50 * 32, 1 } }; /* x = 100, y = 50: pixels times 32 */ +const minipsd_grid_and_guides grid = { .version = 1, .horizontal_grid = 576, .vertical_grid = 576 }; +uint8_t guide_bytes[MINIPSD_GRID_AND_GUIDES_SIZE(2)]; +size_t n; +minipsd_resource_make_grid_and_guides(&grid, guides, 2, guide_bytes, sizeof guide_bytes, &n); +const minipsd_resource resources[] = { { .id = MINIPSD_RES_GRID_AND_GUIDES, .data = { guide_bytes, n } } }; +doc.resources = resources; +doc.resource_count = 1; + +const minipsd_block_settings blue = { .present = MINIPSD_PRESENT_SHEET_COLOR, .sheet_color = 5 }; +uint8_t label[MINIPSD_BLOCK_SETTINGS_MAX]; +minipsd_layer_make_block_settings(NULL, doc.format, &blue, label, sizeof label, &n); /* a new layer: no blocks yet */ +logo.additional_info = (minipsd_bytes){ label, n }; +``` + +The resolution is `dpi`: `minipsd_resource_make_resolution_info` makes nothing and says so +(`MINIPSD_ERR_USE_DOC_FIELD`). Version info's names are UTF-16BE, as the view gives them; +`minipsd_utf8_to_utf16be` makes them from text. + The document, its pixels and the workspace must stay valid, and unchanged, until the last byte is out: the writer sized everything from them at the start. @@ -564,7 +588,17 @@ visible. ### Metadata -`info` has what most programs need: `dpi` and the ICC profile. For more, find a resource by its +`info` has what most programs need: `dpi`, the ICC profile, and `pixel_aspect_ratio`, a pixel's +width over its height (1.0 when the file says nothing). A program that handles only square pixels +can refuse the others; compare within a tolerance, never with `!=`: + +```c +if (fabs(info.pixel_aspect_ratio - 1.0) > 1e-6) + return refuse("non-square pixels"); +``` + +A document writes one when its `pixel_aspect_ratio` is above 0; `minipsd_read` sets it from the +file. For more, find a resource by its id and let a typed view parse it; none of them needs the reader again, so a document's `resources` work too: @@ -645,6 +679,30 @@ The document lives in one block: from `MINIPSD_MALLOC`, freed by `minipsd_doc_fr 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. +To change a setting the file already has, set it in the layer's blocks. The setter replaces the +block where it is, since readers take the first block of a key, and copies every other one (text, +effects) as it was. The document's layers are `const`, so edit a copy of the array: + +```c +minipsd_layer *layers = malloc(doc->layer_count * sizeof *layers); +const minipsd_block_settings red = { .present = MINIPSD_PRESENT_SHEET_COLOR, .sheet_color = 1 }; +const size_t room = doc->layers[0].additional_info.size + MINIPSD_BLOCK_SETTINGS_MAX; +uint8_t *blocks = malloc(room); +size_t n; +memcpy(layers, doc->layers, doc->layer_count * sizeof *layers); +minipsd_layer_make_block_settings(&layers[0].additional_info, doc->format, &red, blocks, room, &n); +layers[0].additional_info = (minipsd_bytes){ blocks, n }; /* the bottom layer's label: red */ +doc->layers = layers; +rc = minipsd_write(doc, NULL, to_file, f); +free(blocks); +free(layers); +minipsd_doc_free(doc); +``` + +A resource the file has is replaced the same way: copy `doc->resources`, put the made one in place +of the entry of that id, and point `doc->resources` at the copy. `minipsd_blocks_settings` reads a +document layer's settings from its blocks, as `minipsd_layer_block_settings` reads a file's. + **What comes back.** Every layer with its rectangle, name, opacity, visibility, blend mode, clipping and flags, its pixels, its mask, its "Blend If" ranges and its place in the groups; the blocks a layer carries (text, effects, vector masks, smart objects) and the file's resources and global blocks, byte for byte, so a program @@ -778,6 +836,8 @@ Define before the `#include` in the implementation file: | Macro | Effect | |---|---| -| `MINIPSD_STATIC` | every function `static`, private to that file: several copies never clash | +| `MINIPSD_STATIC` | every function `static`, private to that file: several copies never clash, and those you do not call do not warn | | `MINIPSD_MALLOC` / `MINIPSD_FREE` | replace `malloc` / `free`; define both | | `MINIPSD_NO_MALLOC` | no allocation at all, and no ``: `minipsd_write` goes, the pull writer and the reader stay | +| `MINIPSD_NO_READER` | no reader, typed views or `minipsd_read`: less code for a program that only writes | +| `MINIPSD_NO_WRITER` | no writer, typed setters or `minipsd_read`: less code for a program that only reads | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 97408f1..ca3fa49 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -183,12 +183,18 @@ 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 +- [x] **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`; done: `linux-gcc`), so that Photoshop's ZIP files are checked through zlib there too, not only locally. +- [ ] **S4.11** Release **0.3.1** per [GITHUB-RELEASE.md](GITHUB-RELEASE.md): additions and a fix, + so a PATCH. `MINIPSD_STATIC` without unused-function warnings, `MINIPSD_NO_READER` and + `MINIPSD_NO_WRITER`, the typed setters (S6.20), the pixel aspect ratio (S6.21); the release + workflow checks CI's run on the tagged commit instead of running it again. Before it: + [COMPATIBILITY.md](COMPATIBILITY.md) regenerated with the version (done), the PR merged + and CI green on `master`. ## 5. Round trip and conveniences @@ -489,6 +495,26 @@ feature; a supplied composite needs nothing. See 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. +- [x] **S6.20** Typed setters: the mirror of S6.17's views, for the writer. A struct in, the + bytes of a resource or a block out, into the caller's buffer (the size asked first, as + `minipsd_layer_name` does), ready for `minipsd_doc.resources` or a layer's + `additional_info`: resolution info (1005; today only `dpi` writes it), version info + (1057), grid and guides (1032), alpha channel names and identifiers (1045, 1006, 1053), + the transparency index (1047), and a layer's block settings (`lyid`, `lclr`, `iOpa`, + `lspf`). Today a caller builds those bytes by hand. They go with the writer: under + `MINIPSD_NO_WRITER` they are left out, as the views are under `MINIPSD_NO_READER`. + *Done when:* each setter's bytes read back through its view to the same struct, and + psd-tools reads them as written. + Done: 1005 stays `dpi`'s (`MINIPSD_ERR_USE_DOC_FIELD`); the block setter edits the run a + layer has (replaced where it is, added only when missing), all seven blocks. +- [x] **S6.21** The pixel aspect ratio (resource 1064), as `dpi` is the resolution: read into + `minipsd_info.pixel_aspect_ratio` (1.0 without it), so a program can refuse non-square + pixels; written from `minipsd_doc.pixel_aspect_ratio` (0: none of the writer's own; a kept + 1064 that says the same stays where it is); `minipsd_read` keeps the file's. The resolution + stays square: the ratio is not folded into 1005. Doubles are compared within a tolerance + (`detail/floats.h`). + *Done when:* psd-tools reads a ratio minipsd writes, and every file of its set reads and + writes back unchanged. ## 7. Interoperability @@ -562,6 +588,9 @@ Large cases. Each would start with a *design* step, and only when a real consume - Segment output for zero-copy saves (`E3`). - In-place patching of fixed-size fields. - Making `packbits` or `composite` public. +- Typed setters that allocate (S6.20's, through `MINIPSD_MALLOC`): each piece would need freeing + after the write, apart from `minipsd_doc_free`, and they would not exist under + `MINIPSD_NO_MALLOC`. The size constants make one call with the caller's buffer the common case. - The C++ facade. - 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 diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index aab35d1..b55773f 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -80,14 +80,18 @@ real readers win (see [Verification](#verification)). `-Wconversion -Wsign-conversion -Wshadow -Wcast-qual -Wundef -Wdouble-promotion -Wnull-dereference -Wformat=2 -Wstrict-prototypes -Wmissing-prototypes -Wold-style-definition`, and with gcc `-Wcast-align -Wduplicated-cond -Wlogical-op` (the list is in - `tests/CMakeLists.txt`). + `tests/CMakeLists.txt`). So is a program that uses part of it, as C and as C++17: under + `MINIPSD_STATIC` calling only `minipsd_write`, and without the reader or the writer + (`tests/part_check.c`). - Configuration macros (define before the implementation include): | Macro | Effect | |---|---| -| `MINIPSD_STATIC` | every function is `static`, nothing is exported | +| `MINIPSD_STATIC` | every function is `static`, nothing is exported; each is marked possibly unused, so a file that calls only some builds without warnings | | `MINIPSD_MALLOC` / `MINIPSD_FREE` | replace `malloc` / `free` (define both) | | `MINIPSD_NO_MALLOC` | omit every allocation: `minipsd_write` and ``; the pull writer stays | +| `MINIPSD_NO_READER` | omit the reader, the typed views and `minipsd_read`, their types and functions | +| `MINIPSD_NO_WRITER` | omit the writer, the typed setters and `minipsd_read`; with `MINIPSD_NO_READER`, an error | | `MINIPSD_NO_STDIO` | accepted, no effect: minipsd never includes `` | - **No I/O.** minipsd opens no file, pipe or socket; bytes go to the caller's buffer or callback. @@ -124,6 +128,10 @@ The declarations, with every parameter and return code, are in | `minipsd_resource_id_name(id)` | Adobe's name for a resource id ("Resolution info", "Path information"), to list what a file holds; `NULL` for an id Adobe does not list | | `minipsd_resource_transparency_index(res, &index)` | an indexed file's transparent palette entry (1047) | | `minipsd_layer_block_settings(layer, &out)` | a layer's fixed-layout blocks parsed: id, colour label, fill opacity, locks, knockout, blending of clipped and interior elements; `present` says which it has | +| `minipsd_blocks_settings(blocks, format, &out)` | the same over a run of blocks alone, such as a document's `minipsd_layer.additional_info` | +| `minipsd_resource_make_version_info`, `_grid_and_guides`, `_alpha_names`, `_alpha_identifiers`, `_transparency_index` (`..., buf, cap, &needed`) | typed setters: a resource's bytes made from the values its view reads, into the caller's buffer, for `minipsd_doc.resources`; `minipsd_resource_make_resolution_info` makes nothing (`MINIPSD_ERR_USE_DOC_FIELD`: `dpi` says it) | +| `minipsd_layer_make_block_settings(blocks, format, &settings, buf, cap, &needed)` | a layer's blocks with its settings set: each replaced where it is, added only when missing, every other block as it is | +| `minipsd_utf8_to_utf16be(utf8, utf16, cap, &needed)` | UTF-8 text as UTF-16BE, for version info's names: the converse of `minipsd_utf16be_to_utf8` | | `minipsd_utf16be_to_utf8(text, utf8, cap, &needed)` | UTF-16BE text from a view (a version info's names) as UTF-8, as `minipsd_layer_name` gives a name | | `minipsd_target_rgba(&target, rgba, row_stride)` | a target for interleaved RGBA, the common case | | `minipsd_target_rgba16(&target, rgba, row_stride)` | the same for 16-bit samples, `uint16_t` | @@ -171,7 +179,7 @@ its layers and composite as stored: no colour is converted. | `minipsd_info` | what the file says about itself: size, depth, colour mode and its colour channels, colour mode data, dpi, ICC profile, layer count, format (PSD or PSB) | | `minipsd_bytes` | a read-only view into the caller's buffer | | `minipsd_resource`, `minipsd_layer_view`, `minipsd_block` | an image resource, a layer record, an additional-information block, as views | -| `minipsd_resolution_info`, `minipsd_version_info`, `minipsd_grid_and_guides`, `minipsd_guide`, `minipsd_block_settings` | typed views: what resources 1005, 1057 and 1032 and a layer's fixed-layout blocks store, parsed; strings stay views | +| `minipsd_resolution_info`, `minipsd_version_info`, `minipsd_grid_and_guides`, `minipsd_guide`, `minipsd_block_settings` | typed values: what resources 1005, 1057 and 1032 and a layer's fixed-layout blocks store, parsed by the typed views and made by the typed setters; strings stay views | | `minipsd_target`, `minipsd_plane` | where decoded samples go: per plane (the colour channels, then transparency) a pointer, a pixel step and a row step; any layout | | `minipsd_decode_opts` | the rows to decode, the target's `depth` and, for the composite, `keep_matte`; `NULL` for all rows, 8-bit, un-matted | @@ -226,6 +234,7 @@ its layers and composite as stored: no colour is converted. | `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 | +| `MINIPSD_ERR_USE_DOC_FIELD` | -11 | a typed setter made nothing, since a field of the document says it: resource 1005 is `minipsd_doc.dpi` | ## Memory and ownership @@ -288,7 +297,7 @@ interact, whatever the table says. | `minipsd_layer_name` | yes | reads the view; writes only the caller's buffer | | `minipsd_layer_next_block` | yes | each thread its own `pos` | | `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_resource_*`, `minipsd_layer_block_settings`, `minipsd_blocks_settings`, `minipsd_layer_make_block_settings`, `minipsd_utf16be_to_utf8`, `minipsd_utf8_to_utf16be` | yes | read the view or the values; 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`) | | `minipsd_decode_mask` | yes, with the whole file | as `minipsd_decode_layer` | @@ -420,7 +429,31 @@ several threads from one reader into one image. dpi is written instead, with its own units and vertical resolution; - 1039 ICC profile, only when `icc` is set; - a later kept 1005 or 1039 goes: `dpi` and `icc` win; + - 1064 pixel aspect ratio, from `pixel_aspect_ratio`: at 0 none of the writer's own, a kept 1064 + written as it is; above 0, a kept 1064 that says the same (within a tolerance) stays as it is, + where it is, otherwise the writer's own (version 2, a double) comes after 1039 and every kept + 1064 goes. The resolution stays square: the ratio is never folded into 1005. Photoshop's files + in psd-tools' set all hold 1.0 (290 version 2, 15 version 1), so another ratio is written as + the format lays it out but not verified against a reader; - a resource of odd length is padded to even. +- **Typed setters:** the mirror of the typed views (in [What is read](#what-is-read)): a struct in, + the bytes the view reads out, into the caller's buffer, counted first (`needed`, and + `MINIPSD_MORE` with nothing written when the buffer is too small). Version info (1057), its + names UTF-16BE as the view gives them; grid and guides (1032) from the header and an array, + the count given winning over the header's; the alpha channels' names from UTF-8, as Unicode + (1045) or as Pascal strings (1006: ASCII, `?` for each other character, at most 255 bytes); their + identifiers (1053); the transparency index (1047). Resolution info (1005) is never made: it is + `dpi`, and `minipsd_resource_make_resolution_info` says so (`MINIPSD_ERR_USE_DOC_FIELD`). A + layer's fixed-layout blocks are set in the run it has, not added to it: the first block of each + key `present` asks for is replaced where it is, keeping its signature, one the run lacks is + added at the end (in the order `lyid`, `lclr`, `iOpa`, `lspf`, `knko`, `clbl`, `infx`), every + other block is copied as it is; a second block of a key would be ignored, since readers take the + first. Each block's data takes the bytes Photoshop's files in psd-tools' set hold: eight for + `lclr`, four for the others, the value first, then zeros. Constants bound the sizes, so one call + with a buffer of that size does: `MINIPSD_TRANSPARENCY_INDEX_SIZE`, + `MINIPSD_GRID_AND_GUIDES_SIZE(n)`, `MINIPSD_ALPHA_IDENTIFIERS_SIZE(n)`, and + `MINIPSD_BLOCK_SETTINGS_MAX` over the run given. `roundtrip.py` writes a document with every + setter's bytes and checks psd-tools reads the values given. - **The layer info's padding:** its length padded to even, as the format has it, whatever the file read had (Photoshop pads to 4, some programs not at all). - **After the layer info:** the kept `layer_tail` (global layer mask, global blocks) as it is, @@ -484,7 +517,8 @@ them; nothing is copied or allocated. Results on real files from other programs: a document whose `format` is the file's, written back as a PSB; - four wrong bytes at the start are `MINIPSD_ERR_FORMAT`. - **Header and resources:** size, channels, depth, colour mode; dpi from resource 1005 - (horizontal, 0 without it); the ICC profile from 1039. Every resource, these two included, is + (horizontal, 0 without it); the ICC profile from 1039; the pixel aspect ratio from 1064 (1.0 + without it; one shorter than its 12 bytes is `MINIPSD_ERR_FORMAT`). Every resource, these two included, is also returned as a raw view, with its name, in file order, or found by id (the first of it). - **Typed views:** what a kept resource stores, parsed into plain values by a function that takes the resource and needs no reader: nothing allocated, a string left as a view of its @@ -519,6 +553,7 @@ them; nothing is copied or allocated. Results on real files from other programs: | 1057 version info | `MINIPSD_RES_VERSION_INFO` | `minipsd_resource_version_info` | | 1058 EXIF | `MINIPSD_RES_EXIF_DATA_1` | bytes | | 1060 XMP | `MINIPSD_RES_XMP_METADATA` | bytes (UTF-8 XML) | + | 1064 pixel aspect ratio | `MINIPSD_RES_PIXEL_ASPECT_RATIO` | `minipsd_info.pixel_aspect_ratio`; written from `minipsd_doc.pixel_aspect_ratio` | | any other id | | bytes, from `minipsd_reader_next_resource` or `minipsd_reader_find_resource` | - **Layer records**, bottom first: - the rectangle; one stored inverted (Photoshop's fill layers) comes back empty; diff --git a/include/minipsd.h b/include/minipsd.h index 1dfd6f3..c2a2fc3 100644 --- a/include/minipsd.h +++ b/include/minipsd.h @@ -38,7 +38,11 @@ * - \c MINIPSD_MALLOC / \c MINIPSD_FREE: replace malloc / free (define both); * - \c MINIPSD_NO_MALLOC: leave out every allocation: minipsd_write, minipsd_read and * minipsd_doc_free go (and ); the pull writer, the reader and minipsd_read_into, on - * the caller's memory, stay. + * the caller's memory, stay; + * - \c MINIPSD_NO_READER: leave out the reader, the typed views and minipsd_read, for a program + * that only writes; + * - \c MINIPSD_NO_WRITER: leave out the writer, the typed setters and minipsd_read, for a program + * that only reads. * * minipsd does no I/O of its own and includes no : files, pipes and sockets are the * caller's, fed by minipsd_write or minipsd_writer_fill (examples/ in the repository has each). @@ -92,6 +96,10 @@ #include #include +#if defined(MINIPSD_NO_READER) && defined(MINIPSD_NO_WRITER) +# error "minipsd: MINIPSD_NO_READER and MINIPSD_NO_WRITER together leave nothing" +#endif + /* --- Version and linkage ------------------------------------------------------------------- */ /** @@ -101,17 +109,28 @@ */ #define MINIPSD_VERSION_MAJOR 0 #define MINIPSD_VERSION_MINOR 3 -#define MINIPSD_VERSION_PATCH 0 +#define MINIPSD_VERSION_PATCH 1 #define MPSD__STR(x) #x #define MPSD__XSTR(x) MPSD__STR(x) -/** \brief The version as a string literal, e.g. "0.3.0". */ +/** \brief The version as a string literal, e.g. "0.3.1". */ #define MINIPSD_VERSION_STRING \ MPSD__XSTR(MINIPSD_VERSION_MAJOR) "." MPSD__XSTR(MINIPSD_VERSION_MINOR) "." MPSD__XSTR(MINIPSD_VERSION_PATCH) -/** \brief Linkage of every public function: static under MINIPSD_STATIC, otherwise extern (C). */ +/** \brief Linkage of every public function: static under MINIPSD_STATIC, otherwise extern (C). + * + * Static, each is marked as possibly unused, so a file that calls only some of them builds + * without warnings: GNU's attribute in gcc and clang, [[maybe_unused]] in C++17, and in MSVC + * warning C4505 off for the implementation. + */ #ifdef MINIPSD_STATIC -# define MINIPSDDEF static +# if defined(__GNUC__) || defined(__clang__) +# define MINIPSDDEF __attribute__((unused)) static +# elif defined(__cplusplus) && (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +# define MINIPSDDEF [[maybe_unused]] static +# else +# define MINIPSDDEF static +# endif #elif defined(__cplusplus) # define MINIPSDDEF extern "C" #else @@ -151,8 +170,10 @@ enum { 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); + MINIPSD_ERR_NOT_FOUND = -10, /*!< A lookup found no such item (a resource of that id); not a fault of the file. */ + MINIPSD_ERR_USE_DOC_FIELD = -11 /*!< Typed setters: nothing made, since a field of the + document says it (resource 1005: minipsd_doc.dpi). */ }; /** @@ -270,7 +291,9 @@ enum { minipsd_resource_next_alpha_identifier. */ MINIPSD_RES_VERSION_INFO = 1057, /*!< Version info: minipsd_resource_version_info. */ MINIPSD_RES_EXIF_DATA_1 = 1058, /*!< EXIF data: bytes. */ - MINIPSD_RES_XMP_METADATA = 1060 /*!< XMP metadata, UTF-8 XML: bytes. */ + MINIPSD_RES_XMP_METADATA = 1060, /*!< XMP metadata, UTF-8 XML: bytes. */ + MINIPSD_RES_PIXEL_ASPECT_RATIO = 1064 /*!< The pixel aspect ratio: minipsd_info and + minipsd_doc.pixel_aspect_ratio. */ }; /** \brief One tagged block of a layer's additional information ('luni', 'lsct', 'tySh', ...). */ @@ -484,6 +507,16 @@ typedef struct minipsd_doc { and curves (minipsd_info.color_mode_data), at 32 bits whatever the file held (an 'hdrt' block: HDR toning); size 0 otherwise. */ + double pixel_aspect_ratio; /*!< A pixel's width over its height (resource + 1064). 0: the writer writes none of its own (a + kept 1064 is written as it is). Above 0: a kept + 1064 that says the same stays as it is, where it + is; otherwise the writer writes its own, first + among the kept, as dpi rules 1005. The + resolution stays square. + Photoshop's files in psd-tools' set all hold 1.0: + another value is not verified against a reader. + minipsd_read sets the file's, 0 without one. */ } minipsd_doc; /** @@ -523,6 +556,119 @@ typedef struct minipsd_write_opts { void *deflate_user; /*!< Passed to deflate. */ } minipsd_write_opts; +/** + * \brief Receives the file's bytes, in order. + * + * \param user The pointer given to minipsd_write. + * \param data The next bytes of the file. + * \param size How many bytes data holds; never 0. + * \return Non-zero on success, 0 to abort the write. + */ +typedef int (*minipsd_write_fn)(void *user, const void *data, size_t size); + +/** \brief RGB's planes in a target and channels in a layer view; Lab's are L, a, b, transparency. */ +enum { + MINIPSD_CH_R = 0, /*!< Red. */ + MINIPSD_CH_G = 1, /*!< Green. */ + MINIPSD_CH_B = 2, /*!< Blue. */ + MINIPSD_CH_A = 3 /*!< Transparency (the channel with id -1). */ +}; + +/* --- Typed values: what a resource or a layer's blocks say, read by the typed views and made + by the typed setters ------------------------------------------------------------- */ + +/** + * \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 The bytes resource 1047 takes: one palette entry. */ +#define MINIPSD_TRANSPARENCY_INDEX_SIZE 2 + +/** + * \brief The bytes resource 1032 takes with count guides: the version, two grid values and the + * guide count (four bytes each), then each guide's location (four) and direction (one). + */ +#define MINIPSD_GRID_AND_GUIDES_SIZE(count) (16u + (size_t)(count) * 5u) + +/** \brief The bytes resource 1053 takes with count identifiers, four each. */ +#define MINIPSD_ALPHA_IDENTIFIERS_SIZE(count) ((size_t)(count) * 4u) + +/** \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; + +#ifndef MINIPSD_NO_WRITER /** * \brief Checks that a document can be written, without writing anything. * @@ -560,16 +706,6 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *doc, const minipsd_write_opt /* --- Writer -------------------------------------------------------------------------------- */ -/** - * \brief Receives the file's bytes, in order. - * - * \param user The pointer given to minipsd_write. - * \param data The next bytes of the file. - * \param size How many bytes data holds; never 0. - * \return Non-zero on success, 0 to abort the write. - */ -typedef int (*minipsd_write_fn)(void *user, const void *data, size_t size); - #ifndef MINIPSD_NO_MALLOC /** * \brief Writes a document as PSD (or PSB) through a callback. @@ -668,7 +804,9 @@ MINIPSDDEF uint64_t minipsd_writer_total(const minipsd_writer *w); * \retval MINIPSD_ERR_ARG w or produced is NULL, or buf is NULL with cap above 0. */ MINIPSDDEF int minipsd_writer_fill(minipsd_writer *w, void *buf, size_t cap, size_t *produced); +#endif /* MINIPSD_NO_WRITER */ +#ifndef MINIPSD_NO_READER /* --- Reader -------------------------------------------------------------------------------- */ /** @@ -722,6 +860,9 @@ typedef struct minipsd_info { (256 reds, 256 greens, 256 blues), a duotone file's inks and curves; size 0 for none. minipsd converts no colour: this, or the ICC profile, says what samples mean. */ + double pixel_aspect_ratio; /*!< A pixel's width over its height (resource 1064), as stored; + 1.0 without it. Compare it within a tolerance: a value + off 1.0 is a non-square pixel, to show stretched. */ } minipsd_info; /** @@ -734,14 +875,6 @@ typedef struct minipsd_info { */ #define MINIPSD_PLANES 5 -/** \brief RGB's planes in a target and channels in a layer view; Lab's are L, a, b, transparency. */ -enum { - MINIPSD_CH_R = 0, /*!< Red. */ - MINIPSD_CH_G = 1, /*!< Green. */ - MINIPSD_CH_B = 2, /*!< Blue. */ - MINIPSD_CH_A = 3 /*!< Transparency (the channel with id -1). */ -}; - /** * \brief One layer record, as minipsd_reader_next_layer reads it. * @@ -1131,85 +1264,6 @@ MINIPSDDEF int minipsd_decode_mask(minipsd_reader *r, const minipsd_layer_view * /* --- 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. * @@ -1325,6 +1379,173 @@ MINIPSDDEF const char *minipsd_resource_id_name(uint16_t id); */ MINIPSDDEF int minipsd_layer_block_settings(const minipsd_layer_view *layer, minipsd_block_settings *out); +/** + * \brief The same over a run of blocks alone, such as a document's minipsd_layer.additional_info. + * + * \param blocks The blocks, laid out as format lays them out. + * \param format MINIPSD_PSD (or 0) or MINIPSD_PSB: minipsd_doc.format for a document's layer. + * \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, a size without data, or another format. + */ +MINIPSDDEF int minipsd_blocks_settings(const minipsd_bytes *blocks, int format, minipsd_block_settings *out); +#endif /* MINIPSD_NO_READER */ + +#ifndef MINIPSD_NO_WRITER +/* --- Typed setters: a resource's bytes made from plain values, for the writer ---------------- */ + +/* + * Each setter is the mirror of a typed view: a struct in, the bytes the view reads out, into the + * caller's buffer, ready for a minipsd_resource of that id in minipsd_doc.resources. The bytes + * are counted first: *needed receives their size, and a buffer too small is MINIPSD_MORE, nothing + * written; the sizes below fit the fixed ones, so one call with a buffer of that size does. + */ + +/** + * \brief UTF-8 text as UTF-16BE, as the views give names (minipsd_version_info): the converse of + * minipsd_utf16be_to_utf8. + * + * \param utf8 NUL-terminated; NULL is "". An invalid sequence becomes U+FFFD. + * \param utf16 Receives the UTF-16 units, two bytes each, high first, no terminator; may be NULL + * when cap is 0. + * \param cap The size of utf16 in bytes. + * \param needed Receives the bytes the text takes; may be NULL. + * \return MINIPSD_OK when it fits; MINIPSD_MORE when cap is too small (nothing written); + * MINIPSD_ERR_ARG for utf16 NULL with cap above 0. + */ +MINIPSDDEF int minipsd_utf8_to_utf16be(const char *utf8, uint8_t *utf16, size_t cap, size_t *needed); + +/** + * \brief Resource 1005, ResolutionInfo: not made. The writer writes it from minipsd_doc.dpi, + * so the resolution has one place to be set. + * + * A kept 1005 that says the same dpi is written instead of the writer's own, with its units (the + * spec's "Image resources"); one that says another is dropped. + * + * \return MINIPSD_ERR_USE_DOC_FIELD, always; nothing is written, and no argument is read. + */ +MINIPSDDEF int minipsd_resource_make_resolution_info(const minipsd_resolution_info *info, uint8_t *buf, size_t cap, + size_t *needed); + +/** + * \brief Resource 1064, the pixel aspect ratio: not made. The writer writes it from + * minipsd_doc.pixel_aspect_ratio, so it has one place to be set. + * + * \return MINIPSD_ERR_USE_DOC_FIELD, always; nothing is written, and no argument is read. + */ +MINIPSDDEF int minipsd_resource_make_pixel_aspect_ratio(double ratio, uint8_t *buf, size_t cap, size_t *needed); + +/** + * \brief Resource 1057, version info, as minipsd_resource_version_info reads it. + * + * \param info The values; the names are UTF-16BE, as the view gives them (minipsd_utf8_to_utf16be + * makes them from text), and may be empty. + * \param buf Receives the bytes; may be NULL when cap is 0. + * \param cap The size of buf in bytes. + * \param needed Receives the bytes the resource takes; may be NULL. + * \return MINIPSD_OK; MINIPSD_MORE when cap is too small (nothing written); MINIPSD_ERR_ARG for a + * NULL info, buf NULL with cap above 0, or a name of an odd size or with a size and no data. + */ +MINIPSDDEF int minipsd_resource_make_version_info(const minipsd_version_info *info, uint8_t *buf, size_t cap, + size_t *needed); + +/** + * \brief Resource 1047: which palette entry of an indexed document is transparent. + * + * \param index The entry, 0 to 255 in a palette; written as given. + * \param buf Receives MINIPSD_TRANSPARENCY_INDEX_SIZE bytes; may be NULL when cap is 0. + * \param cap The size of buf in bytes. + * \param needed Receives MINIPSD_TRANSPARENCY_INDEX_SIZE; may be NULL. + * \return MINIPSD_OK; MINIPSD_MORE when cap is too small (nothing written); MINIPSD_ERR_ARG for buf + * NULL with cap above 0. + */ +MINIPSDDEF int minipsd_resource_make_transparency_index(uint16_t index, uint8_t *buf, size_t cap, size_t *needed); + +/** + * \brief Resource 1032, grid and guides information, as minipsd_resource_grid_and_guides and + * minipsd_resource_next_guide read it. + * + * \param grid The header; its guide_count is not read: count says how many guides follow. + * \param guides The guides, in the order to store them; may be NULL when count is 0. + * \param count How many guides. + * \param buf Receives MINIPSD_GRID_AND_GUIDES_SIZE(count) bytes; may be NULL when cap is 0. + * \param cap The size of buf in bytes. + * \param needed Receives MINIPSD_GRID_AND_GUIDES_SIZE(count); may be NULL. + * \return MINIPSD_OK; MINIPSD_MORE when cap is too small (nothing written); MINIPSD_ERR_ARG for a + * NULL grid, guides NULL with count above 0, more guides than a count stores (2^32 - 1), + * or buf NULL with cap above 0. + */ +MINIPSDDEF int minipsd_resource_make_grid_and_guides(const minipsd_grid_and_guides *grid, const minipsd_guide *guides, + size_t count, uint8_t *buf, size_t cap, size_t *needed); + +/** + * \brief The alpha channels' names: resource 1045 (Unicode) or 1006 (Pascal strings), as + * minipsd_resource_next_alpha_name reads them. + * + * Photoshop's files commonly hold both: make 1045 for the names, and 1006 for readers that know + * only it. In 1006 each name is ASCII, one '?' for each other character and at most 255 bytes, + * as the writer's legacy layer name. + * + * \param id MINIPSD_RES_UNICODE_ALPHA_NAMES or MINIPSD_RES_ALPHA_NAMES. + * \param names UTF-8, one a channel in channel order; a NULL name is "". May be NULL when count + * is 0. + * \param count How many names. + * \param buf Receives the bytes; may be NULL when cap is 0. + * \param cap The size of buf in bytes. + * \param needed Receives the bytes the resource takes; may be NULL. + * \return MINIPSD_OK; MINIPSD_MORE when cap is too small (nothing written); MINIPSD_ERR_ARG for + * another id, names NULL with count above 0, or buf NULL with cap above 0. + */ +MINIPSDDEF int minipsd_resource_make_alpha_names(uint16_t id, const char *const *names, size_t count, uint8_t *buf, + size_t cap, size_t *needed); + +/** + * \brief Resource 1053, the alpha channels' identifiers, as minipsd_resource_next_alpha_identifier + * reads them. + * + * \param ids The identifiers, in channel order; may be NULL when count is 0. + * \param count How many. + * \param buf Receives MINIPSD_ALPHA_IDENTIFIERS_SIZE(count) bytes; may be NULL when cap is 0. + * \param cap The size of buf in bytes. + * \param needed Receives MINIPSD_ALPHA_IDENTIFIERS_SIZE(count); may be NULL. + * \return MINIPSD_OK; MINIPSD_MORE when cap is too small (nothing written); MINIPSD_ERR_ARG for ids + * NULL with count above 0, or buf NULL with cap above 0. + */ +MINIPSDDEF int minipsd_resource_make_alpha_identifiers(const uint32_t *ids, size_t count, uint8_t *buf, size_t cap, + size_t *needed); + +/** + * \brief The most bytes minipsd_layer_make_block_settings adds to a run: all seven blocks, each a + * twelve-byte header and its data (eight for 'lclr', four for the others). + */ +#define MINIPSD_BLOCK_SETTINGS_MAX 116u + +/** + * \brief A layer's blocks with its settings set, as minipsd_blocks_settings reads them: the run + * given, each block present asks for replaced where it is, added at the end only when the + * run has none. + * + * Every other block is copied as it is, in its place: text, effects, groups. Readers take the + * first block of a key, so a setting is replaced, never added a second time. For a new layer, + * blocks is empty; the result goes to minipsd_layer.additional_info. + * + * \param blocks The layer's blocks (minipsd_layer.additional_info), laid out as format lays them + * out; NULL is none. + * \param format MINIPSD_PSD (or 0) or MINIPSD_PSB: minipsd_doc.format. + * \param settings The values; present says which to set, the others are not read. + * \param buf Receives the run; at most blocks' size plus MINIPSD_BLOCK_SETTINGS_MAX bytes. + * May be NULL when cap is 0; it must not overlap blocks. + * \param cap The size of buf in bytes. + * \param needed Receives the bytes the run takes; may be NULL. + * \return MINIPSD_OK; MINIPSD_MORE when cap is too small (nothing written); MINIPSD_ERR_FORMAT when + * blocks is not a run of well-formed blocks to its end; MINIPSD_ERR_ARG for a NULL settings, + * a size without data, another format, or buf NULL with cap above 0. + */ +MINIPSDDEF int minipsd_layer_make_block_settings(const minipsd_bytes *blocks, int format, const minipsd_block_settings *settings, + uint8_t *buf, size_t cap, size_t *needed); +#endif /* MINIPSD_NO_WRITER */ + +#if !defined(MINIPSD_NO_READER) && !defined(MINIPSD_NO_WRITER) /* --- Document from a file ------------------------------------------------------------------ */ /** @@ -1430,17 +1651,61 @@ MINIPSDDEF int minipsd_read(const void *data, size_t size, const minipsd_limits MINIPSDDEF void minipsd_doc_free(minipsd_doc *doc); #endif +#endif /* reader and writer */ + #endif /* MINIPSD_H */ /* ============================================================================================== */ #ifdef MINIPSD_IMPLEMENTATION +/* Some functions go unused: public ones under MINIPSD_STATIC, and without the reader or the writer + the helpers in detail/ that only the other one calls. Those are marked MPSD__SHARED, possibly + unused then, so a full build still warns of one nothing calls. MSVC's C has no such + attribute: warning C4505 is off for the implementation instead. */ +#if defined(MINIPSD_STATIC) || defined(MINIPSD_NO_READER) || defined(MINIPSD_NO_WRITER) +# define MPSD__SOME_UNUSED 1 +#else +# define MPSD__SOME_UNUSED 0 +#endif +#if !(defined(MINIPSD_NO_READER) || defined(MINIPSD_NO_WRITER)) +# define MPSD__SHARED +#elif defined(__GNUC__) || defined(__clang__) +# define MPSD__SHARED __attribute__((unused)) +#elif defined(__cplusplus) && (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +# define MPSD__SHARED [[maybe_unused]] +#else +# define MPSD__SHARED +#endif +#if MPSD__SOME_UNUSED && defined(_MSC_VER) && !defined(__clang__) +# pragma warning(push) +# pragma warning(disable: 4505) /* an unreferenced static function removed: one not called */ +#endif #include "minipsd/base.h" +/* The helpers the writer and the reader share, before either: the single file the release attaches + (tools/amalgamate.cmake) holds each header once, where it is first included, so they must not + land inside a part that may be left out. */ +#include "minipsd/detail/bytes.h" +#include "minipsd/detail/format.h" +#include "minipsd/detail/samples.h" +#include "minipsd/detail/utf.h" +#include "minipsd/detail/packbits.h" +#include "minipsd/detail/composite.h" +#include "minipsd/detail/floats.h" +#ifndef MINIPSD_NO_WRITER #include "minipsd/writer.h" +#include "minipsd/builders.h" +#endif +#ifndef MINIPSD_NO_READER #include "minipsd/views.h" #include "minipsd/reader.h" +#endif +#if !defined(MINIPSD_NO_READER) && !defined(MINIPSD_NO_WRITER) #include "minipsd/read.h" #endif +#if MPSD__SOME_UNUSED && defined(_MSC_VER) && !defined(__clang__) +# pragma warning(pop) +#endif +#endif /* -------------------------------------------------------------------------------------------------- diff --git a/include/minipsd/base.h b/include/minipsd/base.h index 47b04cc..7c8739a 100644 --- a/include/minipsd/base.h +++ b/include/minipsd/base.h @@ -13,19 +13,20 @@ MINIPSDDEF const char *minipsd_error_string(int code) { switch (code) { - case MINIPSD_OK: return "success"; - case MINIPSD_MORE: return "not finished: call again"; - case MINIPSD_ERR_ARG: return "invalid argument"; - case MINIPSD_ERR_SIZE: return "too large: a side above the format's (30000 PSD, 300000 PSB), or a section above its length field"; - case MINIPSD_ERR_WRITE: return "write failed"; - case MINIPSD_ERR_ALLOC: return "out of memory"; - case MINIPSD_ERR_OPEN: return "cannot open or close the file"; - case MINIPSD_ERR_FORMAT: return "not a well-formed PSD"; - case MINIPSD_ERR_UNSUPPORTED: return "valid, but outside what minipsd handles yet"; - case MINIPSD_ERR_SHORT: return "the buffer ends too soon: read more and call again"; - case MINIPSD_ERR_LIMIT: return "over a limit set by the caller"; - case MINIPSD_ERR_NOT_FOUND: return "not found"; - default: return "unknown error"; + case MINIPSD_OK: return "success"; + case MINIPSD_MORE: return "not finished: call again"; + case MINIPSD_ERR_ARG: return "invalid argument"; + case MINIPSD_ERR_SIZE: return "too large: a side above the format's (30000 PSD, 300000 PSB), or a section above its length field"; + case MINIPSD_ERR_WRITE: return "write failed"; + case MINIPSD_ERR_ALLOC: return "out of memory"; + case MINIPSD_ERR_OPEN: return "cannot open or close the file"; + case MINIPSD_ERR_FORMAT: return "not a well-formed PSD"; + case MINIPSD_ERR_UNSUPPORTED: return "valid, but outside what minipsd handles yet"; + case MINIPSD_ERR_SHORT: return "the buffer ends too soon: read more and call again"; + case MINIPSD_ERR_LIMIT: return "over a limit set by the caller"; + case MINIPSD_ERR_NOT_FOUND: return "not found"; + case MINIPSD_ERR_USE_DOC_FIELD: return "not made: a field of the document says it"; + default: return "unknown error"; } } diff --git a/include/minipsd/builders.h b/include/minipsd/builders.h new file mode 100644 index 0000000..01e7861 --- /dev/null +++ b/include/minipsd/builders.h @@ -0,0 +1,278 @@ +/** + * \file minipsd/builders.h + * \brief Implementation: typed setters, a resource's bytes made from plain values: the mirror of + * the typed views (views.h). Included by minipsd.h under MINIPSD_IMPLEMENTATION, with the + * writer; the declarations are in minipsd.h. + * + * Each setter describes its bytes once, to a sink: counted first, then, when the caller's buffer + * holds them, written into it. Nothing is allocated or kept. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#ifndef MPSD__BUILDERS_DONE +#define MPSD__BUILDERS_DONE + +#include "detail/bytes.h" +#include "detail/format.h" +#include "detail/utf.h" + +#include + +/* --- the bytes, counted then written --- */ + +/* What a setter makes: its bytes described to a sink, from what. */ +typedef void (*mpsd__make_fn)(mpsd__sink *s, const void *what); + +/* Where the next bytes go in the caller's buffer. */ +typedef struct mpsd__memory { + uint8_t *at; +} mpsd__memory; + +static int mpsd__to_memory(void *user, const void *data, size_t size) +{ + mpsd__memory *m = (mpsd__memory *)user; + memcpy(m->at, data, size); + m->at += size; + return 1; +} + +/* make's bytes into buf when cap holds them, their size into *needed either way. */ +static int mpsd__made(mpsd__make_fn make, const void *what, uint8_t *buf, size_t cap, size_t *needed) +{ + mpsd__sink count = { NULL, NULL, 0, 0 }; + mpsd__sink out = { mpsd__to_memory, NULL, 0, 0 }; + mpsd__memory to; + if (!buf && cap) return MINIPSD_ERR_ARG; + make(&count, what); + if (needed) *needed = (size_t)count.count; + if (cap < count.count) return MINIPSD_MORE; + if (!count.count) return MINIPSD_OK; /* nothing to write: buf may be NULL */ + to.at = buf; + out.user = &to; + make(&out, what); + return MINIPSD_OK; +} + +/* --- text --- */ + +static void mpsd__make_utf16be(mpsd__sink *s, const void *utf8) +{ + (void)mpsd__utf16((const char *)utf8, mpsd__unit_out, s); /* writer.h */ +} + +MINIPSDDEF int minipsd_utf8_to_utf16be(const char *utf8, uint8_t *utf16, size_t cap, size_t *needed) +{ + return mpsd__made(mpsd__make_utf16be, utf8 ? utf8 : "", utf16, cap, needed); +} + +/* --- image resources --- */ + +MINIPSDDEF int minipsd_resource_make_resolution_info(const minipsd_resolution_info *info, uint8_t *buf, size_t cap, + size_t *needed) +{ + (void)info; + (void)buf; + (void)cap; + (void)needed; + return MINIPSD_ERR_USE_DOC_FIELD; +} + +MINIPSDDEF int minipsd_resource_make_pixel_aspect_ratio(double ratio, uint8_t *buf, size_t cap, size_t *needed) +{ + (void)ratio; + (void)buf; + (void)cap; + (void)needed; + return MINIPSD_ERR_USE_DOC_FIELD; +} + +/* A name of version info: its count of UTF-16 units, then the units, as the view reads it. */ +static int mpsd__name_ok(const minipsd_bytes *name) { return name->size % 2 == 0 && (name->data || !name->size); } + +static void mpsd__unicode_out(mpsd__sink *s, const minipsd_bytes *text) +{ + mpsd__u32(s, (uint32_t)(text->size / 2)); + mpsd__put(s, text->data, text->size); +} + +static void mpsd__make_version_info(mpsd__sink *s, const void *what) +{ + const minipsd_version_info *v = (const minipsd_version_info *)what; + mpsd__u32(s, v->version); /* MPSD__VERSION_INFO_HEAD: the version, the flag */ + mpsd__u8(s, v->has_real_merged_data); + mpsd__unicode_out(s, &v->writer_name); + mpsd__unicode_out(s, &v->reader_name); + mpsd__u32(s, v->file_version); +} + +MINIPSDDEF int minipsd_resource_make_version_info(const minipsd_version_info *info, uint8_t *buf, size_t cap, + size_t *needed) +{ + if (!info || !mpsd__name_ok(&info->writer_name) || !mpsd__name_ok(&info->reader_name)) return MINIPSD_ERR_ARG; + return mpsd__made(mpsd__make_version_info, info, buf, cap, needed); +} + +static void mpsd__make_transparency_index(mpsd__sink *s, const void *what) +{ + mpsd__u16(s, *(const uint16_t *)what); /* MINIPSD_TRANSPARENCY_INDEX_SIZE */ +} + +MINIPSDDEF int minipsd_resource_make_transparency_index(uint16_t index, uint8_t *buf, size_t cap, size_t *needed) +{ + return mpsd__made(mpsd__make_transparency_index, &index, buf, cap, needed); +} + +/* --- the lists: guides, alpha channels' names and identifiers --- */ + +typedef struct mpsd__guides_in { + const minipsd_grid_and_guides *grid; + const minipsd_guide *guides; + size_t count; +} mpsd__guides_in; + +static void mpsd__make_grid_and_guides(mpsd__sink *s, const void *what) +{ + const mpsd__guides_in *in = (const mpsd__guides_in *)what; + size_t i; + mpsd__u32(s, in->grid->version); /* MPSD__GUIDES_HEAD */ + mpsd__u32(s, in->grid->horizontal_grid); + mpsd__u32(s, in->grid->vertical_grid); + mpsd__u32(s, (uint32_t)in->count); + for (i = 0; i < in->count; ++i) { /* MPSD__GUIDE_SIZE each */ + mpsd__u32(s, (uint32_t)in->guides[i].location); + mpsd__u8(s, in->guides[i].direction); + } +} + +MINIPSDDEF int minipsd_resource_make_grid_and_guides(const minipsd_grid_and_guides *grid, const minipsd_guide *guides, + size_t count, uint8_t *buf, size_t cap, size_t *needed) +{ + mpsd__guides_in in; + if (!grid || (count && !guides) || (uint64_t)count > UINT32_MAX) return MINIPSD_ERR_ARG; + in.grid = grid; + in.guides = guides; + in.count = count; + return mpsd__made(mpsd__make_grid_and_guides, &in, buf, cap, needed); +} + +typedef struct mpsd__names_in { + unsigned unicode; /* 1045: Unicode strings; 1006: Pascal strings */ + const char *const *names; + size_t count; +} mpsd__names_in; + +static void mpsd__make_alpha_names(mpsd__sink *s, const void *what) +{ + const mpsd__names_in *in = (const mpsd__names_in *)what; + size_t i; + for (i = 0; i < in->count; ++i) { + if (in->unicode) { /* its count of UTF-16 units, then the units */ + mpsd__u32(s, (uint32_t)mpsd__utf16(in->names[i], NULL, NULL)); + (void)mpsd__utf16(in->names[i], mpsd__unit_out, s); + } else { /* a Pascal string, not padded */ + uint8_t pascal[MPSD__PASCAL_MAX + 1]; + mpsd__put(s, pascal, mpsd__legacy_name(in->names[i], pascal) + 1); /* writer.h */ + } + } +} + +MINIPSDDEF int minipsd_resource_make_alpha_names(uint16_t id, const char *const *names, size_t count, uint8_t *buf, + size_t cap, size_t *needed) +{ + mpsd__names_in in; + if ((id != MINIPSD_RES_UNICODE_ALPHA_NAMES && id != MINIPSD_RES_ALPHA_NAMES) || (count && !names)) + return MINIPSD_ERR_ARG; + in.unicode = id == MINIPSD_RES_UNICODE_ALPHA_NAMES; + in.names = names; + in.count = count; + return mpsd__made(mpsd__make_alpha_names, &in, buf, cap, needed); +} + +typedef struct mpsd__ids_in { + const uint32_t *ids; + size_t count; +} mpsd__ids_in; + +static void mpsd__make_alpha_identifiers(mpsd__sink *s, const void *what) +{ + const mpsd__ids_in *in = (const mpsd__ids_in *)what; + size_t i; + for (i = 0; i < in->count; ++i) mpsd__u32(s, in->ids[i]); +} + +MINIPSDDEF int minipsd_resource_make_alpha_identifiers(const uint32_t *ids, size_t count, uint8_t *buf, size_t cap, + size_t *needed) +{ + mpsd__ids_in in; + if (count && !ids) return MINIPSD_ERR_ARG; + in.ids = ids; + in.count = count; + return mpsd__made(mpsd__make_alpha_identifiers, &in, buf, cap, needed); +} + +/* --- a layer's fixed-layout blocks --- */ + +typedef struct mpsd__settings_in { + minipsd_bytes blocks; + unsigned psb; + const minipsd_block_settings *settings; +} mpsd__settings_in; + +/* Setting k's block: the signature given, its key, the length, the value, then zeros. */ +static void mpsd__setting_out(mpsd__sink *s, const uint8_t *signature, const minipsd_block_settings *settings, size_t k) +{ + const uint32_t v = mpsd__setting_get(settings, k); + mpsd__put(s, signature, MPSD__BLOCK_KEY); + mpsd__put(s, mpsd__settings[k].key, MPSD__BLOCK_KEY); + mpsd__u32(s, mpsd__settings[k].span); /* a short key's length: four bytes in a PSB too */ + if (mpsd__settings[k].size == 4) mpsd__u32(s, v); + else if (mpsd__settings[k].size == 2) mpsd__u16(s, v); + else mpsd__u8(s, v); + mpsd__zeros(s, (size_t)(mpsd__settings[k].span - mpsd__settings[k].size)); +} + +/* The run: each block as it is, but the first of each key present asks for, made anew in its + place; then those the run lacked, in the table's order. mpsd__settings_ok walked it. */ +static void mpsd__make_settings(mpsd__sink *s, const void *what) +{ + const mpsd__settings_in *in = (const mpsd__settings_in *)what; + unsigned done = 0; + size_t pos, next, k; + minipsd_block block; + for (pos = 0; pos < in->blocks.size; pos = next) { + const uint8_t *p = in->blocks.data + pos; + (void)mpsd__block(in->blocks, pos, in->psb, &block, &next); + for (k = 0; k < MPSD__SETTINGS; ++k) + if (mpsd__tag((const uint8_t *)block.key, mpsd__settings[k].key)) break; + if (k < MPSD__SETTINGS && (in->settings->present & mpsd__settings[k].bit) && !(done & mpsd__settings[k].bit)) { + mpsd__setting_out(s, p, in->settings, k); + done |= mpsd__settings[k].bit; + } else { + mpsd__put(s, p, next - pos); + } + } + for (k = 0; k < MPSD__SETTINGS; ++k) + if ((in->settings->present & mpsd__settings[k].bit) && !(done & mpsd__settings[k].bit)) + mpsd__setting_out(s, (const uint8_t *)"8BIM", in->settings, k); +} + +MINIPSDDEF int minipsd_layer_make_block_settings(const minipsd_bytes *blocks, int format, const minipsd_block_settings *settings, + uint8_t *buf, size_t cap, size_t *needed) +{ + static const minipsd_bytes none = { NULL, 0 }; + mpsd__settings_in in; + minipsd_block block; + size_t pos, next; + if (!settings || (blocks && !blocks->data && blocks->size) || (format != 0 && format != MINIPSD_PSD && format != MINIPSD_PSB)) + return MINIPSD_ERR_ARG; + in.blocks = blocks ? *blocks : none; + in.psb = format == MINIPSD_PSB; + in.settings = settings; + for (pos = 0; pos < in.blocks.size; pos = next) /* a run of blocks to its end */ + if (mpsd__block(in.blocks, pos, in.psb, &block, &next) != MINIPSD_OK) return MINIPSD_ERR_FORMAT; + return mpsd__made(mpsd__make_settings, &in, buf, cap, needed); +} + +#endif /* MPSD__BUILDERS_DONE */ diff --git a/include/minipsd/detail/bytes.h b/include/minipsd/detail/bytes.h index 42e0700..d19eea7 100644 --- a/include/minipsd/detail/bytes.h +++ b/include/minipsd/detail/bytes.h @@ -22,37 +22,45 @@ typedef struct mpsd__sink { int failed; /* 1 if the write function returned 0, else 0 */ } mpsd__sink; -static void mpsd__put(mpsd__sink *s, const void *data, size_t size) +MPSD__SHARED static void mpsd__put(mpsd__sink *s, const void *data, size_t size) { s->count += size; if (s->fn && !s->failed && size && !s->fn(s->user, data, size)) s->failed = 1; } -static void mpsd__u8(mpsd__sink *s, unsigned v) { uint8_t b = (uint8_t)v; mpsd__put(s, &b, 1); } -static void mpsd__u16(mpsd__sink *s, unsigned v) { uint8_t b[2] = { (uint8_t)(v >> 8), (uint8_t)v }; mpsd__put(s, b, 2); } -static void mpsd__u32(mpsd__sink *s, uint32_t v) +MPSD__SHARED static void mpsd__u8(mpsd__sink *s, unsigned v) { uint8_t b = (uint8_t)v; mpsd__put(s, &b, 1); } +MPSD__SHARED static void mpsd__u16(mpsd__sink *s, unsigned v) { uint8_t b[2] = { (uint8_t)(v >> 8), (uint8_t)v }; mpsd__put(s, b, 2); } +MPSD__SHARED static void mpsd__u32(mpsd__sink *s, uint32_t v) { uint8_t b[4] = { (uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v }; mpsd__put(s, b, 4); } -static void mpsd__zeros(mpsd__sink *s, size_t n) { while (n--) mpsd__u8(s, 0); } +MPSD__SHARED static void mpsd__zeros(mpsd__sink *s, size_t n) { while (n--) mpsd__u8(s, 0); } -static size_t mpsd__pad(size_t n, size_t to) { return (n + to - 1) / to * to; } +MPSD__SHARED static size_t mpsd__pad(size_t n, size_t to) { return (n + to - 1) / to * to; } /* The alignment the writer and the reader give their own state inside a caller's workspace. */ #define MPSD__ALIGN 8u /* --- input: big-endian integers out of a buffer --- */ -static uint32_t mpsd__get16(const uint8_t *p) { return ((uint32_t)p[0] << 8) | p[1]; } -static uint32_t mpsd__get32(const uint8_t *p) { return (mpsd__get16(p) << 16) | mpsd__get16(p + 2); } -static uint64_t mpsd__get64(const uint8_t *p) { return ((uint64_t)mpsd__get32(p) << 32) | mpsd__get32(p + 4); } +MPSD__SHARED static uint32_t mpsd__get16(const uint8_t *p) { return ((uint32_t)p[0] << 8) | p[1]; } +MPSD__SHARED static uint32_t mpsd__get32(const uint8_t *p) { return (mpsd__get16(p) << 16) | mpsd__get16(p + 2); } +MPSD__SHARED static uint64_t mpsd__get64(const uint8_t *p) { return ((uint64_t)mpsd__get32(p) << 32) | mpsd__get32(p + 4); } -static void mpsd__set16(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; } +MPSD__SHARED static double mpsd__get_f64(const uint8_t *p) +{ + const uint64_t bits = mpsd__get64(p); + double v; + memcpy(&v, &bits, sizeof v); + return v; +} + +MPSD__SHARED static void mpsd__set16(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; } /* Four bytes equal to a four-character tag. */ -static int mpsd__tag(const uint8_t *p, const char *tag) +MPSD__SHARED static int mpsd__tag(const uint8_t *p, const char *tag) { size_t i; for (i = 0; i < 4; ++i) diff --git a/include/minipsd/detail/composite.h b/include/minipsd/detail/composite.h index f4770b6..601786f 100644 --- a/include/minipsd/detail/composite.h +++ b/include/minipsd/detail/composite.h @@ -19,21 +19,21 @@ typedef struct mpsd__composite { } mpsd__composite; /* 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)); } +MPSD__SHARED static unsigned mpsd__doc_depth(const minipsd_doc *d) { return d->depth ? d->depth : MPSD__CH_8BIT; } +MPSD__SHARED static unsigned mpsd__doc_shift(const minipsd_doc *d) { return mpsd__shift(mpsd__doc_depth(d)); } /* 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)); } +MPSD__SHARED static uint32_t mpsd__doc_full(const minipsd_doc *d) { return mpsd__full(mpsd__doc_depth(d)); } /* 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) +MPSD__SHARED 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) +MPSD__SHARED 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; @@ -42,10 +42,10 @@ static int mpsd__doc_flat(const minipsd_doc *d) /* 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); } +MPSD__SHARED static unsigned mpsd__doc_colors(const minipsd_doc *d) { return mpsd__colors(mpsd__doc_mode(d), d->composite_channels); } +MPSD__SHARED static unsigned mpsd__doc_pixel(const minipsd_doc *d) { return mpsd__doc_colors(d) + !mpsd__doc_flat(d); } -static int mpsd__contributes(const minipsd_layer *l) { return l->pixels && !l->hidden && l->opacity; } +MPSD__SHARED static int mpsd__contributes(const minipsd_layer *l) { return l->pixels && !l->hidden && l->opacity; } /* A layer's rectangle as written, and the size of its pixels: 0 by 0 for a layer without pixels or without area. All four sides zero with pixels (rgba or stored channels) is the full canvas. @@ -55,7 +55,7 @@ typedef struct mpsd__rect { uint32_t width, height; } mpsd__rect; -static mpsd__rect mpsd__layer_rect(const minipsd_doc *d, const minipsd_layer *l) +MPSD__SHARED 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; @@ -69,7 +69,7 @@ static mpsd__rect mpsd__layer_rect(const minipsd_doc *d, const minipsd_layer *l) return r; } -static void mpsd__composite_init(mpsd__composite *c, const minipsd_doc *d, uint8_t *row) +MPSD__SHARED static void mpsd__composite_init(mpsd__composite *c, const minipsd_doc *d, uint8_t *row) { c->doc = d; c->row = row; @@ -81,7 +81,7 @@ static void mpsd__composite_init(mpsd__composite *c, const minipsd_doc *d, uint8 #endif /* 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) +MPSD__SHARED static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint32_t y) { const unsigned wide = mpsd__doc_shift(d), pixel = (unsigned)MPSD__RGBA << wide; const float max = (float)mpsd__doc_full(d); @@ -120,7 +120,7 @@ static void mpsd__compose(const minipsd_doc *d, uint8_t *row, uint32_t y) 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) +MPSD__SHARED static const uint8_t *mpsd__composite_row(mpsd__composite *c, uint32_t y) { const minipsd_doc *d = c->doc; const unsigned wide = mpsd__doc_shift(d), pixel = mpsd__doc_pixel(d) << wide; @@ -158,7 +158,7 @@ static const uint8_t *mpsd__composite_row(mpsd__composite *c, uint32_t y) bits): c = max - (max - s) * max / a, rounded and clamped; 0 where a is 0, since a transparent pixel has no colour. Exact at a == max; below, matting has already merged nearby colours, about max / (2a) levels apart. */ -static uint32_t mpsd__unmatte(uint32_t s, uint32_t a, uint32_t max) +MPSD__SHARED static uint32_t mpsd__unmatte(uint32_t s, uint32_t a, uint32_t max) { uint32_t d; if (a == 0) return 0; diff --git a/include/minipsd/detail/floats.h b/include/minipsd/detail/floats.h new file mode 100644 index 0000000..c05a257 --- /dev/null +++ b/include/minipsd/detail/floats.h @@ -0,0 +1,25 @@ +/** + * \file minipsd/detail/floats.h + * \brief Internal: floating-point values compared within a tolerance, never with == or !=, and + * checked to be finite; no libm (minipsd links nothing beyond the C library). + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#ifndef MPSD__FLOATS_DONE +#define MPSD__FLOATS_DONE + +#include + +/* Two ratios this close say the same: a pixel aspect ratio kept from a file and the document's. */ +#define MPSD__RATIO_EPSILON 1e-9 + +/* Are a and b within epsilon of each other? */ +MPSD__SHARED static int mpsd__floats_equal(double a, double b) { return a - b < MPSD__RATIO_EPSILON && b - a < MPSD__RATIO_EPSILON; } + +/* Is v a finite value above 0, or at least 0? NaN fails every comparison, so both. */ +MPSD__SHARED static int mpsd__floats_positive(double v) { return v > 0.0 && v <= DBL_MAX; } +MPSD__SHARED static int mpsd__floats_not_negative(double v) { return v >= 0.0 && v <= DBL_MAX; } + +#endif /* MPSD__FLOATS_DONE */ diff --git a/include/minipsd/detail/format.h b/include/minipsd/detail/format.h index c8379d4..1bce862 100644 --- a/include/minipsd/detail/format.h +++ b/include/minipsd/detail/format.h @@ -26,27 +26,27 @@ #define MPSD__LEN 4u /* A length that a PSB widens: four bytes, or (psb) eight. */ -static unsigned mpsd__len_size(unsigned psb) { return MPSD__LEN << psb; } +MPSD__SHARED static unsigned mpsd__len_size(unsigned psb) { return MPSD__LEN << psb; } -static uint64_t mpsd__getlen(const uint8_t *p, unsigned psb) { return psb ? mpsd__get64(p) : mpsd__get32(p); } +MPSD__SHARED static uint64_t mpsd__getlen(const uint8_t *p, unsigned psb) { return psb ? mpsd__get64(p) : mpsd__get32(p); } -static void mpsd__putlen(mpsd__sink *s, uint64_t v, unsigned psb) +MPSD__SHARED static void mpsd__putlen(mpsd__sink *s, uint64_t v, unsigned psb) { if (psb) mpsd__u32(s, (uint32_t)(v >> 32)); mpsd__u32(s, (uint32_t)v); } /* An RLE row's byte count: two bytes, or four in a PSB. */ -static unsigned mpsd__count_size(unsigned psb) { return 2u << psb; } +MPSD__SHARED static unsigned mpsd__count_size(unsigned psb) { return 2u << psb; } /* Row y's byte count in an RLE channel's table. */ -static uint32_t mpsd__count(const uint8_t *table, uint32_t y, unsigned psb) +MPSD__SHARED static uint32_t mpsd__count(const uint8_t *table, uint32_t y, unsigned psb) { const uint8_t *p = table + (size_t)y * mpsd__count_size(psb); return psb ? mpsd__get32(p) : mpsd__get16(p); } -static void mpsd__putcount(mpsd__sink *s, uint32_t v, unsigned psb) +MPSD__SHARED static void mpsd__putcount(mpsd__sink *s, uint32_t v, unsigned psb) { if (psb) mpsd__u32(s, v); else mpsd__u16(s, v); @@ -72,7 +72,7 @@ static void mpsd__putcount(mpsd__sink *s, uint32_t v, unsigned psb) #define MPSD__MAX_CHANNELS 56 /* in the file, the composite's */ /* The largest width or height a format allows. */ -static uint32_t mpsd__max_side(unsigned psb) { return psb ? MINIPSD_MAX_SIDE_PSB : MINIPSD_MAX_SIDE; } +MPSD__SHARED static uint32_t mpsd__max_side(unsigned psb) { return psb ? MINIPSD_MAX_SIDE_PSB : MINIPSD_MAX_SIDE; } /* --- image resources --- @@ -96,14 +96,19 @@ static uint32_t mpsd__max_side(unsigned psb) { return psb ? MINIPSD_MAX_SIDE_PSB /* Grid and guides: version, two grid values and the guide count (4 each), then each guide: its location (4) and direction (1). */ -#define MPSD__GUIDES_HEAD 16 -#define MPSD__GUIDE_SIZE 5 +/* The pixel aspect ratio (1064): a version, then the ratio as a double. Version 2 is what 290 of + psd-tools' 305 files with one hold (the others 1). */ +#define MPSD__PIXEL_ASPECT_SIZE 12 +#define MPSD__PIXEL_ASPECT_RATIO 4 /* where the double starts */ +#define MPSD__PIXEL_ASPECT_VERSION 2u + +#define MPSD__GUIDES_HEAD MINIPSD_GRID_AND_GUIDES_SIZE(0) +#define MPSD__GUIDE_SIZE (MINIPSD_GRID_AND_GUIDES_SIZE(1) - MPSD__GUIDES_HEAD) #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 */ -static uint64_t mpsd__resource_size(size_t name, size_t data) +MPSD__SHARED static uint64_t mpsd__resource_size(size_t name, size_t data) { return MPSD__RES_NAME + (uint64_t)mpsd__pad(1 + name, 2) + MPSD__LEN + (uint64_t)mpsd__pad(data, 2); } @@ -135,15 +140,15 @@ static uint64_t mpsd__resource_size(size_t name, size_t data) #define MPSD__RANGE_SIZE 8 /* a blending range: source and destination */ /* The height of a rectangle laid out as above. */ -static int64_t mpsd__rect_height(const uint8_t *rect) +MPSD__SHARED static int64_t mpsd__rect_height(const uint8_t *rect) { return (int64_t)(int32_t)mpsd__get32(rect + 8) - (int32_t)mpsd__get32(rect); } -static unsigned mpsd__entry_size(unsigned psb) { return 2 + mpsd__len_size(psb); } +MPSD__SHARED static unsigned mpsd__entry_size(unsigned psb) { return 2 + mpsd__len_size(psb); } /* A record without its extra data. */ -static uint64_t mpsd__record_size(uint32_t channels, unsigned psb) +MPSD__SHARED static uint64_t mpsd__record_size(uint32_t channels, unsigned psb) { return MPSD__RECORD_HEAD + (uint64_t)channels * mpsd__entry_size(psb) + MPSD__RECORD_TAIL; } @@ -203,7 +208,7 @@ static uint64_t mpsd__record_size(uint32_t channels, unsigned psb) /* The keys whose block length is eight bytes in a PSB, four in a PSD: the key decides, not the signature. Adobe's specification names 13; psd-tools found 8 more in files Photoshop wrote ('lnkE' and 'pths' are in its test files), and this is its list. */ -static int mpsd__long_key(const uint8_t *key) +MPSD__SHARED static int mpsd__long_key(const uint8_t *key) { static const char keys[] = "LMskLr16Lr32LayrMt16Mt32MtrnAlphFMsklnk2lnk3lnkEFEidFXidFELSPxSDpthsextdextncinfartd"; size_t i; @@ -213,14 +218,14 @@ static int mpsd__long_key(const uint8_t *key) } /* The bytes before the data of the block at p, laid out as a PSB's when psb. */ -static size_t mpsd__block_head(const uint8_t *p, unsigned psb) +MPSD__SHARED static size_t mpsd__block_head(const uint8_t *p, unsigned psb) { return MPSD__BLOCK_LEN + mpsd__len_size(psb && mpsd__long_key(p + MPSD__BLOCK_KEY)); } /* The block at pos of region, laid out as a PSB's when psb; *next is where the one after it starts. The length already includes any padding. */ -static int mpsd__block(minipsd_bytes region, size_t pos, unsigned psb, minipsd_block *block, size_t *next) +MPSD__SHARED static int mpsd__block(minipsd_bytes region, size_t pos, unsigned psb, minipsd_block *block, size_t *next) { const uint8_t *p = region.data + pos; const size_t left = region.size - pos; @@ -247,7 +252,7 @@ static int mpsd__block(minipsd_bytes region, size_t pos, unsigned psb, minipsd_b PSB twins: masks' and transparency's parameters, paths, linked files (whose inner lengths are eight bytes in both) and descriptors. Not yet: the layer structures ('Lr16', 'Lr32', 'Layr', whose data is a layer info of the format), and the long keys no file has shown. */ -static int mpsd__convertible(const uint8_t *key) +MPSD__SHARED static int mpsd__convertible(const uint8_t *key) { static const char keys[] = "FMskLMskMt32pthslnk2lnkEcinfartdextdextn"; size_t i; @@ -260,11 +265,53 @@ static int mpsd__convertible(const uint8_t *key) /* The block at p, whose header takes head bytes and whose length is size, written as the out format lays it out: signature and key, the length in out's width, then the span bytes after the header (the data, and any padding that follows it). */ -static void mpsd__put_block(mpsd__sink *s, const uint8_t *p, size_t head, uint64_t size, size_t span, unsigned out) +MPSD__SHARED static void mpsd__put_block(mpsd__sink *s, const uint8_t *p, size_t head, uint64_t size, size_t span, unsigned out) { mpsd__put(s, p, MPSD__BLOCK_LEN); mpsd__putlen(s, size, out && mpsd__long_key(p + MPSD__BLOCK_KEY)); mpsd__put(s, p + head, span); } +/* --- a layer's fixed-layout blocks: the typed views read them, the setters make them --- */ + +/* Each block's key, the bytes its value takes, the bytes its data takes (the value first, then + zeros: what Photoshop's files in psd-tools' set hold), and its bit in minipsd_block_settings. */ +MPSD__SHARED static const struct { char key[5]; unsigned char size, span, bit; } mpsd__settings[] = { + { "lyid", 4, 4, MINIPSD_PRESENT_LAYER_ID }, { "lclr", 2, 8, MINIPSD_PRESENT_SHEET_COLOR }, + { "iOpa", 1, 4, MINIPSD_PRESENT_FILL_OPACITY }, { "lspf", 4, 4, MINIPSD_PRESENT_PROTECTION_FLAGS }, + { "knko", 1, 4, MINIPSD_PRESENT_KNOCKOUT }, { "clbl", 1, 4, MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS }, + { "infx", 1, 4, MINIPSD_PRESENT_BLEND_INTERIOR_ELEMENTS } +}; +#define MPSD__SETTINGS (sizeof mpsd__settings / sizeof mpsd__settings[0]) + +/* The value of setting k in s. */ +MPSD__SHARED static uint32_t mpsd__setting_get(const minipsd_block_settings *s, size_t k) +{ + switch (mpsd__settings[k].bit) { + case MINIPSD_PRESENT_LAYER_ID: return s->layer_id; + case MINIPSD_PRESENT_SHEET_COLOR: return s->sheet_color; + case MINIPSD_PRESENT_FILL_OPACITY: return s->fill_opacity; + case MINIPSD_PRESENT_PROTECTION_FLAGS: return s->protection_flags; + case MINIPSD_PRESENT_KNOCKOUT: return s->knockout; + case MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS: return s->blend_clipped_elements; + default: return s->blend_interior_elements; + } +} + +/* Setting k of s set to the value at d, of the setting's size, big-endian. */ +MPSD__SHARED static void mpsd__setting_set(minipsd_block_settings *s, size_t k, const uint8_t *d) +{ + const uint32_t v = mpsd__settings[k].size == 4 ? mpsd__get32(d) : mpsd__settings[k].size == 2 ? mpsd__get16(d) : d[0]; + switch (mpsd__settings[k].bit) { + case MINIPSD_PRESENT_LAYER_ID: s->layer_id = v; break; + case MINIPSD_PRESENT_SHEET_COLOR: s->sheet_color = (uint16_t)v; break; + case MINIPSD_PRESENT_FILL_OPACITY: s->fill_opacity = (uint8_t)v; break; + case MINIPSD_PRESENT_PROTECTION_FLAGS: s->protection_flags = v; break; + case MINIPSD_PRESENT_KNOCKOUT: s->knockout = (uint8_t)v; break; + case MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS: s->blend_clipped_elements = (uint8_t)v; break; + default: s->blend_interior_elements = (uint8_t)v; break; + } + s->present = (uint8_t)(s->present | mpsd__settings[k].bit); +} + #endif /* MPSD__FORMAT_DONE */ diff --git a/include/minipsd/detail/packbits.h b/include/minipsd/detail/packbits.h index e4ccfe6..63c06e9 100644 --- a/include/minipsd/detail/packbits.h +++ b/include/minipsd/detail/packbits.h @@ -14,7 +14,7 @@ /* Encodes a row of n bytes as the file stores it, out of samples stride bytes apart (one channel out of interleaved RGBA), of 1 << shift bytes in host order. */ -static void mpsd__packbits(mpsd__sink *s, const uint8_t *src, size_t n, size_t stride, unsigned shift) +MPSD__SHARED static void mpsd__packbits(mpsd__sink *s, const uint8_t *src, size_t n, size_t stride, unsigned shift) { const ptrdiff_t step = (ptrdiff_t)stride; uint8_t packet[129]; @@ -47,7 +47,7 @@ static void mpsd__packbits(mpsd__sink *s, const uint8_t *src, size_t n, size_t s samples step bytes apart, of 1 << shift bytes in host order. Returns 0 when the packets do not hold width bytes, or one would pass the row's end or the width. Bytes left after the last are ignored: some writers pad a row. */ -static int mpsd__unpackbits(const uint8_t *src, size_t n, uint8_t *dst, ptrdiff_t step, uint32_t width, unsigned shift) +MPSD__SHARED static int mpsd__unpackbits(const uint8_t *src, size_t n, uint8_t *dst, ptrdiff_t step, uint32_t width, unsigned shift) { size_t i = 0, k; uint32_t x = 0; diff --git a/include/minipsd/detail/samples.h b/include/minipsd/detail/samples.h index 47db01e..808039a 100644 --- a/include/minipsd/detail/samples.h +++ b/include/minipsd/detail/samples.h @@ -25,7 +25,7 @@ /* A mode's colour channels, before transparency, alpha and spot channels: three in RGB and Lab, four in CMYK, every one of `channels` in multichannel, one in the rest (bitmap, grey, indexed, duotone). Channel ids 0 .. colours - 1 in a layer, the first channels of the composite. */ -static unsigned mpsd__colors(unsigned mode, unsigned channels) +MPSD__SHARED static unsigned mpsd__colors(unsigned mode, unsigned channels) { switch (mode) { case MINIPSD_MODE_RGB: case MINIPSD_MODE_LAB: return MPSD__TRIPLE_COLOR_CHAN; @@ -36,7 +36,7 @@ static unsigned mpsd__colors(unsigned mode, unsigned channels) } /* 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); } +MPSD__SHARED 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 --- */ @@ -59,24 +59,24 @@ static int mpsd__mode_ok(uint32_t mode) { return mode <= MINIPSD_MODE_CMYK || (m #define MPSD__FULL_32 0x3F800000u /* 1.0f's bits, IEEE 754 */ /* Is depth one a file may have? */ -static int mpsd__depth_ok(uint32_t depth) +MPSD__SHARED static int mpsd__depth_ok(uint32_t depth) { return depth == MPSD__CH_1BIT || depth == MPSD__CH_8BIT || depth == MPSD__CH_16BIT || depth == MPSD__CH_32BIT; } -static unsigned mpsd__shift(unsigned depth) +MPSD__SHARED static unsigned mpsd__shift(unsigned depth) { return depth == MPSD__CH_32BIT ? MPSD__SHIFT_32 : depth == MPSD__CH_16BIT ? MPSD__SHIFT_16 : MPSD__SHIFT_8; } /* A row of width samples as the file stores it: big-endian samples, or at 1 bit eight pixels a byte, the last one padded. */ -static uint64_t mpsd__row_bytes(uint32_t width, unsigned depth) +MPSD__SHARED static uint64_t mpsd__row_bytes(uint32_t width, unsigned depth) { return ((uint64_t)width * depth + MPSD__BYTE_BITS - 1) / MPSD__BYTE_BITS; } -static uint32_t mpsd__full(unsigned depth) +MPSD__SHARED static uint32_t mpsd__full(unsigned depth) { return depth == MPSD__CH_32BIT ? MPSD__FULL_32 : depth == MPSD__CH_16BIT ? MPSD__FULL_16 : depth == MPSD__CH_1BIT ? MPSD__FULL_1 : MPSD__FULL_8; @@ -84,7 +84,7 @@ static uint32_t mpsd__full(unsigned depth) /* Where byte i of a row as the file stores it (big-endian) lies in memory, the samples step bytes apart: a sample's high byte comes first in the file, last in memory on a little-endian host. */ -static ptrdiff_t mpsd__at(size_t i, ptrdiff_t step, unsigned shift) +MPSD__SHARED static ptrdiff_t mpsd__at(size_t i, ptrdiff_t step, unsigned shift) { static const uint16_t one = 1; const size_t byte = ((size_t)1 << shift) - 1; /* the bits of i that pick a sample's byte */ @@ -93,7 +93,7 @@ static ptrdiff_t mpsd__at(size_t i, ptrdiff_t step, unsigned shift) } /* A sample in memory: a byte or a uint16_t. 32-bit samples are floats: mpsd__float. */ -static uint32_t mpsd__sample(const uint8_t *p, unsigned shift) +MPSD__SHARED static uint32_t mpsd__sample(const uint8_t *p, unsigned shift) { uint16_t h; if (shift == MPSD__SHIFT_8) return *p; @@ -101,7 +101,7 @@ static uint32_t mpsd__sample(const uint8_t *p, unsigned shift) return h; } -static void mpsd__set_sample(uint8_t *p, unsigned shift, uint32_t v) +MPSD__SHARED static void mpsd__set_sample(uint8_t *p, unsigned shift, uint32_t v) { const uint16_t h = (uint16_t)v; if (shift == MPSD__SHIFT_8) *p = (uint8_t)v; @@ -109,14 +109,14 @@ static void mpsd__set_sample(uint8_t *p, unsigned shift, uint32_t v) else memcpy(p, &v, sizeof v); } -static float mpsd__float(const uint8_t *p) +MPSD__SHARED static float mpsd__float(const uint8_t *p) { float f; memcpy(&f, p, sizeof f); return f; } -static void mpsd__set_float(uint8_t *p, float f) { memcpy(p, &f, sizeof f); } +MPSD__SHARED static void mpsd__set_float(uint8_t *p, float f) { memcpy(p, &f, sizeof f); } /* --- ZIP prediction: each sample stored as its difference from its left neighbour --- @@ -126,20 +126,20 @@ static void mpsd__set_float(uint8_t *p, float f) { memcpy(p, &f, sizeof f); } /* Where byte i of a 32-bit row as the file stores it lies once the row is shuffled: its byte of the sample picks the run, the sample its place in the run. */ -static size_t mpsd__shuffled(size_t i, uint32_t width) +MPSD__SHARED static size_t mpsd__shuffled(size_t i, uint32_t width) { const size_t byte = ((size_t)1 << MPSD__SHIFT_32) - 1; return (i & byte) * width + (i >> MPSD__SHIFT_32); } /* What the differences of a row go over: its samples at 16 bits, its bytes otherwise. */ -static size_t mpsd__units(uint32_t width, unsigned depth) +MPSD__SHARED static size_t mpsd__units(uint32_t width, unsigned depth) { return depth == MPSD__CH_16BIT ? width : (size_t)mpsd__row_bytes(width, depth); } /* Unit x of a row, from its left neighbour: the difference made (sum 0), or undone (sum 1). */ -static void mpsd__delta(uint8_t *row, size_t x, unsigned depth, int sum) +MPSD__SHARED static void mpsd__delta(uint8_t *row, size_t x, unsigned depth, int sum) { if (depth == MPSD__CH_16BIT) { /* big-endian, as the raw rows are */ uint8_t *s = row + (x << MPSD__SHIFT_16); @@ -151,14 +151,14 @@ static void mpsd__delta(uint8_t *row, size_t x, unsigned depth, int sum) } /* A row of the file's bytes made differences, in place: right to left. */ -static void mpsd__predict(uint8_t *row, uint32_t width, unsigned depth) +MPSD__SHARED static void mpsd__predict(uint8_t *row, uint32_t width, unsigned depth) { size_t x = mpsd__units(width, depth); while (x-- > 1) mpsd__delta(row, x, depth, 0); } /* The differences of a row summed back, in place: left to right. A 32-bit row stays shuffled. */ -static void mpsd__unpredict(uint8_t *row, uint32_t width, unsigned depth) +MPSD__SHARED static void mpsd__unpredict(uint8_t *row, uint32_t width, unsigned depth) { const size_t n = mpsd__units(width, depth); size_t x; @@ -168,7 +168,7 @@ static void mpsd__unpredict(uint8_t *row, uint32_t width, unsigned depth) /* --- 1 bit: eight pixels a byte, the first in the high bit; a set bit is black --- */ /* Pixel x of a packed row: 1 or 0. */ -static uint8_t mpsd__bit(uint8_t packed, uint32_t x) +MPSD__SHARED static uint8_t mpsd__bit(uint8_t packed, uint32_t x) { return (uint8_t)(((unsigned)packed >> (MPSD__BYTE_BITS - 1u - x % MPSD__BYTE_BITS)) & 1u); } @@ -176,7 +176,7 @@ static uint8_t mpsd__bit(uint8_t packed, uint32_t x) /* A packed row, held in the first (width + 7) / 8 samples of a row in memory, step bytes apart, spread over its width samples in place, a byte of 0 or 1 each. Right to left: sample x is read from sample x / 8, never one already written. */ -static void mpsd__unpack_bits(uint8_t *row, ptrdiff_t step, uint32_t width) +MPSD__SHARED static void mpsd__unpack_bits(uint8_t *row, ptrdiff_t step, uint32_t width) { uint32_t x = width; while (x-- > 0) row[(ptrdiff_t)x * step] = mpsd__bit(row[(ptrdiff_t)(x / MPSD__BYTE_BITS) * step], x); diff --git a/include/minipsd/detail/utf.h b/include/minipsd/detail/utf.h index 260594e..196235b 100644 --- a/include/minipsd/detail/utf.h +++ b/include/minipsd/detail/utf.h @@ -13,7 +13,7 @@ #include "bytes.h" /* Decodes one UTF-8 code point; malformed input yields U+FFFD and moves on by one byte. */ -static uint32_t mpsd__utf8(const unsigned char **p) +MPSD__SHARED static uint32_t mpsd__utf8(const unsigned char **p) { const unsigned char *s = *p; uint32_t cp; @@ -32,7 +32,7 @@ static uint32_t mpsd__utf8(const unsigned char **p) } /* Calls back with each UTF-16 unit of a UTF-8 string; returns how many there were. */ -static size_t mpsd__utf16(const char *utf8, void (*unit)(void *, unsigned), void *ctx) +MPSD__SHARED static size_t mpsd__utf16(const char *utf8, void (*unit)(void *, unsigned), void *ctx) { const unsigned char *p = (const unsigned char *)(utf8 ? utf8 : ""); size_t n = 0; @@ -51,7 +51,7 @@ static size_t mpsd__utf16(const char *utf8, void (*unit)(void *, unsigned), void /* Decodes one code point of UTF-16BE from [*p, end), which holds at least two bytes; an unpaired surrogate yields U+FFFD. */ -static uint32_t mpsd__utf16be(const uint8_t **p, const uint8_t *end) +MPSD__SHARED static uint32_t mpsd__utf16be(const uint8_t **p, const uint8_t *end) { const uint32_t hi = mpsd__get16(*p); *p += 2; @@ -68,7 +68,7 @@ static uint32_t mpsd__utf16be(const uint8_t **p, const uint8_t *end) /* Encodes one code point (at most 0x10FFFF, no surrogates) as UTF-8 into out, or only counts when out is NULL; returns the bytes it takes. */ -static size_t mpsd__put_utf8(uint8_t *out, uint32_t cp) +MPSD__SHARED static size_t mpsd__put_utf8(uint8_t *out, uint32_t cp) { uint8_t b[4]; size_t n, i; @@ -84,7 +84,7 @@ static size_t mpsd__put_utf8(uint8_t *out, uint32_t cp) /* UTF-16BE [p, p + size) as UTF-8 into out, or only its length when out is NULL: up to a U+0000, an odd last byte dropped. */ -static size_t mpsd__utf16be_utf8(const uint8_t *p, size_t size, uint8_t *out) +MPSD__SHARED static size_t mpsd__utf16be_utf8(const uint8_t *p, size_t size, uint8_t *out) { const uint8_t *end; size_t n = 0; @@ -100,7 +100,7 @@ static size_t mpsd__utf16be_utf8(const uint8_t *p, size_t size, uint8_t *out) /* Does a UTF-8 name say what a file's name bytes say, as the reader reads them: UTF-16BE units up to a U+0000, or Latin-1 bytes (latin1)? Code point by code point, U+FFFD for what is broken. */ -static int mpsd__same_name(const char *utf8, const uint8_t *bytes, size_t size, int latin1) +MPSD__SHARED static int mpsd__same_name(const char *utf8, const uint8_t *bytes, size_t size, int latin1) { const unsigned char *p = (const unsigned char *)(utf8 ? utf8 : ""); const uint8_t *q = bytes; diff --git a/include/minipsd/read.h b/include/minipsd/read.h index 4128bcd..e138cbc 100644 --- a/include/minipsd/read.h +++ b/include/minipsd/read.h @@ -17,6 +17,7 @@ #include "detail/bytes.h" #include "detail/format.h" +#include "detail/floats.h" #include @@ -320,6 +321,12 @@ static int mpsd__read(const void *data, size_t size, const minipsd_limits *limit d->composite_stored = composite; d->composite_channels = keep_composite ? (uint16_t)info.channels : 0; d->composite_alpha = (uint8_t)(keep_composite && !flat_mode && info.composite_has_alpha && info.channels > r->colors); + { /* the file's pixel aspect ratio, kept when it is one the writer takes; else 0: none of its own */ + minipsd_resource aspect; + const double ratio = info.pixel_aspect_ratio; + d->pixel_aspect_ratio = minipsd_reader_find_resource(r, MINIPSD_RES_PIXEL_ASPECT_RATIO, &aspect) == MINIPSD_OK + && mpsd__floats_positive(ratio) ? ratio : 0.0; + } 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 9001e88..eef3e0f 100644 --- a/include/minipsd/reader.h +++ b/include/minipsd/reader.h @@ -124,6 +124,7 @@ static void mpsd__info_clear(minipsd_info *i) i->width = i->height = 0; i->channels = i->depth = i->color_mode = 0; i->dpi = 0; + i->pixel_aspect_ratio = 1.0; /* none stored: square */ i->icc.data = NULL; i->icc.size = 0; i->layer_count = 0; @@ -196,7 +197,7 @@ static int mpsd__scan(minipsd_reader *r, minipsd_info *info) uint64_t at, lm_start, length; uint32_t side; unsigned psb; - int rc, has_dpi = 0, has_icc = 0; + int rc, has_dpi = 0, has_icc = 0, has_aspect = 0; mpsd__info_clear(&in); /* The header: a stream that is not a PSD is refused as soon as four bytes show it. */ @@ -241,6 +242,10 @@ static int mpsd__scan(minipsd_reader *r, minipsd_info *info) if (res.data.size < MPSD__RESOLUTION_SIZE) return MINIPSD_ERR_FORMAT; in.dpi = (double)mpsd__get32(res.data.data) / MPSD__FIXED_ONE; has_dpi = 1; + } else if (res.id == MINIPSD_RES_PIXEL_ASPECT_RATIO && !has_aspect) { + if (res.data.size < MPSD__PIXEL_ASPECT_SIZE) return MINIPSD_ERR_FORMAT; + in.pixel_aspect_ratio = mpsd__get_f64(res.data.data + MPSD__PIXEL_ASPECT_RATIO); + has_aspect = 1; } else if (res.id == MINIPSD_RES_ICC_PROFILE && !has_icc) { in.icc = res.data; has_icc = 1; diff --git a/include/minipsd/views.h b/include/minipsd/views.h index 7039c54..2887821 100644 --- a/include/minipsd/views.h +++ b/include/minipsd/views.h @@ -171,7 +171,7 @@ MINIPSDDEF int minipsd_resource_next_alpha_identifier(const minipsd_resource *re MINIPSDDEF int minipsd_resource_transparency_index(const minipsd_resource *res, uint16_t *index) { if (!index || !mpsd__resource_of(res, MINIPSD_RES_TRANSPARENCY_INDEX)) return MINIPSD_ERR_ARG; - if (res->data.size < MPSD__TRANSPARENCY_INDEX_SIZE) return MINIPSD_ERR_FORMAT; + if (res->data.size < MINIPSD_TRANSPARENCY_INDEX_SIZE) return MINIPSD_ERR_FORMAT; *index = (uint16_t)mpsd__get16(res->data.data); return MINIPSD_OK; } @@ -288,43 +288,31 @@ MINIPSDDEF const char *minipsd_resource_id_name(uint16_t id) /* --- a layer's fixed-layout blocks --- */ -/* The blocks minipsd_layer_block_settings reads: key, the bytes its value needs, its present bit. */ -static const struct { char key[5]; unsigned char size, bit; } mpsd__settings[] = { - { "lyid", 4, MINIPSD_PRESENT_LAYER_ID }, { "lclr", 2, MINIPSD_PRESENT_SHEET_COLOR }, - { "iOpa", 1, MINIPSD_PRESENT_FILL_OPACITY }, { "lspf", 4, MINIPSD_PRESENT_PROTECTION_FLAGS }, - { "knko", 1, MINIPSD_PRESENT_KNOCKOUT }, { "clbl", 1, MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS }, - { "infx", 1, MINIPSD_PRESENT_BLEND_INTERIOR_ELEMENTS } -}; - -MINIPSDDEF int minipsd_layer_block_settings(const minipsd_layer_view *layer, minipsd_block_settings *out) +MINIPSDDEF int minipsd_blocks_settings(const minipsd_bytes *blocks, int format, minipsd_block_settings *out) { minipsd_block_settings s; minipsd_block block; - size_t pos = 0, k; - int rc; - if (!layer || !out) return MINIPSD_ERR_ARG; + size_t pos = 0, next, k; + if (!blocks || !out || (!blocks->data && blocks->size) || (format != 0 && format != MINIPSD_PSD && format != MINIPSD_PSB)) + return MINIPSD_ERR_ARG; memset(&s, 0, sizeof s); s.fill_opacity = MPSD__FILL_OPAQUE; - while ((rc = minipsd_layer_next_block(layer, &pos, &block)) == MINIPSD_MORE) { - for (k = 0; k < sizeof mpsd__settings / sizeof mpsd__settings[0]; ++k) { - const uint8_t *d = block.data.data; + for (; pos < blocks->size; pos = next) { + if (mpsd__block(*blocks, pos, format == MINIPSD_PSB, &block, &next) != MINIPSD_OK) return MINIPSD_ERR_FORMAT; + for (k = 0; k < MPSD__SETTINGS; ++k) { /* the first block of each key counts */ if (!mpsd__tag((const uint8_t *)block.key, mpsd__settings[k].key) || (s.present & mpsd__settings[k].bit)) continue; if (block.data.size < mpsd__settings[k].size) return MINIPSD_ERR_FORMAT; - s.present |= mpsd__settings[k].bit; - switch (mpsd__settings[k].bit) { - case MINIPSD_PRESENT_LAYER_ID: s.layer_id = mpsd__get32(d); break; - case MINIPSD_PRESENT_SHEET_COLOR: s.sheet_color = (uint16_t)mpsd__get16(d); break; - case MINIPSD_PRESENT_FILL_OPACITY: s.fill_opacity = d[0]; break; - case MINIPSD_PRESENT_PROTECTION_FLAGS: s.protection_flags = mpsd__get32(d); break; - case MINIPSD_PRESENT_KNOCKOUT: s.knockout = d[0]; break; - case MINIPSD_PRESENT_BLEND_CLIPPED_ELEMENTS: s.blend_clipped_elements = d[0]; break; - default: s.blend_interior_elements = d[0]; break; - } + mpsd__setting_set(&s, k, block.data.data); } } - if (rc != MINIPSD_OK) return rc; *out = s; return MINIPSD_OK; } +MINIPSDDEF int minipsd_layer_block_settings(const minipsd_layer_view *layer, minipsd_block_settings *out) +{ + if (!layer) return MINIPSD_ERR_ARG; + return minipsd_blocks_settings(&layer->additional_info, layer->format, out); +} + #endif /* MPSD__VIEWS_DONE */ diff --git a/include/minipsd/writer.h b/include/minipsd/writer.h index 928ba78..3685afb 100644 --- a/include/minipsd/writer.h +++ b/include/minipsd/writer.h @@ -27,6 +27,7 @@ #include "detail/utf.h" #include "detail/packbits.h" #include "detail/composite.h" +#include "detail/floats.h" /* 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; } @@ -232,6 +233,22 @@ static mpsd__source mpsd__composite_source(const minipsd_doc *d) /* --- names: a legacy ASCII Pascal string and the UTF-16 one --- */ +#define MPSD__PASCAL_MAX 255 /* a Pascal string's bytes: its length is one byte */ + +/* A UTF-8 name as a legacy Pascal string into pascal (MPSD__PASCAL_MAX + 1 bytes): ASCII, one '?' + per other character, at most MPSD__PASCAL_MAX; returns its length, also in pascal[0]. */ +static size_t mpsd__legacy_name(const char *utf8, uint8_t *pascal) +{ + const unsigned char *p = (const unsigned char *)(utf8 ? utf8 : ""); + size_t ascii = 0; + while (*p && ascii < MPSD__PASCAL_MAX) { + const uint32_t cp = mpsd__utf8(&p); + pascal[1 + ascii++] = (uint8_t)(cp < 0x80 ? cp : '?'); + } + pascal[0] = (uint8_t)ascii; + return ascii; +} + static void mpsd__unit_out(void *ctx, unsigned u) { mpsd__u16((mpsd__sink *)ctx, u); } /* The section type an 'lsct' block holds; -1 for any other block, or one too short to say. */ @@ -353,21 +370,16 @@ static void mpsd__blending_ranges(mpsd__sink *s, const minipsd_layer *l) out as the from format lays them out, written in the out format). */ static void mpsd__layer_extra(mpsd__sink *s, const minipsd_layer *l, unsigned from, unsigned out) { - const char *name = l->name; - const unsigned char *p = (const unsigned char *)(name ? name : ""); - const int named = mpsd__kept_names(l, from); - size_t ascii = 0; - uint8_t pascal[256]; + const int named = mpsd__kept_names(l, from); + size_t ascii = 0; + uint8_t pascal[MPSD__PASCAL_MAX + 1]; if (named) { /* the kept legacy name, as it is */ ascii = l->name_pascal.size; if (ascii) memcpy(pascal + 1, l->name_pascal.data, ascii); + pascal[0] = (uint8_t)ascii; } else { - while (*p && ascii < 255) { /* one '?' per non-ASCII character */ - const uint32_t cp = mpsd__utf8(&p); - pascal[1 + ascii++] = (uint8_t)(cp < 0x80 ? cp : '?'); - } + ascii = mpsd__legacy_name(l->name, pascal); } - pascal[0] = (uint8_t)ascii; mpsd__mask_data(s, l->mask); mpsd__blending_ranges(s, l); mpsd__put(s, pascal, ascii + 1); @@ -441,8 +453,16 @@ static void mpsd__layer_record(mpsd__sink *s, const minipsd_doc *d, const minips static uint64_t mpsd__max_layers_section(unsigned psb) { return psb ? MPSD__FAR - 1 : MPSD__MAX_SECTION; } -/* A kept resource is written unless the document's own field says the same: dpi, icc. */ -static int mpsd__kept_resource(const minipsd_resource *r) { return r->id != MINIPSD_RES_RESOLUTION_INFO && r->id != MINIPSD_RES_ICC_PROFILE; } +static const minipsd_resource *mpsd__find_resource(const minipsd_doc *d, unsigned id); +static int mpsd__aspect_kept(const minipsd_doc *d); + +/* A kept resource is written unless the document's own field says it: dpi, icc, and a pixel + aspect ratio the document sets that no kept 1064 says. */ +static int mpsd__kept_resource(const minipsd_doc *d, const minipsd_resource *r) +{ + return r->id != MINIPSD_RES_RESOLUTION_INFO && r->id != MINIPSD_RES_ICC_PROFILE + && !(r->id == MINIPSD_RES_PIXEL_ASPECT_RATIO && d->pixel_aspect_ratio > 0.0 && !mpsd__aspect_kept(d)); +} /* The dpi as stored: 16.16 fixed point. */ static uint32_t mpsd__dpi_fixed(const minipsd_doc *d) { return (uint32_t)(d->dpi * MPSD__FIXED_ONE + 0.5); } @@ -495,8 +515,35 @@ static void mpsd__resolution(mpsd__sink *s, const minipsd_doc *d, minipsd_bytes mpsd__put_resource(s, MINIPSD_RES_RESOLUTION_INFO, name, kept ? kept : own, MPSD__RESOLUTION_SIZE); } -/* The image resources: the writer's own first (the resolution, the ICC profile if any), then the - kept ones. A kept resolution or profile goes: the document's field says it. */ +/* Does the first kept 1064 say the ratio the document sets? Then it stays as it is, where it is, + as a file read and written back has it. */ +static int mpsd__aspect_kept(const minipsd_doc *d) +{ + const minipsd_resource *kept = mpsd__find_resource(d, MINIPSD_RES_PIXEL_ASPECT_RATIO); + return kept && kept->data.size == MPSD__PIXEL_ASPECT_SIZE && kept->data.data + && mpsd__floats_equal(mpsd__get_f64(kept->data.data + MPSD__PIXEL_ASPECT_RATIO), d->pixel_aspect_ratio); +} + +/* The writer's own pixel aspect ratio, version MPSD__PIXEL_ASPECT_VERSION, named as a kept 1064 + it replaces. */ +static void mpsd__pixel_aspect(mpsd__sink *s, const minipsd_doc *d) +{ + const minipsd_resource *kept = mpsd__find_resource(d, MINIPSD_RES_PIXEL_ASPECT_RATIO); + const minipsd_bytes none = { NULL, 0 }; + uint8_t own[MPSD__PIXEL_ASPECT_SIZE]; + uint64_t bits; + size_t i; + memcpy(&bits, &d->pixel_aspect_ratio, sizeof bits); + for (i = 0; i < MPSD__PIXEL_ASPECT_RATIO; ++i) /* the version, then the double, big-endian */ + own[i] = (uint8_t)(MPSD__PIXEL_ASPECT_VERSION >> (8 * (MPSD__PIXEL_ASPECT_RATIO - 1 - i))); + for (i = MPSD__PIXEL_ASPECT_RATIO; i < MPSD__PIXEL_ASPECT_SIZE; ++i) + own[i] = (uint8_t)(bits >> (8 * (MPSD__PIXEL_ASPECT_SIZE - 1 - i))); + mpsd__put_resource(s, MINIPSD_RES_PIXEL_ASPECT_RATIO, kept ? kept->name : none, own, sizeof own); +} + +/* The image resources: the writer's own first (the resolution, the ICC profile if any, the pixel + aspect ratio if set), then the kept ones. A kept one of these goes: the document's field says + it. */ static void mpsd__resources(mpsd__sink *s, const minipsd_doc *d) { const minipsd_resource *res = mpsd__find_resource(d, MINIPSD_RES_RESOLUTION_INFO); @@ -505,8 +552,9 @@ static void mpsd__resources(mpsd__sink *s, const minipsd_doc *d) size_t i; mpsd__resolution(s, d, res ? res->name : none); if (d->icc) mpsd__put_resource(s, MINIPSD_RES_ICC_PROFILE, icc ? icc->name : none, d->icc, d->icc_size); + if (d->pixel_aspect_ratio > 0.0 && !mpsd__aspect_kept(d)) mpsd__pixel_aspect(s, d); for (i = 0; i < d->resource_count; ++i) - if (mpsd__kept_resource(&d->resources[i])) + if (mpsd__kept_resource(d, &d->resources[i])) mpsd__put_resource(s, d->resources[i].id, d->resources[i].name, d->resources[i].data.data, d->resources[i].data.size); } @@ -782,7 +830,8 @@ MINIPSDDEF int minipsd_doc_check(const minipsd_doc *d, const minipsd_write_opts if ((d->layer_count && !d->layers) || (!d->layer_count && !d->composite && !d->composite_stored.size) || d->layer_count > MPSD__MAX_LAYERS || !(res >= 1.0 && res < 4294967296.0) || (d->depth != 0 && d->depth != MPSD__CH_1BIT && d->depth != MPSD__CH_8BIT && d->depth != MPSD__CH_16BIT && d->depth != MPSD__CH_32BIT) || d->format > MINIPSD_PSB || d->compression < MINIPSD_RLE || d->compression > MINIPSD_ZIP_PREDICTED || (mpsd__zip(d) && !(opts && opts->deflate)) - || (d->color_mode_data.size && !d->color_mode_data.data)) + || (d->color_mode_data.size && !d->color_mode_data.data) + || !mpsd__floats_not_negative(d->pixel_aspect_ratio)) return MINIPSD_ERR_ARG; rc = mpsd__doc_mode_ok(d); if (rc != MINIPSD_OK) return rc; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 171547d..7b97054 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -152,6 +152,37 @@ foreach(_check include no-malloc) set_target_properties(minipsd-${_check}-check PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) endforeach() +# A program that uses part of minipsd builds without warnings, as C and as C++17: private to it +# (MINIPSD_STATIC) and calling only minipsd_write, without the reader, without the writer. +foreach(_variant static no-reader no-writer static-no-reader static-no-writer) + set(_defines "") + if(_variant MATCHES "static") + list(APPEND _defines MINIPSD_STATIC) + endif() + if(_variant MATCHES "no-reader") + list(APPEND _defines MINIPSD_NO_READER) + elseif(_variant MATCHES "no-writer") + list(APPEND _defines MINIPSD_NO_WRITER) + endif() + foreach(_lang c cpp) + set(_target minipsd-part-${_variant}-${_lang}-check) + add_library(${_target} OBJECT part_check.${_lang}) + target_link_libraries(${_target} PRIVATE minipsd::minipsd) + target_compile_definitions(${_target} PRIVATE ${_defines}) + target_compile_options(${_target} PRIVATE ${MINIPSD_WARNINGS}) + set_target_properties(${_target} PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF + CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) + if(_variant MATCHES "^no-") # the single file too: a part left out takes no shared helper with it + add_library(${_target}-amalgamated OBJECT part_check.${_lang}) + target_include_directories(${_target}-amalgamated PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/amalgamated") + target_compile_definitions(${_target}-amalgamated PRIVATE ${_defines}) + target_compile_options(${_target}-amalgamated PRIVATE ${MINIPSD_WARNINGS}) + set_target_properties(${_target}-amalgamated PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF + CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) + endif() + endforeach() +endforeach() + # The reader's fuzz target (fuzz/): replayed here on every file in fuzz/regressions/, which are # inputs that once failed; with MINIPSD_FUZZ (the linux-fuzz preset, clang) also built as a # libFuzzer target, run by hand and never by CI. diff --git a/tests/minipsd_c_smoke.c b/tests/minipsd_c_smoke.c index eccea49..005ce48 100644 --- a/tests/minipsd_c_smoke.c +++ b/tests/minipsd_c_smoke.c @@ -106,12 +106,24 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size) made.name.data = NULL; made.name.size = 0; if (minipsd_reader_find_resource(rd, MINIPSD_RES_RESOLUTION_INFO, &res) != MINIPSD_OK || minipsd_resource_resolution_info(&res, &resolution) != MINIPSD_OK - || resolution.vertical_resolution != 300.0 + || !mpsd__floats_equal(resolution.vertical_resolution, 300.0) || minipsd_reader_find_resource(rd, MINIPSD_RES_VERSION_INFO, &res) != MINIPSD_ERR_NOT_FOUND || minipsd_resource_version_info(&made, &version) != MINIPSD_OK || !version.has_real_merged_data || minipsd_utf16be_to_utf8(&version.writer_name, name, sizeof name, NULL) != MINIPSD_OK || memcmp(name, "A", 2) != 0) return -116; + { /* the setters, the mirror: the same bytes back; 1005 is the document's dpi */ + uint8_t remade[sizeof vi], unit[2], ti[MINIPSD_TRANSPARENCY_INDEX_SIZE]; + size_t made_size = 0; + if (minipsd_resource_make_version_info(&version, remade, sizeof remade, &made_size) != MINIPSD_OK + || made_size != sizeof vi || memcmp(remade, vi, sizeof vi) != 0 + || minipsd_utf8_to_utf16be("A", unit, sizeof unit, &made_size) != MINIPSD_OK || made_size != 2 || unit[1] != 'A' + || minipsd_resource_make_transparency_index(7, ti, sizeof ti, NULL) != MINIPSD_OK || ti[1] != 7 + || minipsd_resource_make_resolution_info(&resolution, NULL, 0, NULL) != MINIPSD_ERR_USE_DOC_FIELD + || minipsd_resource_make_pixel_aspect_ratio(1.0, NULL, 0, NULL) != MINIPSD_ERR_USE_DOC_FIELD + || !mpsd__floats_equal(info.pixel_aspect_ratio, 1.0)) + return -122; + } } { /* the other typed views, each over a resource built by hand: one item, then the end */ static const uint8_t guides[] = { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 1 }; @@ -142,6 +154,20 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size) || strcmp(minipsd_resource_id_name(MINIPSD_RES_TRANSPARENCY_INDEX), "Transparency index") != 0 || minipsd_resource_id_name(1004) != NULL) return -120; + { /* the setters: the same bytes from the values */ + static const char *const one_name[] = { "a" }; + const minipsd_guide one_guide = { 64, 1 }; + const uint32_t one_id = 9; + uint8_t list[MINIPSD_GRID_AND_GUIDES_SIZE(1)]; + size_t list_size = 0; + if (minipsd_resource_make_grid_and_guides(&grid, &one_guide, 1, list, sizeof list, &list_size) != MINIPSD_OK + || list_size != sizeof guides || memcmp(list, guides, sizeof guides) != 0 + || minipsd_resource_make_alpha_names(MINIPSD_RES_ALPHA_NAMES, one_name, 1, list, sizeof list, &list_size) != MINIPSD_OK + || list_size != sizeof names || memcmp(list, names, sizeof names) != 0 + || minipsd_resource_make_alpha_identifiers(&one_id, 1, list, sizeof list, &list_size) != MINIPSD_OK + || list_size != sizeof ids || memcmp(list, ids, sizeof ids) != 0) + return -123; + } } if (minipsd_reader_next_layer(rd, &lv) != MINIPSD_MORE || minipsd_layer_name(&lv, name, sizeof name, &rneed) != MINIPSD_OK @@ -158,6 +184,21 @@ int minipsd_c_smoke(unsigned char *out, size_t capacity, size_t *size) if (minipsd_layer_block_settings(&lv, &settings) != MINIPSD_OK || settings.present != 0 || settings.fill_opacity != 255) return -121; + { /* the colour label set on the layer's blocks, read back from the bytes alone */ + uint8_t run[256 + MINIPSD_BLOCK_SETTINGS_MAX]; + size_t run_size = 0; + minipsd_bytes edited; + settings.present = MINIPSD_PRESENT_SHEET_COLOR; + settings.sheet_color = 3; + if (lv.additional_info.size > 256 + || minipsd_layer_make_block_settings(&lv.additional_info, lv.format, &settings, run, sizeof run, &run_size) != MINIPSD_OK) + return -124; + edited.data = run; + edited.size = run_size; + if (minipsd_blocks_settings(&edited, lv.format, &settings) != MINIPSD_OK + || settings.present != MINIPSD_PRESENT_SHEET_COLOR || settings.sheet_color != 3) + return -125; + } } if (blocks != 1 || minipsd_reader_next_layer(rd, &lv) != MINIPSD_MORE || minipsd_layer_blending_range(&lv, 0, &br) != MINIPSD_OK || br.destination.white[0] != 210 diff --git a/tests/part_check.c b/tests/part_check.c new file mode 100644 index 0000000..cda3817 --- /dev/null +++ b/tests/part_check.c @@ -0,0 +1,53 @@ +/** + * \file + * \brief A program that uses part of minipsd: the implementation in this file, private to it + * under MINIPSD_STATIC, only writing (as it is, or MINIPSD_NO_READER) or only reading + * (MINIPSD_NO_WRITER). Built, never run, as C and as C++17 (part_check.cpp), each variant + * set by tests/CMakeLists.txt: the check is that nothing it leaves uncalled warns. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#define MINIPSD_IMPLEMENTATION +#include "minipsd.h" + +#include + +#ifdef MINIPSD_NO_WRITER + +int main(void) +{ + size_t size = 0; + return minipsd_reader_workspace_size(&size) == MINIPSD_OK ? 0 : 1; +} + +#else + +static int discard(void *user, const void *data, size_t size) +{ + (void)user; + (void)data; + (void)size; + return 1; +} + +int main(void) +{ + static const uint8_t pixel[4] = {255, 0, 0, 255}; + minipsd_layer layer; + minipsd_doc doc; + memset(&layer, 0, sizeof layer); + memset(&doc, 0, sizeof doc); + layer.name = "Layer"; + layer.pixels = pixel; + layer.opacity = 255; + doc.width = 1; + doc.height = 1; + doc.dpi = 72.0; + doc.layers = &layer; + doc.layer_count = 1; + return minipsd_write(&doc, NULL, discard, NULL) == MINIPSD_OK ? 0 : 1; +} + +#endif diff --git a/tests/part_check.cpp b/tests/part_check.cpp new file mode 100644 index 0000000..df788c7 --- /dev/null +++ b/tests/part_check.cpp @@ -0,0 +1,9 @@ +/** + * \file + * \brief part_check.c compiled as C++17: a C++ program that uses part of minipsd. + * + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2026 Bartlomiej Mucha + */ + +#include "part_check.c" diff --git a/tests/psd_tools/roundtrip.py b/tests/psd_tools/roundtrip.py index ece4fb2..5003108 100644 --- a/tests/psd_tools/roundtrip.py +++ b/tests/psd_tools/roundtrip.py @@ -124,7 +124,8 @@ class Doc(ctypes.Structure): ("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_alpha", ctypes.c_uint8), ("color_mode", ctypes.c_uint16), ("color_mode_data", Bytes)] + ("composite_alpha", ctypes.c_uint8), ("color_mode", ctypes.c_uint16), ("color_mode_data", Bytes), + ("pixel_aspect_ratio", ctypes.c_double)] DEFLATE_FN = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, @@ -722,6 +723,105 @@ def expect(what, got, wanted): return errors +# --- the typed setters: resources and a layer's blocks made from values --------------------------- + +SETTER_NAMES = ["Alpha 1", "Spot \u2713"] # 1006 holds the second as "Spot ?" +SETTER_GUIDES = [(10 * 32, 0), (-32, 1)] # a column at x = 10, a row above the canvas +SETTER_IDS = [5, 7] +SETTER_ASPECT = 0.9 # minipsd_doc.pixel_aspect_ratio: the writer's own 1064 +FLOAT_EPSILON = 1e-9 # floats are compared within it, never with == +SETTER_SETTINGS = {"layer_id": 42, "sheet_color": 3, "fill_opacity": 200, "protection_flags": 0x05, "knockout": 1, + "blend_clipped_elements": 0, "blend_interior_elements": 1} + + +def setters_document(lib, path): + """A document whose resources (1057, 1032, 1045, 1006, 1053, 1047) and whose layer's blocks the + typed setters made, each measured first, then made into a buffer of that size.""" + out_args = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] + lib.minipsd_utf8_to_utf16be.argtypes = [ctypes.c_char_p] + out_args + lib.minipsd_resource_make_version_info.argtypes = [ctypes.POINTER(VersionInfo)] + out_args + lib.minipsd_resource_make_grid_and_guides.argtypes = [ctypes.POINTER(GridAndGuides), ctypes.POINTER(Guide), + ctypes.c_size_t] + out_args + lib.minipsd_resource_make_alpha_names.argtypes = [ctypes.c_uint16, ctypes.POINTER(ctypes.c_char_p), + ctypes.c_size_t] + out_args + lib.minipsd_resource_make_alpha_identifiers.argtypes = [ctypes.POINTER(ctypes.c_uint32), ctypes.c_size_t] + out_args + lib.minipsd_resource_make_transparency_index.argtypes = [ctypes.c_uint16] + out_args + lib.minipsd_layer_make_block_settings.argtypes = [ctypes.POINTER(Bytes), ctypes.c_int, + ctypes.POINTER(BlockSettings)] + out_args + keep = [] + + def made(fn, *args): + need = ctypes.c_size_t() + if fn(*args, None, 0, ctypes.byref(need)) not in (MINIPSD_OK, MINIPSD_MORE): + raise RuntimeError(f"{fn.__name__}: refused") + buf = ctypes.create_string_buffer(max(need.value, 1)) + keep.append(buf) + rc = fn(*args, ctypes.cast(buf, ctypes.c_void_p), need.value, ctypes.byref(need)) + if rc != MINIPSD_OK: + raise RuntimeError(f"{fn.__name__} returned {rc}") + return Bytes(ctypes.cast(buf, ctypes.c_void_p), need.value) + + version = VersionInfo(1, 1, made(lib.minipsd_utf8_to_utf16be, b"minipsd"), + made(lib.minipsd_utf8_to_utf16be, b"Photoshop"), 1) + guides = (Guide * len(SETTER_GUIDES))(*[Guide(*g) for g in SETTER_GUIDES]) + names = (ctypes.c_char_p * len(SETTER_NAMES))(*[n.encode() for n in SETTER_NAMES]) + ids = (ctypes.c_uint32 * len(SETTER_IDS))(*SETTER_IDS) + resources = (Resource * 6)( + Resource(RES_VERSION_INFO, made(lib.minipsd_resource_make_version_info, ctypes.byref(version)), Bytes()), + Resource(RES_GRID_AND_GUIDES, made(lib.minipsd_resource_make_grid_and_guides, + ctypes.byref(GridAndGuides(1, 576, 576, 0)), guides, len(guides)), Bytes()), + Resource(RES_UNICODE_ALPHA_NAMES, made(lib.minipsd_resource_make_alpha_names, RES_UNICODE_ALPHA_NAMES, + names, len(names)), Bytes()), + Resource(RES_ALPHA_NAMES, made(lib.minipsd_resource_make_alpha_names, RES_ALPHA_NAMES, names, len(names)), + Bytes()), + Resource(RES_ALPHA_IDENTIFIERS, made(lib.minipsd_resource_make_alpha_identifiers, ids, len(ids)), Bytes()), + Resource(RES_TRANSPARENCY_INDEX, made(lib.minipsd_resource_make_transparency_index, 3), Bytes())) + settings = BlockSettings(present=0x7F, **SETTER_SETTINGS) + pixels = (ctypes.c_uint8 * 16)(*([255, 0, 0, 255] * 4)) + layer = (Layer * 1)() + layer[0] = Layer(name=b"Layer", pixels=ctypes.cast(pixels, ctypes.c_void_p), opacity=255, + additional_info=made(lib.minipsd_layer_make_block_settings, None, 0, ctypes.byref(settings))) + doc = Doc(2, 2, 72.0, layer, 1, None, 0, MINIPSD_RLE) + doc.resources, doc.resource_count = ctypes.cast(resources, ctypes.c_void_p), len(resources) + doc.pixel_aspect_ratio = SETTER_ASPECT + 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 + + +def check_setters_document(psd, name): + """psd-tools reads each made resource and block as the values the setters were given.""" + errors = [] + + def expect(what, got, want): + if got != want: + errors.append(f"{name}: {what} is {got!r}, expected {want!r}") + + res = psd.image_resources + v = res.get_data(RES_VERSION_INFO) + expect("version info", (v.version, bool(v.has_composite), v.writer.split("\0")[0], v.reader.split("\0")[0], + v.file_version), (1, True, "minipsd", "Photoshop", 1)) + g = res.get_data(RES_GRID_AND_GUIDES) + expect("grid and guides", (g.version, g.horizontal, g.vertical, [tuple(x) for x in g.data]), + (1, 576, 576, [(loc & 0xFFFFFFFF, d) for loc, d in SETTER_GUIDES])) + expect("Unicode alpha names", [n.split("\0")[0] for n in res.get_data(RES_UNICODE_ALPHA_NAMES)], SETTER_NAMES) + expect("Pascal alpha names", list(res.get_data(RES_ALPHA_NAMES)), ["Alpha 1", "Spot ?"]) + expect("alpha identifiers", list(res.get_data(RES_ALPHA_IDENTIFIERS)), SETTER_IDS) + expect("transparency index", as_int(res.get_data(RES_TRANSPARENCY_INDEX)), 3) + aspect = res.get_data(1064) # psd-tools gives the ratio itself + expect("pixel aspect ratio within epsilon", abs(aspect - SETTER_ASPECT) < FLOAT_EPSILON, True) + blocks = psd._record.layer_and_mask_information.layer_info.layer_records[0].tagged_blocks + expect("layer blocks", {field: as_int(blocks.get_data(key)) for key, field, _ in SETTINGS}, SETTER_SETTINGS) + 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. @@ -1228,6 +1328,13 @@ def main(): errors += found print(f"{name}: {'FAILED' if found else 'ok'}") + path = out / "setters.psd" + rc = setters_document(lib, path) + found = [f"setters: minipsd_write returned {rc}"] if rc != MINIPSD_OK else ( + check_setters_document(PSDImage.open(path), "setters") + check_decoded(lib, path)) + errors += found + print(f"setters: {'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" diff --git a/tests/test_base.cpp b/tests/test_base.cpp index 6dad1cf..13b48ec 100644 --- a/tests/test_base.cpp +++ b/tests/test_base.cpp @@ -27,7 +27,7 @@ TEST(MinipsdBaseTest, OnlyErrorsAreNegative) EXPECT_GT(MINIPSD_MORE, 0) << "rc < 0 is the failure test for every function"; for (int code : {MINIPSD_ERR_ARG, MINIPSD_ERR_SIZE, MINIPSD_ERR_WRITE, MINIPSD_ERR_ALLOC, MINIPSD_ERR_OPEN, MINIPSD_ERR_FORMAT, MINIPSD_ERR_UNSUPPORTED, MINIPSD_ERR_SHORT, MINIPSD_ERR_LIMIT, - MINIPSD_ERR_NOT_FOUND}) + MINIPSD_ERR_NOT_FOUND, MINIPSD_ERR_USE_DOC_FIELD}) EXPECT_LT(code, 0); } @@ -38,6 +38,6 @@ TEST(MinipsdBaseTest, EveryReturnCodeHasItsOwnText) EXPECT_STREQ(minipsd_error_string(-100), "unknown error"); for (int code : {MINIPSD_OK, MINIPSD_MORE, MINIPSD_ERR_ARG, MINIPSD_ERR_WRITE, MINIPSD_ERR_ALLOC, MINIPSD_ERR_OPEN, MINIPSD_ERR_FORMAT, MINIPSD_ERR_UNSUPPORTED, MINIPSD_ERR_SHORT, MINIPSD_ERR_LIMIT, - MINIPSD_ERR_NOT_FOUND}) + MINIPSD_ERR_NOT_FOUND, MINIPSD_ERR_USE_DOC_FIELD}) EXPECT_STRNE(minipsd_error_string(code), "unknown error") << "every return code has its own text: " << code; } diff --git a/tests/test_document.cpp b/tests/test_document.cpp index 66a29dd..2cb782d 100644 --- a/tests/test_document.cpp +++ b/tests/test_document.cpp @@ -109,6 +109,13 @@ TEST(MinipsdDocumentTest, RejectsBadDocuments) 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"; + // A pixel aspect ratio: 0 (none of the writer's own) or a finite value above it. + for (double ratio : {-1.0, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + d = docOf(2, 2, &layer, 1); d.pixel_aspect_ratio = ratio; + EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_ERR_ARG) << "pixel aspect ratio " << ratio; + } + d.pixel_aspect_ratio = 0.9; + EXPECT_EQ(minipsd_doc_check(&d, nullptr), MINIPSD_OK) << "a non-square pixel"; 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); diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp index 215b102..9a3a5c1 100644 --- a/tests/test_reader.cpp +++ b/tests/test_reader.cpp @@ -602,7 +602,8 @@ TEST(MinipsdReaderTest, AFileWithoutResourcesHasNoDpiAndNoProfile) s.colorModeData = Bytes(10, 0xCC); // skipped Open o(build(s).bytes); ASSERT_EQ(o.rc, MINIPSD_OK); - EXPECT_EQ(o.info.dpi, 0.0); + EXPECT_DOUBLE_EQ(o.info.dpi, 0.0); + EXPECT_DOUBLE_EQ(o.info.pixel_aspect_ratio, 1.0) << "no 1064: square"; EXPECT_EQ(o.info.icc.size, 0u); minipsd_resource res; std::size_t pos = 0; @@ -619,6 +620,7 @@ TEST(MinipsdReaderTest, MalformedResourcesAreRefused) EXPECT_EQ(rc(bad), MINIPSD_ERR_FORMAT) << "signature"; EXPECT_EQ(rc(Bytes(11, 0)), MINIPSD_ERR_FORMAT) << "less than the smallest block"; EXPECT_EQ(rc(resource(1005, Bytes(8, 0))), MINIPSD_ERR_FORMAT) << "a resolution of 8 bytes"; + EXPECT_EQ(rc(resource(1064, Bytes(8, 0))), MINIPSD_ERR_FORMAT) << "a pixel aspect ratio of 8 bytes"; Bytes name = resource(1036, Bytes(2, 0)); // a name length that runs past the section name[6] = 255; @@ -681,6 +683,23 @@ TEST(MinipsdReaderTest, ResolutionAndVersionInfoAreParsedFieldByField) EXPECT_EQ(minipsd_utf16be_to_utf8(&vi.writer_name, text, sizeof text, nullptr), MINIPSD_OK); EXPECT_STREQ(text, "Ps") << "the NUL counted in the name ends it"; + // The setters, the mirror: version info made from the view's struct is the bytes it came from, + // measured first; 1005 is the document's dpi, never made. + std::size_t needed = 0; + EXPECT_EQ(minipsd_resource_make_version_info(&vi, nullptr, 0, &needed), MINIPSD_MORE); + ASSERT_EQ(needed, vo.b.size()); + Bytes made(needed, 0xEE); + EXPECT_EQ(minipsd_resource_make_version_info(&vi, made.data(), needed - 1, nullptr), MINIPSD_MORE); + EXPECT_EQ(made, Bytes(needed, 0xEE)) << "too small: nothing written"; + EXPECT_EQ(minipsd_resource_make_version_info(&vi, made.data(), made.size(), nullptr), MINIPSD_OK); + EXPECT_EQ(made, vo.b); + minipsd_version_info odd = vi; + odd.reader_name.size = 1; + EXPECT_EQ(minipsd_resource_make_version_info(&odd, made.data(), made.size(), nullptr), MINIPSD_ERR_ARG) << "half a unit"; + EXPECT_EQ(minipsd_resource_make_version_info(nullptr, made.data(), made.size(), nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_resource_make_version_info(&vi, nullptr, 1, nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_resource_make_resolution_info(&ri, made.data(), made.size(), &needed), MINIPSD_ERR_USE_DOC_FIELD); + for (std::size_t cut = 0; cut < vo.b.size(); ++cut) { // every prefix ends inside a field const Bytes part(vo.b.begin(), vo.b.begin() + static_cast(cut)); minipsd_version_info untouched{}; @@ -732,6 +751,20 @@ TEST(MinipsdReaderTest, GuidesAlphaChannelsAndTheTransparencyIndexAreParsed) pos = 0; EXPECT_EQ(minipsd_resource_grid_and_guides(&cutGuides, &grid), MINIPSD_ERR_FORMAT) << "two guides counted, one and a bit stored"; EXPECT_EQ(minipsd_resource_next_guide(&cutGuides, &pos, &guide), MINIPSD_ERR_FORMAT); + // The setter: the same bytes from the header and the guides, its size known before; the count + // given wins over the header's. + const minipsd_guide two[2] = {{10 * 32, 0}, {-32, 1}}; + std::uint8_t madeGuides[MINIPSD_GRID_AND_GUIDES_SIZE(2)]; + std::size_t size = 0; + minipsd_grid_and_guides stale = grid; + stale.guide_count = 9; + EXPECT_EQ(minipsd_resource_make_grid_and_guides(&stale, two, 2, madeGuides, sizeof madeGuides, &size), MINIPSD_OK); + EXPECT_EQ(Bytes(madeGuides, madeGuides + size), guides); + EXPECT_EQ(minipsd_resource_make_grid_and_guides(&grid, two, 2, madeGuides, sizeof madeGuides - 1, nullptr), MINIPSD_MORE); + EXPECT_EQ(minipsd_resource_make_grid_and_guides(&grid, nullptr, 0, madeGuides, sizeof madeGuides, &size), MINIPSD_OK); + EXPECT_EQ(size, MINIPSD_GRID_AND_GUIDES_SIZE(0)) << "no guides: the header"; + EXPECT_EQ(minipsd_resource_make_grid_and_guides(&grid, nullptr, 1, madeGuides, sizeof madeGuides, nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_resource_make_grid_and_guides(nullptr, two, 2, madeGuides, sizeof madeGuides, nullptr), MINIPSD_ERR_ARG); // Alpha channel names, Pascal (1006) and Unicode (1045); a name past the data is refused. const auto names = [](const minipsd_resource &res) { @@ -753,6 +786,24 @@ TEST(MinipsdReaderTest, GuidesAlphaChannelsAndTheTransparencyIndexAreParsed) EXPECT_EQ(minipsd_resource_next_alpha_name(&longPascal, &pos, &name), MINIPSD_ERR_FORMAT); const minipsd_resource longUnicode = of(MINIPSD_RES_UNICODE_ALPHA_NAMES, Out().u32(2).u16('A').b); EXPECT_EQ(minipsd_resource_next_alpha_name(&longUnicode, &pos, &name), MINIPSD_ERR_FORMAT); + // The setter, from UTF-8: Unicode as given, a pair for the turtle; Pascal ASCII, '?' for each + // other character, at most 255 bytes; a NULL name is "". + const std::string longName(300, 'x'); + const char *const alphaNames[] = {"A\xF0\x9F\x90\xA2", nullptr, longName.c_str()}; + const auto make = [&](std::uint16_t nameId) { + std::size_t need = 0; + EXPECT_EQ(minipsd_resource_make_alpha_names(nameId, alphaNames, 3, nullptr, 0, &need), MINIPSD_MORE); + Bytes out(need); + EXPECT_EQ(minipsd_resource_make_alpha_names(nameId, alphaNames, 3, out.data(), out.size(), nullptr), MINIPSD_OK); + return names(of(nameId, out)); + }; + Bytes longUnits; + for (int i = 0; i < 300; ++i) longUnits.insert(longUnits.end(), {0, 'x'}); + EXPECT_EQ(make(MINIPSD_RES_UNICODE_ALPHA_NAMES), (std::vector{{0, 'A', 0xD8, 0x3D, 0xDC, 0x22}, {}, longUnits})); + EXPECT_EQ(make(MINIPSD_RES_ALPHA_NAMES), (std::vector{{'A', '?'}, {}, Bytes(255, 'x')})); + EXPECT_EQ(minipsd_resource_make_alpha_names(MINIPSD_RES_ALPHA_IDENTIFIERS, alphaNames, 3, nullptr, 0, nullptr), + MINIPSD_ERR_ARG) << "another id"; + EXPECT_EQ(minipsd_resource_make_alpha_names(MINIPSD_RES_ALPHA_NAMES, nullptr, 1, nullptr, 0, nullptr), MINIPSD_ERR_ARG); // Alpha identifiers, four bytes each. const minipsd_resource ids = of(MINIPSD_RES_ALPHA_IDENTIFIERS, Out().u32(5).u32(0xABCDEF01u).b); @@ -766,6 +817,12 @@ TEST(MinipsdReaderTest, GuidesAlphaChannelsAndTheTransparencyIndexAreParsed) pos = 0; const minipsd_resource oddIds = of(MINIPSD_RES_ALPHA_IDENTIFIERS, Bytes(6, 0)); EXPECT_EQ(minipsd_resource_next_alpha_identifier(&oddIds, &pos, &id), MINIPSD_ERR_FORMAT); + const std::uint32_t twoIds[2] = {5, 0xABCDEF01u}; // the setter: the same bytes + std::uint8_t madeIds[MINIPSD_ALPHA_IDENTIFIERS_SIZE(2)]; + EXPECT_EQ(minipsd_resource_make_alpha_identifiers(twoIds, 2, madeIds, sizeof madeIds, &size), MINIPSD_OK); + EXPECT_EQ(Bytes(madeIds, madeIds + size), Bytes(ids.data.data, ids.data.data + ids.data.size)); + EXPECT_EQ(minipsd_resource_make_alpha_identifiers(twoIds, 2, madeIds, sizeof madeIds - 1, nullptr), MINIPSD_MORE); + EXPECT_EQ(minipsd_resource_make_alpha_identifiers(nullptr, 2, madeIds, sizeof madeIds, nullptr), MINIPSD_ERR_ARG); // The transparency index. std::uint16_t index = 0; @@ -774,6 +831,12 @@ TEST(MinipsdReaderTest, GuidesAlphaChannelsAndTheTransparencyIndexAreParsed) EXPECT_EQ(index, 200); const minipsd_resource shortTi = of(MINIPSD_RES_TRANSPARENCY_INDEX, Bytes{0}); EXPECT_EQ(minipsd_resource_transparency_index(&shortTi, &index), MINIPSD_ERR_FORMAT); + std::uint8_t madeTi[MINIPSD_TRANSPARENCY_INDEX_SIZE]; // the setter: one call, the size known + std::size_t tiSize = 0; + EXPECT_EQ(minipsd_resource_make_transparency_index(200, madeTi, sizeof madeTi, &tiSize), MINIPSD_OK); + EXPECT_EQ(Bytes(madeTi, madeTi + tiSize), (Bytes{0, 200})); + EXPECT_EQ(minipsd_resource_make_transparency_index(200, madeTi, 1, nullptr), MINIPSD_MORE); + EXPECT_EQ(minipsd_resource_make_transparency_index(200, nullptr, 1, nullptr), MINIPSD_ERR_ARG); // Another id, or a NULL, is the caller's mistake. pos = 0; @@ -1165,6 +1228,77 @@ TEST(MinipsdReaderTest, FixedLayoutBlocksGiveTheLayersSettings) const minipsd_layer_view none{}; EXPECT_EQ(minipsd_layer_block_settings(nullptr, &s), MINIPSD_ERR_ARG); EXPECT_EQ(minipsd_layer_block_settings(&none, nullptr), MINIPSD_ERR_ARG); + + // The setter edits a run: the first block of each key asked for replaced where it is, every + // other block as it is, the missing ones added at the end; the view over the bytes alone reads + // the result. A second 'iOpa' stays: readers never see it. + const Bytes kept = Out() + .raw(luniBlock(u"L")) + .raw(block("lclr", Out().u16(4).zeros(6).b)) + .raw(block("iOpa", Bytes{128, 0, 0, 0})) + .raw(block("iOpa", Bytes{7, 0, 0, 0})) + .b; + minipsd_block_settings set{}; + set.present = MINIPSD_PRESENT_SHEET_COLOR | MINIPSD_PRESENT_FILL_OPACITY | MINIPSD_PRESENT_LAYER_ID; + set.sheet_color = 6; + set.fill_opacity = 50; + set.layer_id = 9; + set.knockout = 2; // not present: not read + const Bytes expected = Out() + .raw(luniBlock(u"L")) + .raw(block("lclr", Out().u16(6).zeros(6).b)) + .raw(block("iOpa", Bytes{50, 0, 0, 0})) + .raw(block("iOpa", Bytes{7, 0, 0, 0})) + .raw(block("lyid", Out().u32(9).b)) + .b; + const minipsd_bytes keptRun{kept.data(), kept.size()}; + std::size_t need = 0; + EXPECT_EQ(minipsd_layer_make_block_settings(&keptRun, MINIPSD_PSD, &set, nullptr, 0, &need), MINIPSD_MORE); + ASSERT_EQ(need, expected.size()); + Bytes edited(kept.size() + MINIPSD_BLOCK_SETTINGS_MAX); // the bound: one call + ASSERT_EQ(minipsd_layer_make_block_settings(&keptRun, 0, &set, edited.data(), edited.size(), &need), MINIPSD_OK); + edited.resize(need); + EXPECT_EQ(edited, expected); + const minipsd_bytes editedRun{edited.data(), edited.size()}; + minipsd_block_settings back{}; + ASSERT_EQ(minipsd_blocks_settings(&editedRun, MINIPSD_PSD, &back), MINIPSD_OK); + EXPECT_EQ(std::tie(back.present, back.sheet_color, back.fill_opacity, back.layer_id), + std::make_tuple(std::uint8_t(set.present), std::uint16_t(6), std::uint8_t(50), 9u)); + set.present = 0; + ASSERT_EQ(minipsd_layer_make_block_settings(&keptRun, MINIPSD_PSD, &set, edited.data(), kept.size(), &need), MINIPSD_OK); + EXPECT_EQ(Bytes(edited.begin(), edited.begin() + std::ptrdiff_t(need)), kept) << "nothing asked: the run as it is"; + + // A new layer: every block, the bound exactly, read back as set; in a PSB, a long key's + // eight-byte length kept as it is. + minipsd_block_settings every = s; + every.present = 0x7F; + Bytes fresh(MINIPSD_BLOCK_SETTINGS_MAX); + ASSERT_EQ(minipsd_layer_make_block_settings(nullptr, MINIPSD_PSD, &every, fresh.data(), fresh.size(), &need), MINIPSD_OK); + EXPECT_EQ(need, std::size_t{MINIPSD_BLOCK_SETTINGS_MAX}); + const minipsd_bytes freshRun{fresh.data(), need}; + ASSERT_EQ(minipsd_blocks_settings(&freshRun, MINIPSD_PSD, &back), MINIPSD_OK); + EXPECT_EQ(std::tie(back.present, back.layer_id, back.sheet_color, back.fill_opacity, back.protection_flags, back.knockout, + back.blend_clipped_elements, back.blend_interior_elements), + std::tie(every.present, every.layer_id, every.sheet_color, every.fill_opacity, every.protection_flags, + every.knockout, every.blend_clipped_elements, every.blend_interior_elements)); + const Bytes psb = Out().raw(block("LMsk", Bytes(4, 1), "8BIM", true)).raw(block("lclr", Out().u16(4).zeros(6).b)).b; + const minipsd_bytes psbRun{psb.data(), psb.size()}; + set.present = MINIPSD_PRESENT_SHEET_COLOR; + Bytes psbOut(psb.size()); + ASSERT_EQ(minipsd_layer_make_block_settings(&psbRun, MINIPSD_PSB, &set, psbOut.data(), psbOut.size(), &need), MINIPSD_OK); + EXPECT_EQ(psbOut, Out().raw(block("LMsk", Bytes(4, 1), "8BIM", true)).raw(block("lclr", Out().u16(6).zeros(6).b)).b); + + // Refused: a malformed run, before anything is written; a NULL, a size without data, another format. + const minipsd_bytes cutRun{cut.data(), cut.size()}, noData{nullptr, 4}; + EXPECT_EQ(minipsd_layer_make_block_settings(&cutRun, MINIPSD_PSD, &set, edited.data(), edited.size(), nullptr), MINIPSD_ERR_FORMAT); + EXPECT_EQ(minipsd_blocks_settings(&cutRun, MINIPSD_PSD, &back), MINIPSD_ERR_FORMAT); + EXPECT_EQ(minipsd_layer_make_block_settings(&keptRun, MINIPSD_PSD, nullptr, edited.data(), edited.size(), nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_layer_make_block_settings(&noData, MINIPSD_PSD, &set, edited.data(), edited.size(), nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_layer_make_block_settings(&keptRun, 3, &set, edited.data(), edited.size(), nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_layer_make_block_settings(&keptRun, MINIPSD_PSD, &set, nullptr, 1, nullptr), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_blocks_settings(nullptr, MINIPSD_PSD, &back), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_blocks_settings(&keptRun, 3, &back), MINIPSD_ERR_ARG); + EXPECT_EQ(minipsd_blocks_settings(&noData, MINIPSD_PSD, &back), MINIPSD_ERR_ARG); } TEST(MinipsdReaderTest, MalformedRecordsAreRefused) @@ -1655,10 +1789,10 @@ TEST(MinipsdReaderTest, EveryModeAndDepthDecodesItsOwnChannels) ASSERT_EQ(minipsd_decode_layer(l.o.r, &l.v, &t, &opts), MINIPSD_OK); const std::vector got = floatsOf(out); for (std::size_t i = 0; i < red.size(); ++i) { - EXPECT_EQ(got[i * 4 + 0], red[i]) << i; - EXPECT_EQ(got[i * 4 + 1], green[i]) << i; - EXPECT_EQ(got[i * 4 + 2], red[i]) << i; - EXPECT_EQ(got[i * 4 + 3], 1.0f) << "no transparency: opaque"; + EXPECT_FLOAT_EQ(got[i * 4 + 0], red[i]) << i; + EXPECT_FLOAT_EQ(got[i * 4 + 1], green[i]) << i; + EXPECT_FLOAT_EQ(got[i * 4 + 2], red[i]) << i; + EXPECT_FLOAT_EQ(got[i * 4 + 3], 1.0f) << "no transparency: opaque"; } } // A 32-bit RGB composite: the matte undone, c = 1 - (1 - s) / a. @@ -1678,7 +1812,9 @@ TEST(MinipsdReaderTest, EveryModeAndDepthDecodesItsOwnChannels) minipsd_target_interleaved(&t, out.data(), 16, 4, 32); const minipsd_decode_opts opts{0, 0, 0, 32}; ASSERT_EQ(minipsd_decode_composite(o.r, &t, &opts), MINIPSD_OK); - EXPECT_EQ(floatsOf(out), (std::vector{0.5f, 1.0f, 0.0f, 0.5f})); + const std::vector got = floatsOf(out), want = {0.5f, 1.0f, 0.0f, 0.5f}; + ASSERT_EQ(got.size(), want.size()); + for (std::size_t i = 0; i < want.size(); ++i) EXPECT_FLOAT_EQ(got[i], want[i]) << i; } // 1 bit: rows packed, eight pixels a byte, padded; a byte of 0 or 1 each (1 black), raw and RLE. { @@ -2231,7 +2367,7 @@ TEST(MinipsdReadTest, AFileBecomesADocumentThatStandsAlone) EXPECT_EQ(r->width, 7u); EXPECT_EQ(r->height, 4u); - EXPECT_EQ(r->dpi, 72.5); + EXPECT_DOUBLE_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); @@ -2497,7 +2633,7 @@ TEST(MinipsdReadTest, AFlatFileBecomesOneBackgroundLayer) const Doc d = readDoc(build(s).bytes, &lost); ASSERT_TRUE(d); EXPECT_EQ(lost, 0u); - EXPECT_EQ(d->dpi, 72.0) << "no resolution stored"; + EXPECT_DOUBLE_EQ(d->dpi, 72.0) << "no resolution stored"; ASSERT_EQ(d->layer_count, 1u); EXPECT_STREQ(d->layers[0].name, "Background"); EXPECT_EQ(Bytes(d->layers[0].pixels, d->layers[0].pixels + 16), (Bytes{0x40, 0x40, 0x40, 255, 0x40, 0x40, 0x40, 255, @@ -2564,8 +2700,8 @@ TEST(MinipsdReadTest, EveryModeComesBackAsWritten) 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"; + EXPECT_FLOAT_EQ(composite[0], 0.25f) << "opaque: as given"; + EXPECT_FLOAT_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); diff --git a/tests/test_utf.cpp b/tests/test_utf.cpp index b95507a..56b039c 100644 --- a/tests/test_utf.cpp +++ b/tests/test_utf.cpp @@ -80,6 +80,17 @@ TEST(MinipsdUtfTest, Utf16BeDecodesPairsAndReplacesUnpairedSurrogates) EXPECT_EQ(minipsd_utf16be_to_utf8(&text, got.data(), needed, nullptr), MINIPSD_OK); EXPECT_EQ(got, utf8 + '\0'); + // And back: the converse gives the units, measured first, a pair for the turtle. + const std::uint8_t units[] = {0x00, 0x41, 0x20, 0xAC, 0xD8, 0x3D, 0xDC, 0x22}; + std::uint8_t made[sizeof units]; + EXPECT_EQ(minipsd_utf8_to_utf16be("A\xE2\x82\xAC\xF0\x9F\x90\xA2", nullptr, 0, &needed), MINIPSD_MORE); + ASSERT_EQ(needed, sizeof units); + EXPECT_EQ(minipsd_utf8_to_utf16be("A\xE2\x82\xAC\xF0\x9F\x90\xA2", made, sizeof made, nullptr), MINIPSD_OK); + EXPECT_EQ(Bytes(made, made + sizeof made), Bytes(units, units + sizeof units)); + EXPECT_EQ(minipsd_utf8_to_utf16be(nullptr, made, sizeof made, &needed), MINIPSD_OK); + EXPECT_EQ(needed, 0u) << "NULL is empty"; + EXPECT_EQ(minipsd_utf8_to_utf16be("A", nullptr, 1, nullptr), MINIPSD_ERR_ARG); + // A U+0000 ends the text; an odd last byte is dropped; empty is empty. const std::uint8_t ended[] = {0x00, 0x41, 0x00, 0x00, 0x00, 0x42}, odd[] = {0x00, 0x41, 0x00}; char small[4]; diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index d9b9ddd..8cee7f8 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -285,7 +285,7 @@ TEST(MinipsdWriterTest, KeptContentIsWrittenBackAsGiven) d.dpi = 72; 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); + EXPECT_DOUBLE_EQ(info.dpi, 72.0); ASSERT_EQ(minipsd_reader_find_resource(r, MINIPSD_RES_RESOLUTION_INFO, &res), MINIPSD_OK); EXPECT_EQ(res.data.data[7], 1) << "shown in inches"; @@ -313,6 +313,46 @@ TEST(MinipsdWriterTest, KeptContentIsWrittenBackAsGiven) ids.clear(); for (at = 0; minipsd_reader_next_resource(r, &at, &res) == MINIPSD_MORE;) ids.push_back(res.id); EXPECT_EQ(ids, (std::vector{1005, 1036})); + + // The pixel aspect ratio: 0, none of the writer's own and a kept 1064 as it is; set, a kept one + // that says the same stays where it is, one that says another gives way to the writer's own, + // first among the kept; minipsd_read gives the file's back, 0 without one. + const Bytes square = {0, 0, 0, 1, 0x3F, 0xF0, 0, 0, 0, 0, 0, 0}; // version 1, 1.0 + const minipsd_resource withAspect[2] = {{1036, view(thumb), view(nm)}, {1064, view(square), {}}}; + d.resources = withAspect; d.resource_count = 2; + const auto aspectOf = [&](double ratio, std::vector *order, Bytes *stored) { + d.pixel_aspect_ratio = ratio; + const Bytes f = write(d); + EXPECT_EQ(minipsd_reader_init(&r, f.data(), f.size(), workspace.data(), workspace.size(), nullptr, &info), MINIPSD_OK); + order->clear(); + for (at = 0; minipsd_reader_next_resource(r, &at, &res) == MINIPSD_MORE;) { + order->push_back(res.id); + if (res.id == 1064) *stored = Bytes(res.data.data, res.data.data + res.data.size); + } + minipsd_doc *back = nullptr; + EXPECT_EQ(minipsd_read(f.data(), f.size(), nullptr, nullptr, &back, nullptr), MINIPSD_OK); + const double kept = back ? back->pixel_aspect_ratio : -1.0; + minipsd_doc_free(back); + return kept; + }; + Bytes stored; + EXPECT_DOUBLE_EQ(aspectOf(0.0, &ids, &stored), 1.0) << "read back: the kept 1064 says 1.0"; + EXPECT_EQ(ids, (std::vector{1005, 1036, 1064})); + EXPECT_EQ(stored, square); + EXPECT_DOUBLE_EQ(info.pixel_aspect_ratio, 1.0); + EXPECT_DOUBLE_EQ(aspectOf(1.0, &ids, &stored), 1.0); + EXPECT_EQ(ids, (std::vector{1005, 1036, 1064})) << "the same: kept, in its place"; + EXPECT_EQ(stored, square) << "its version too"; + EXPECT_DOUBLE_EQ(aspectOf(0.9, &ids, &stored), 0.9); + EXPECT_EQ(ids, (std::vector{1005, 1064, 1036})) << "another: the writer's own, first"; + EXPECT_EQ(stored, (Bytes{0, 0, 0, 2, 0x3F, 0xEC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCD})) << "version 2, 0.9"; + EXPECT_DOUBLE_EQ(info.pixel_aspect_ratio, 0.9); + d.resource_count = 1; // the thumbnail alone + EXPECT_DOUBLE_EQ(aspectOf(0.0, &ids, &stored), 0.0) << "no 1064: 0 in the document"; + EXPECT_DOUBLE_EQ(info.pixel_aspect_ratio, 1.0) << "and square to the reader"; + EXPECT_EQ(ids, (std::vector{1005, 1036})); + std::uint8_t none[1]; + EXPECT_EQ(minipsd_resource_make_pixel_aspect_ratio(0.9, none, sizeof none, nullptr), MINIPSD_ERR_USE_DOC_FIELD); } TEST(MinipsdWriterTest, GroupsAndMasksAreWritten)