Skip to content

Commit 4516aa9

Browse files
authored
Consolidated image format support (#148)
1 parent bf156f4 commit 4516aa9

131 files changed

Lines changed: 66065 additions & 724 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/coverage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ jobs:
129129
file: ${{ runner.workspace }}/build/coverage/coverage_final.info
130130
name: overall-coverage
131131
fail_ci_if_error: false
132-
verbose: false
132+
verbose: true
133133
- name: Upload Test Results
134134
uses: actions/upload-artifact@v7
135135
if: always()

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ __pycache__/
5151
.pytest_cache/
5252
.cache/
5353
docs/superpowers/
54+
reasonix.toml

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
3030
- New `GpuTarget` class (`rhi/yup_GpuTarget.h`): low-level render-pass-only offscreen GPU surface (`create`, `beginRenderPass`, `asTexture`, `asImage`, `readPixels`). Its backing texture is allocated from the context's main render context, so it does not reserve a dedicated `rive::gpu::RenderContext` — use it for custom `GpuPipeline` work (e.g. post-process passes) that needs no 2D drawing.
3131
- New `GpuCanvas` class (`rhi/yup_GpuCanvas.h`): consolidated backend-agnostic offscreen GPU surface that now composes a `GpuTarget` (over a `RenderableTarget`) and creates a non-owning `Graphics` lazily only when 2D drawing is requested.
3232

33+
#### Image Formats
34+
35+
- Added TIFF read/write support (`TiffImageFormat`) via libtiff: RGB, RGBA, Grayscale at 8/16-bit; multi-page reading; DPI and EXIF/ICC/XMP metadata extraction.
36+
- Added TGA read/write support (`TgaImageFormat`): uncompressed and RLE-compressed truecolor and grayscale variants; RGB and RGBA output with alpha channel preservation.
37+
- Added animated WebP encoding and decoding support to `WebPImageFormatWriter` and `WebPImageFormatReader`: per-frame metadata (canvas dimensions, frame count, loop count, per-frame delays, dispose/blend modes) and frame decompression with manual compositing.
38+
- Added animated PNG (APNG) encoding and decoding support to `PngImageFormatWriter` and `PngImageFormatReader`: manual chunk-level parsing of `acTL`/`fcTL`/`fdAT` chunks for animation metadata, per-frame libpng decoding via synthetic minimal PNG construction, and canvas compositing supporting all three APNG disposal operations (none, background, previous) and both blend operations (source, over).
39+
- `ImageFormat::Options` struct controls metadata extraction: `.withMetadata(true)` enables text metadata and DPI; `.withRawChunks(true)` enables raw binary chunks (EXIF, ICC, XMP). When both are false (the default), `ImageMetadata` is not allocated — true zero overhead.
40+
- Introduced a ref-counted `ImageMetadata` object (`ImageMetadata::Ptr`) attached to `Image` and `ImageFormatReader::metadata`. DPI, text entries, and raw binary chunks are all accessed through the metadata object only when requested via `Options`.
41+
- Lossless roundtrip tests for all formats (BMP, PNG, WebP, TGA, TIFF, PPM, GIF) now verify pixel-perfect fidelity after write→read; animated roundtrip tests for GIF, WebP, and PNG verify per-frame pixel integrity.
42+
43+
3344
### Shading
3445

3546
- New GLSL→WGSL direct transpiler in `yup_shading`: parses preprocessed GLSL 4.50, lowers GLSL constructs to WGSL equivalents, and emits WGSL 1.0 source. Supports vertex/fragment/compute stages with full builtin mapping, combined sampler splitting, entry-point IO wrapping, and binding assignment matching glslang's SPIR-V assignment 1:1. Does not require SPIR-V or spirv_cross for code generation. Integrated into `ShaderTranspiler`, `ShaderCache`, and `ShaderBundleCompiler`. WGSL variants are supported in YSLB bundles via the `shader_bundler` tool and `yup_add_shader_bundle()` CMake helper.

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,17 @@ YUP is usable for experimentation, examples, prototypes, and contributors who ar
171171
| **iOS** (enc) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
172172
| **iOS** (dec) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
173173

174+
## Supported Image Formats
175+
| | **Read** | **Write** | **Read Animated** | **Write Animated** | **Metadata R/W** | **Dependent on module** |
176+
|-------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|:-----------------------:|
177+
| **BMP** | :white_check_mark: | :white_check_mark: | | | | |
178+
| **PPM** | :white_check_mark: | :white_check_mark: | | | | |
179+
| **TGA** | :white_check_mark: | :white_check_mark: | | | :white_check_mark: | |
180+
| **TIFF** | :white_check_mark: | :white_check_mark: | | | :white_check_mark: | libtiff |
181+
| **JPG** | :white_check_mark: | :white_check_mark: | | | :white_check_mark: | libjpeg |
182+
| **PNG** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | libpng |
183+
| **GIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | libgif |
184+
| **WEBP** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | libwebp |
174185

175186
## Prerequisites
176187
Before building, make sure you have:

cmake/yup_standalone.cmake

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function (yup_standalone_app)
2626
# Globals
2727
TARGET_NAME TARGET_VERSION TARGET_CONSOLE TARGET_IDE_GROUP TARGET_APP_NAMESPACE TARGET_ICON TARGET_WHEEL TARGET_CXX_STANDARD
2828
# Emscripten
29-
INITIAL_MEMORY MAXIMUM_MEMORY PTHREAD_POOL_SIZE STACK_SIZE CUSTOM_PLIST CUSTOM_SHELL ENABLE_EMSCRIPTEN_WEBGPU ENABLE_EMSCRIPTEN_GL_DEBUGGING)
29+
INITIAL_MEMORY MAXIMUM_MEMORY PTHREAD_POOL_SIZE STACK_SIZE CUSTOM_PLIST CUSTOM_SHELL ENABLE_EMSCRIPTEN_WEBGPU ENABLE_EMSCRIPTEN_GL_DEBUGGING ENABLE_EMSCRIPTEN_NODERAWFS)
3030
set (multi_value_args
3131
# Globals
3232
DEFINITIONS COMPILE_OPTIONS MODULES SOURCES LINK_OPTIONS
@@ -39,6 +39,7 @@ function (yup_standalone_app)
3939
_yup_set_default (YUP_ARG_TARGET_ICON "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/resources/app-icon.png")
4040
_yup_set_default (YUP_ARG_ENABLE_EMSCRIPTEN_WEBGPU OFF)
4141
_yup_set_default (YUP_ARG_ENABLE_EMSCRIPTEN_GL_DEBUGGING OFF)
42+
_yup_set_default (YUP_ARG_ENABLE_EMSCRIPTEN_NODERAWFS OFF)
4243

4344
set (target_name "${YUP_ARG_TARGET_NAME}")
4445
set (target_version "${YUP_ARG_TARGET_VERSION}")
@@ -221,7 +222,7 @@ function (yup_standalone_app)
221222
$<$<BOOL:${YUP_ARG_MAXIMUM_MEMORY}>:-sMAXIMUM_MEMORY=${YUP_ARG_MAXIMUM_MEMORY}>
222223
-sPTHREAD_POOL_SIZE=${YUP_ARG_PTHREAD_POOL_SIZE}
223224
-sFORCE_FILESYSTEM=1
224-
-sNODERAWFS=0
225+
-sNODERAWFS=$<IF:$<BOOL:${YUP_ARG_ENABLE_EMSCRIPTEN_NODERAWFS}>,1,0>
225226
-sWASMFS=1
226227
-sFETCH=1
227228
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap

docs/graphics/rhi/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ offscreen render-to-texture pipelines.
4545
shader sources, binding maps, and the pipeline cache.
4646
- [Offline shader compilation](offline-shaders.md) - build `.ysl` shader bundles
4747
ahead of time with `yup_add_shader_bundle` and the `yup_shader_bundler` tool.
48+
- [Shader bundle binary format](shader-bundle-format.md) - the RIFF `YSLB` container
49+
specification (FourCCs, chunk layout, reflection blob, versioning).
4850
- [Buffers & textures](buffers-and-textures.md) - `GpuBuffer` and `GpuTexture`.
4951
- [Offscreen targets & canvases](targets.md) - `GpuTarget`, `GpuCanvas`, and CPU
5052
readback.
@@ -59,6 +61,7 @@ concepts
5961
frames-and-passes
6062
pipelines
6163
offline-shaders
64+
shader-bundle-format
6265
buffers-and-textures
6366
targets
6467
spinning-cube

docs/graphics/rhi/offline-shaders.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ yup_add_shader_bundle(my_shaders
218218
## See also
219219

220220
- [Pipelines & shaders](pipelines.md) - compiling and configuring pipelines.
221+
- [Shader bundle binary format](shader-bundle-format.md) - the RIFF `YSLB` container
222+
specification for `.ysl` files.
221223
- [`yup_add_embedded_binary_resources`](../../build-system/cmake-api.md#yup_add_embedded_binary_resources) -
222224
the embedding primitive `yup_add_shader_bundle` builds on.
223225
- [Walkthrough: the spinning cube](spinning-cube.md) - an end-to-end example.

modules/yup_shading/shading/yup_ShaderBundle.md renamed to docs/graphics/rhi/shader-bundle-format.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ The whole file is a single `RIFF` chunk whose form type is `YSLB`.
7777

7878
```
7979
+===========================================================================+
80-
| RIFF | size | 'YSLB' |
80+
| RIFF | size | 'YSLB' |
8181
+===========================================================================+
8282
| |
8383
| +--------+--------+------------------+ |
@@ -179,13 +179,13 @@ entry point, transpiled source, optional original input source, and reflection.
179179
+--------------------------------------------------------------------------+
180180
```
181181

182-
| Field | Type | Meaning |
183-
|--------------|---------------|--------------------------------------------------------|
184-
| `language` | `int32` | Target `ShaderLanguage` of this variant. |
185-
| `entryPoint` | prefixed str | Entry-point function name (e.g. `"main"`). |
186-
| `source` | prefixed str | Transpiled source code in `language`. |
182+
| Field | Type | Meaning |
183+
|--------------|---------------|------------------------------------------------------------------------------|
184+
| `language` | `int32` | Target `ShaderLanguage` of this variant. |
185+
| `entryPoint` | prefixed str | Entry-point function name (e.g. `"main"`). |
186+
| `source` | prefixed str | Transpiled source code in `language`. |
187187
| `ISRC` | chunk | Original Vulkan GLSL input source, raw UTF-8. Optional — omitted when empty. |
188-
| `REFL` | chunk | Serialised `ShaderReflection` (see below). |
188+
| `REFL` | chunk | Serialised `ShaderReflection` (see below). |
189189

190190
### `ISRC` — Original input source
191191

@@ -237,28 +237,28 @@ independently of the RIFF envelope version.
237237

238238
## 6. FourCC Reference
239239

240-
| FourCC | Constant | Role |
241-
|--------|-------------------|-----------------------------------------------|
242-
| `RIFF` | `kFourCC_RIFF` | Outer RIFF container. |
243-
| `LIST` | `kFourCC_LIST` | Generic list container. |
244-
| `YSLB` | `kFourCC_YSLB` | Bundle form type (magic). |
245-
| `VERS` | `kFourCC_VERS` | Format version chunk. |
246-
| `SHAD` | `kFourCC_SHAD` | List type for the stage list. |
247-
| `SHDR` | `kFourCC_SHDR` | Per-stage chunk. |
248-
| `SPVB` | `kFourCC_SPVB` | SPIR-V binary chunk. |
249-
| `VARS` | `kFourCC_VARS` | List type for the variant list. |
250-
| `VART` | `kFourCC_VART` | Per-variant chunk. |
240+
| FourCC | Constant | Role |
241+
|--------|-------------------|------------------------------------------------------|
242+
| `RIFF` | `kFourCC_RIFF` | Outer RIFF container. |
243+
| `LIST` | `kFourCC_LIST` | Generic list container. |
244+
| `YSLB` | `kFourCC_YSLB` | Bundle form type (magic). |
245+
| `VERS` | `kFourCC_VERS` | Format version chunk. |
246+
| `SHAD` | `kFourCC_SHAD` | List type for the stage list. |
247+
| `SHDR` | `kFourCC_SHDR` | Per-stage chunk. |
248+
| `SPVB` | `kFourCC_SPVB` | SPIR-V binary chunk. |
249+
| `VARS` | `kFourCC_VARS` | List type for the variant list. |
250+
| `VART` | `kFourCC_VART` | Per-variant chunk. |
251251
| `ISRC` | `kFourCC_ISRC` | Original input source chunk (per-variant, optional). |
252-
| `REFL` | `kFourCC_REFL` | Reflection archive chunk. |
252+
| `REFL` | `kFourCC_REFL` | Reflection archive chunk. |
253253

254254
Current format version: **`2`** (`kCurrentVersion`).
255255

256256
### Version history
257257

258-
| Version | Change |
259-
|---------|------------------------------------------------------------------------|
258+
| Version | Change |
259+
|---------|-------------------------------------------------------------------------|
260260
| 1 | Initial format. Top-level `SRCE` chunk held a single per-bundle source. |
261-
| 2 | Removed `SRCE`. Added per-variant `ISRC` sub-chunk inside `VART`. |
261+
| 2 | Removed `SRCE`. Added per-variant `ISRC` sub-chunk inside `VART`. |
262262

263263
---
264264

docs/imaging/loading.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ else
2222
}
2323
```
2424

25+
Pass `ImageFormat::Options` as a second argument to extract metadata:
26+
27+
```cpp
28+
auto opts = ImageFormat::Options()
29+
.withMetadata (true)
30+
.withRawChunks (true);
31+
auto result = Image::loadFromData (encodedBytes, opts);
32+
```
33+
2534
## The ImageFormatManager
2635

2736
`ImageFormatManager` is the registry that maps files and streams to the right
@@ -54,10 +63,12 @@ PPM/PGM/PBM have no dependency and are always available.
5463
| ----------- | ----------------- | -------------- |
5564
| BMP | `bmp` | *(built in)* |
5665
| PPM/PGM/PBM | `ppm` | *(built in)* |
66+
| TGA | `tga` | *(built in)* |
5767
| PNG | `png` | `libpng` |
5868
| JPEG | `jpeg` | `libjpeg` |
5969
| WebP | `webp` | `libwebp` |
6070
| GIF | `gif` | `libgif` |
71+
| TIFF | `tiff` | `libtiff` |
6172

6273
```cmake
6374
yup_standalone_app(
@@ -96,7 +107,7 @@ Ownership of the stream is **always** consumed by the call - on success the
96107
reader owns it; on failure it is deleted:
97108

98109
```cpp
99-
auto stream = someFile.createInputStream(); // std::unique_ptr<InputStream>
110+
std::unique_ptr<InputStream> stream = someFile.createInputStream();
100111
if (auto reader = formats.createReaderFor (stream.release()))
101112
{
102113
Image image = reader->readImage();
@@ -112,20 +123,45 @@ are plain public fields on `ImageFormatReader`:
112123
int w = reader->width;
113124
int h = reader->height;
114125
auto fmt = reader->pixelFormat;
115-
double dx = reader->dpiX; // 0.0 if not present
116-
double dy = reader->dpiY;
126+
```
127+
128+
By default metadata extraction is **disabled** — the `metadata` field is
129+
`nullptr`. To opt in, pass `ImageFormat::Options` when creating the reader:
130+
131+
```cpp
132+
// Request text metadata and DPI
133+
auto reader = formats.createReaderFor (stream.release(),
134+
ImageFormat::Options().withMetadata (true));
117135

118-
// Arbitrary key/value metadata from the file.
119-
// Standard keys: "dpiX", "dpiY", "colorSpace", "title", "software", "comment".
120-
String title = reader->metadataValues.getValue ("title", {});
136+
if (auto metadata = reader->metadata)
137+
{
138+
double dx = metadata->dpiX; // 0.0 if not present
139+
double dy = metadata->dpiY;
140+
141+
// Arbitrary key/value pairs from the file
142+
// Standard keys: "Title", "Software", "Comment", "dpiX", "dpiY"
143+
String title = metadata->textEntries.getValue ("Title", {});
144+
145+
// Raw binary chunks (EXIF, ICC profile, XMP packet)
146+
if (auto* exif = metadata->getRawChunk ("jpeg/exif"))
147+
processExif (exif->getData(), exif->getSize());
148+
}
149+
```
150+
151+
For `Image::loadFromData()`, pass options as the second argument:
152+
153+
```cpp
154+
auto result = Image::loadFromData (bytes,
155+
ImageFormat::Options().withRawChunks (true));
121156
```
122157

123158
`getFormatName()` returns the human-readable format label (e.g. `"PNG Image"`).
124159

125160
## Animated images
126161

127-
Animated formats such as GIF expose multiple frames. Non-animated readers report
128-
a single frame, so the same code works for both:
162+
Formats that support animation — GIF, WebP, and PNG (APNG) — expose multiple
163+
frames through the same reader API. Non-animated readers report a single frame,
164+
so the same code works for both:
129165

130166
```cpp
131167
if (auto reader = formats.createReaderFor (File ("/path/anim.gif")))

docs/imaging/saving.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ embed metadata, and pick a quality level for compressed formats:
2525
```cpp
2626
std::unique_ptr<ImageFormatWriter> createWriterFor (
2727
const File& file,
28-
PixelFormat pixelFormat = PixelFormat::RGBA,
29-
const StringPairArray& metadataValues = {},
28+
PixelFormat pixelFormat = PixelFormat::RGBA,
29+
const StringPairArray& metadataValues = {},
3030
int qualityOptionIndex = 0);
3131
```
3232

@@ -54,8 +54,8 @@ Compressed formats expose named quality levels. Query them from the
5454
`ImageFormat` and pass the chosen index to `createWriterFor`:
5555

5656
```cpp
57-
// getQualityOptions() returns labels like {"Low", "Medium", "High"};
58-
// isCompressed() is false for BMP/PPM and true for PNG/WebP/JPEG.
57+
getQualityOptions() // returns labels like {"Low", "Medium", "High"}
58+
isCompressed() // is false for BMP/PPM and true for PNG/WebP/JPEG
5959
```
6060

6161
`qualityOptionIndex` is ignored by lossless/uncompressed formats.
@@ -85,13 +85,14 @@ if (auto writer = pngFormat.createWriterFor (outStream.release(),
8585

8686
The writer takes ownership of the destination stream.
8787

88-
## Animated GIF output
88+
## Animated output
8989

90-
Only the GIF writer supports animation (`supportsAnimation()` returns true).
91-
Bracket the frames between `beginAnimation()` and `endAnimation()`:
90+
GIF, WebP, and PNG (APNG) writers support animation (`supportsAnimation()`
91+
returns true). Bracket the frames between `beginAnimation()` and
92+
`endAnimation()`:
9293

9394
```cpp
94-
if (auto writer = formats.createWriterFor (File ("/path/anim.gif")))
95+
if (auto writer = formats.createWriterFor (File ("/path/anim.webp")))
9596
{
9697
if (writer->supportsAnimation())
9798
{
@@ -105,10 +106,10 @@ if (auto writer = formats.createWriterFor (File ("/path/anim.gif")))
105106
}
106107
```
107108

108-
Each `writeFrame` quantizes the frame to a 256-color palette and appends a
109-
Graphic Control Extension with the given delay. `loopCount` follows the reader
110-
convention: `0` = infinite, `1` = play once, `N` = play N times. Calling the
111-
animation methods on a non-animation writer asserts and returns `false`.
109+
Each `writeFrame` encodes the frame with the given display duration in
110+
milliseconds. `loopCount` follows the reader convention: `0` = infinite,
111+
`1` = play once, `N` = play N times. Calling the animation methods on a
112+
non-animation writer asserts and returns `false`.
112113

113114
## See also
114115

0 commit comments

Comments
 (0)