From a5e6c18e35ab2c57d4d747a6a45c28d9e59b0685 Mon Sep 17 00:00:00 2001 From: Ygor Duarte Lemos Pereira Date: Wed, 28 Oct 2026 09:25:56 -0300 Subject: [PATCH 1/2] fix: harden PHF container, add CRC-32, byte-level spec, tests and CI Bug fixes - Header flags: the MED bit was never set ((2 << 1) instead of (1 << 1)); RGB files now carry 0x0E and RGBA 0x0F. - phf_decode_memory: `phf_size < 20 + payload_size` could wrap on 32-bit targets (WASM) and let the decoder read past the buffer; rewritten as a subtraction. phf_read_file validates the untrusted header before sizing its allocation, and the PHF header must agree with the NHF3 packet. - Add an optional CRC-32 trailer (flag bit 3) verified before decoding. Corrupted files previously decoded "successfully" ~15% of the time in fuzzing; with the CRC none do. Files without the flag still decode, and older decoders ignore the trailer. - Remove OpenMP data races on shared error flags. - Web viewer: check _malloc results, validate dimensions/channels, map error codes to messages, bump cache-busting version to 1.0.2. - CLI: load libphf on Windows/macOS/Linux (or PHF_LIB), support grayscale, reject 16-bit input instead of corrupting it, `info` now decodes and verifies the CRC, proper exit codes. API - Named PHF_FLAG_*, PHF_ERR_* and limit constants in phf.h; error codes no longer overlap with the internal NHF3 codes. Docs - SPEC.md rewritten byte by byte (NHF3 packet, plane order, prediction borders, code tables, RLE, CRC, limits); validated by an independent decoder written only from the spec. - README/MANIFESTO: drop claims the code does not implement (Package-Merge, AVX2, ESP32), recompute streaming latencies from the measured throughputs, note benchmark conditions, document build/test/bench. - Fix stale comments (fast-table width, table format, ignored `delta`). Tooling - CMakeLists.txt (shared/static, optional OpenMP, sanitizers), tests/test_phf.c (470 checks), bench/bench.py (PHF vs PNG vs WebP-LL, bit-exact checks), GitHub Actions CI for Linux/macOS/Windows. - build.bat locates MSVC via vswhere; WASM builds write to apps/web/; add build_wasm.sh. Stop tracking generated binaries in libphf/. - sample.phf re-encoded with correct flags and CRC (pixels identical). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 35 ++++++ .gitignore | 12 ++ CMakeLists.txt | 57 ++++++++++ MANIFESTO.md | 10 +- README.md | 103 +++++++++++++---- SPEC.md | 238 ++++++++++++++++++++++++++++++--------- apps/cli/phf_tool.py | 218 ++++++++++++++++++++++++----------- apps/web/phf-view.js | 33 +++++- apps/web/sample.phf | Bin 769416 -> 769420 bytes apps/web/viewer.html | 4 +- bench/bench.py | 152 +++++++++++++++++++++++++ libphf/build.bat | 20 +++- libphf/build_wasm.bat | 16 ++- libphf/build_wasm.sh | 14 +++ libphf/include/phf.h | 42 ++++++- libphf/phf.dll | Bin 55296 -> 0 bytes libphf/phf.js | 2 - libphf/phf.wasm | Bin 81616 -> 0 bytes libphf/src/huff.h | 6 +- libphf/src/nhf3_core.h | 17 +-- libphf/src/phf.c | 212 ++++++++++++++++++++-------------- tests/test_phf.c | 213 +++++++++++++++++++++++++++++++++++ 22 files changed, 1139 insertions(+), 265 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 bench/bench.py create mode 100755 libphf/build_wasm.sh delete mode 100644 libphf/phf.dll delete mode 100644 libphf/phf.js delete mode 100644 libphf/phf.wasm create mode 100644 tests/test_phf.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..050aac1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + name: ${{ matrix.os }} ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: release (OpenMP) + flags: -DCMAKE_BUILD_TYPE=Release + - os: ubuntu-latest + name: sanitizers + flags: -DCMAKE_BUILD_TYPE=Debug -DPHF_SANITIZE=ON -DPHF_OPENMP=OFF + - os: macos-latest + name: release + flags: -DCMAKE_BUILD_TYPE=Release + - os: windows-latest + name: release (MSVC) + flags: "" + steps: + - uses: actions/checkout@v4 + - name: Configure + run: cmake -S . -B build ${{ matrix.flags }} + - name: Build + run: cmake --build build --config Release + - name: Test + run: ctest --test-dir build -C Release --output-on-failure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ffb36e --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Build output (use CMake or the scripts in libphf/) +build/ +build-*/ +libphf/*.dll +libphf/*.lib +libphf/*.exp +libphf/*.obj +libphf/phf.js +libphf/phf.wasm +__pycache__/ +*.pyc +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..597174b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.16) +project(phf VERSION 1.0.0 LANGUAGES C) + +option(PHF_BUILD_SHARED "Build libphf as a shared library (needed by the Python CLI)" ON) +option(PHF_OPENMP "Encode/decode color planes in parallel with OpenMP when available" ON) +option(PHF_SANITIZE "Build with AddressSanitizer + UndefinedBehaviorSanitizer" OFF) +option(PHF_BUILD_TESTS "Build the test suite" ON) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release) +endif() +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +if(PHF_BUILD_SHARED) + set(PHF_LIB_TYPE SHARED) +else() + set(PHF_LIB_TYPE STATIC) +endif() + +add_library(phf ${PHF_LIB_TYPE} libphf/src/phf.c libphf/src/huff.c) +target_include_directories(phf PUBLIC libphf/include) +set_target_properties(phf PROPERTIES C_VISIBILITY_PRESET hidden) +if(WIN32 AND PHF_BUILD_SHARED) + target_compile_definitions(phf PRIVATE PHF_BUILD_DLL) +endif() + +if(MSVC) + target_compile_options(phf PRIVATE /W3) +else() + target_compile_options(phf PRIVATE -Wall -Wextra) +endif() + +if(PHF_OPENMP) + find_package(OpenMP COMPONENTS C) + if(OpenMP_C_FOUND) + target_link_libraries(phf PRIVATE OpenMP::OpenMP_C) + message(STATUS "libphf: OpenMP enabled") + else() + message(STATUS "libphf: OpenMP not found, building single-threaded") + endif() +endif() + +if(PHF_SANITIZE AND NOT MSVC) + set(PHF_SAN_FLAGS -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer) + target_compile_options(phf PUBLIC ${PHF_SAN_FLAGS}) + target_link_options(phf PUBLIC ${PHF_SAN_FLAGS}) +endif() + +if(PHF_BUILD_TESTS) + enable_testing() + add_executable(test_phf tests/test_phf.c) + target_link_libraries(test_phf PRIVATE phf) + add_test(NAME test_phf + COMMAND test_phf ${CMAKE_CURRENT_SOURCE_DIR}/apps/web/sample.phf + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +endif() diff --git a/MANIFESTO.md b/MANIFESTO.md index 2cce9f8..637bb46 100644 --- a/MANIFESTO.md +++ b/MANIFESTO.md @@ -30,9 +30,9 @@ However, embedded directly into the project's history and manifesto is an intent 2. **Modern Systems Engineering (2026):** Through a pair-programming collaboration between **Ygor Pereira** and **Antigravity (Google DeepMind)**, the architecture was re-engineered for modern parallel processors: - - Length-limited canonical Huffman trees constrained via *Package-Merge* (maximum 15 bits). - - Meta-compression of code-length tables via RLE combined with a secondary code-length Huffman tree. - - Vectorized geometric spatial prediction (MED — *Median Edge Detector*) implemented with **SIMD intrinsics (SSE2, AVX2, ARM NEON, and WASM SIMD128)**. + - Length-limited canonical Huffman codes (maximum 15 bits, using the JPEG Annex K.3 length adjustment). + - Compact sparse code-length tables: Elias-gamma symbol gaps plus 4-bit lengths, so a table costs bits only for the symbols it actually uses. + - Vectorized geometric spatial prediction (MED — *Median Edge Detector*) implemented with **SIMD intrinsics (SSE2, ARM NEON, and WASM SIMD128)**. - Exact per-context cost optimization: dynamic selection between custom tables, shared global fallback tables, and deterministic 0-bit immediate states. --- @@ -43,10 +43,10 @@ However, embedded directly into the project's history and manifesto is an intent Zero pixels are discarded, quantized, or approximated. When a camera captures raw sensor data in 24-bit or 32-bit color, PHF preserves every photon upon reconstruction. $\text{PSNR} = \infty$, $\text{MSE} = 0$, maximum pixel error $= 0$. ### II. High-Throughput Production (> 300 MB/s) -The primary obstacle of modern lossless formats (such as WebP Lossless) has been compression latency. PHF was architected to be up to **37× faster than WebP Lossless** and **20× faster than PNG**, processing hundreds of megabytes per second on standard consumer hardware. +The primary obstacle of modern lossless formats (such as WebP Lossless) has been compression latency. In the author's benchmark PHF encoded **37× faster than WebP Lossless** and **20× faster than PNG-6**, processing hundreds of megabytes per second on standard consumer hardware. ### III. Instant & Universal Decoding -PHF is lightweight enough to decode in real time across embedded microcontrollers (ESP32), single-board computers (Raspberry Pi 4), and modern web browsers via WebAssembly SIMD128 at over **400 MB/s**. +PHF's decoder is plain C99 with optional SIMD, so it runs on desktop and server CPUs, single-board computers such as the Raspberry Pi, and modern web browsers via WebAssembly SIMD128. It needs about 600 KB of code tables plus full-frame buffers, which rules out small microcontrollers without external RAM. ### IV. Free & Open to Humanity (MIT License) We dedicate this algorithm and format to the global community under the permissive MIT License. Free of software patents, free of licensing royalties, free of restrictive consortium fees. It belongs to developers, photographers, researchers, and open-source creators worldwide. diff --git a/README.md b/README.md index 374e6b2..a72f1d9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **PHF** (**P**ure **H**igh-Fidelity **F**ormat, with roots in the **P**ereira-**H**uffman **F**ormat) is a next-generation, high-performance, strictly lossless image codec. -By combining order-1 context-adaptive Huffman coding (N-Huffman) with SIMD-vectorized Median Edge Detection (MED) and Package-Merge canonical length-limiting, PHF delivers **higher compression ratios than WebP Lossless** while operating at **300 to 400 MB/s** throughput. +By combining order-1 context-adaptive Huffman coding (N-Huffman) with SIMD-vectorized Median Edge Detection (MED) and length-limited (15-bit) canonical codes, PHF delivered **higher compression ratios than WebP Lossless** at **~300–400 MB/s** in the benchmark below. --- @@ -15,6 +15,11 @@ By combining order-1 context-adaptive Huffman coding (N-Huffman) with SIMD-vecto Tested across 7 ultra-high-resolution real-world photographs (from 4.4 MP up to 36.2 MP Full-Frame DSLRs), totaling **370.4 MB of raw 24-bit RGB pixel data**: +> **About these numbers:** they were measured by the author; the CPU, the PHF thread count +> (libphf can use OpenMP for its color planes) and the codec versions were not recorded. +> Re-run the comparison on your own hardware and images with +> [`bench/bench.py`](#testing--benchmarking), which checks every decode bit-exact. + | Codec | Total Size | Compression Ratio | Space Savings | Total Encode Time | Encode Throughput | Total Decode Time | Decode Throughput | Lossless Verified | | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | 🥇 **PHF** | **95.2 MB** | **3.89×** | **74.3%** | **1.28 s** | **290 MB/s** | **0.92 s** | **403 MB/s** | **100% Bit-Exact (0 errors)** | @@ -39,20 +44,21 @@ Because PHF inherits the architecture of the **NHF3 video streaming engine**, it | Streaming Metric | PNG-6 | WebP-LL | 🥇 PHF | | :--- | :---: | :---: | :---: | -| **Encode Latency (1080p 24-bit)** | ~160 ms *(cannot do 30 fps)* | ~300 ms *(cannot do 15 fps)* | **~8 ms *(solid 60+ fps)*** | +| **Encode Latency (1080p 24-bit, 6.2 MB)** | ~415 ms *(~2 fps)* | ~780 ms *(~1 fps)* | **~21 ms *(~45 fps)*** | | **Encode Throughput** | 15 MB/s | 8 MB/s | **290 MB/s** | | **Decode Throughput** | 223 MB/s | 144 MB/s | **> 400 MB/s** | | **Instant Header Probe** | Variable chunks (IHDR/IDAT) | RIFF header parse | **Fixed 20-byte probe (0 allocations)** | -| **Pipelined Planar Streams** | ❌ Monolithic Deflate stream | ❌ Monolithic VP8L stream | **✅ Independent channel streams** | -| **Inter-Frame Delta (P-frames)** | ❌ Unsupported | ❌ Unsupported | **✅ Native in core engine** | +| **Planar Streams** | ❌ Monolithic Deflate stream | ❌ Monolithic VP8L stream | **✅ Length-prefixed, independently decodable planes** | +| **Inter-Frame Delta (P-frames)** | ❌ Unsupported | ❌ Unsupported | **⚠️ In the NHF3 core engine only (not in `.phf` v1)** | +| **Integrity Check** | ✅ CRC-32 per chunk | ⚠️ None for the image data | **✅ CRC-32 of the payload** | ### Streaming Architectural Strengths: -1. **Sub-Frame Encode Latency:** PHF encodes a 1080p frame in under 8 ms and a 720p frame in ~3 ms. This allows continuous lossless frame streaming at 60 fps with zero frame drops on everyday consumer CPUs. +1. **Low Encode Latency:** at the 290 MB/s measured above, a 1080p frame (6.2 MB) encodes in about 21 ms and a 720p frame (2.8 MB) in about 10 ms: enough for lossless 30 fps at 1080p or 60 fps at 720p. Latencies in the table are frame size ÷ measured throughput. 2. **20-Byte Instant Probe:** Over TCP sockets, HTTP chunked streams, or WebRTC data channels, the receiver only needs the first 20 bytes to immediately ascertain resolution, channel count, and payload size—pre-allocating GPU/Canvas buffers before the payload finishes downloading. -3. **Independent Planar Pipelining:** Color planes ($G$, $R-G$, $B-G$, and $\alpha$) are serialized in independent sequential partitions. A parallel receiver can start entropy decoding and inverse spatial reconstruction as soon as each plane arrives. +3. **Independent Planes:** the color planes ($R-G$, $G$, $B-G$, and $\alpha$) are stored as separate length-prefixed streams, so each can be entropy-decoded on its own (libphf decodes them in parallel when built with OpenMP). The format allows starting on a plane as soon as it arrives; the current `phf_decode_memory()` API still takes the complete file. 4. **Target Applications:** - **High-Fidelity Remote Desktop / VNC:** Crisp, uncorrupted text and UI streaming without JPEG/H.264 chroma blur or ringing artifacts. - - **Medical & Scientific Imaging:** Live transmission of endoscopy, surgical, and radiology feeds where mathematical losslessness is legally required. + - **Medical & Scientific Imaging:** Live transmission of endoscopy, surgical, and radiology feeds where mathematical losslessness is required; the payload CRC-32 detects corrupted files instead of silently decoding them. - **High-Speed Industrial Inspection:** Machine vision camera feeds operating at 60 to 120 FPS. --- @@ -62,19 +68,26 @@ Because PHF inherits the architecture of the **NHF3 video streaming engine**, it ```text phf/ ├── MANIFESTO.md # Origins, vision, and tribute to David Huffman -├── SPEC.md # Binary format specification v1.0 +├── SPEC.md # Byte-level format specification v1.0 ├── LICENSE # Permissive MIT License +├── CMakeLists.txt # Cross-platform build (library + tests) │ ├── libphf/ # Core C Library │ ├── include/ -│ │ └── phf.h # Clean public C API +│ │ └── phf.h # Public C API, flags and error codes │ ├── src/ │ │ ├── phf.c # Encoder, decoder, and container logic -│ │ ├── huff.c / .h # Canonical 15-bit Package-Merge Huffman tree -│ │ ├── bitio.h # Branchless bitstream reader and writer -│ │ └── phf_simd.h # SIMD intrinsics (SSE2, AVX2, ARM NEON, WASM) -│ ├── build.bat # Native Windows MSVC build (phf.dll / phf.lib) -│ └── build_wasm.bat # WebAssembly build (phf.js / phf.wasm) +│ │ ├── huff.c / .h # Canonical length-limited (15-bit) Huffman codes +│ │ ├── bitio.h # Bitstream reader and writer +│ │ ├── nhf3_core.h # Internal NHF3 engine API +│ │ └── phf_simd.h # SIMD prediction (SSE2, ARM NEON, WASM SIMD128, scalar) +│ ├── build.bat # Windows MSVC shortcut (phf.dll) +│ └── build_wasm.bat/.sh# WebAssembly build -> apps/web/phf.js + phf.wasm +│ +├── tests/ +│ └── test_phf.c # Round-trip, container, CRC and corruption tests +├── bench/ +│ └── bench.py # Reproducible PHF vs PNG vs WebP Lossless benchmark │ └── apps/ ├── cli/ @@ -88,35 +101,69 @@ phf/ ## Building libphf -### Windows (MSVC): +### Any platform (CMake 3.16+): +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release +``` +Produces `libphf.so` / `libphf.dylib` / `phf.dll` in `build/` (`build/Release/` with Visual Studio). +OpenMP is used automatically when the compiler provides it (`-DPHF_OPENMP=OFF` to disable). +Other options: `-DPHF_BUILD_SHARED=OFF` (static library), `-DPHF_SANITIZE=ON` (ASan + UBSan). + +### Windows shortcut (MSVC): ```cmd cd libphf build.bat ``` -Produces `phf.dll` and `phf.lib`. +Produces `libphf/phf.dll` and `phf.lib`. Finds Visual Studio through `vswhere` if `cl.exe` is not already in `PATH`. ### WebAssembly (Emscripten): -```cmd -cd libphf -build_wasm.bat +```bash +source /path/to/emsdk/emsdk_env.sh # or emsdk_env.bat on Windows +./libphf/build_wasm.sh # or libphf\build_wasm.bat +``` +Writes `apps/web/phf.js` and `apps/web/phf.wasm` (SIMD128 enabled). After rebuilding, bump +`BUILD_VERSION` in `phf-view.js` and the `?v=` strings in `viewer.html` to bust browser caches. + +--- + +## Testing & Benchmarking + +```bash +ctest --test-dir build -C Release --output-on-failure +``` +Runs round-trips over many sizes and image types (RGB/RGBA, stride 3/4), header and error +checks, CRC detection, a corruption fuzz, file I/O, and decoding of `apps/web/sample.phf`. +CI runs the suite on Linux (with OpenMP and with sanitizers), macOS and Windows. + +```bash +pip install opencv-python numpy +python bench/bench.py photo1.png photo2.png ... +OMP_NUM_THREADS=1 python bench/bench.py photo1.png ... # single-threaded PHF ``` -Produces `phf.js` and `phf.wasm` (SIMD128 enabled). +Prints a Markdown table (size, ratio, encode/decode time and MB/s, bit-exact check) for +PHF, WebP Lossless, PNG-6 and PNG-1, plus the machine and thread settings used. --- ## Quickstart (CLI) +Requires the shared library (see above; or point `PHF_LIB` at it) and `pip install opencv-python numpy`. + ```bash -# Encode an image to PHF -python apps/cli/phf_tool.py encode photo.jpg photo.phf +# Encode an image to PHF (8-bit RGB, RGBA or grayscale; grayscale is stored as RGB) +python apps/cli/phf_tool.py encode photo.png photo.phf # Decode a PHF image back to PNG python apps/cli/phf_tool.py decode photo.phf photo.png -# Display image metadata and compression statistics +# Show metadata and verify the file decodes and its CRC matches python apps/cli/phf_tool.py info photo.phf ``` +JPEG inputs work too: PHF stores the decoded JPEG pixels losslessly (it cannot undo the JPEG's own loss). +16-bit images are rejected rather than truncated. + --- ## C API Example @@ -124,10 +171,11 @@ python apps/cli/phf_tool.py info photo.phf ```c #include "phf.h" -// Encode RGB image to a .phf file +// Encode RGB image to a .phf file (max 65535 px per side, 2^26 px total) int err = phf_write_file("output.phf", rgb_buffer, width, height, 3); +if (err != PHF_OK) { /* PHF_ERR_* code */ } -// Read and decode a .phf file from disk +// Read and decode a .phf file from disk (CRC-32 is verified when present) phf_info_t info; uint8_t *decoded_rgb = phf_read_file("output.phf", &info); @@ -135,6 +183,10 @@ if (decoded_rgb) { // Process pixels: info.width, info.height, info.channels phf_free(decoded_rgb); } + +// Or decode from memory straight into an RGBA buffer (alpha = 255 for RGB files) +int rc = phf_decode_memory(file_bytes, file_size, rgba, info.width, info.height, 4); +if (rc == PHF_ERR_CHECKSUM) { /* file is corrupted */ } ``` --- @@ -143,6 +195,7 @@ if (decoded_rgb) { - **Ygor D. L. Pereira** — Original N-Huffman concept and academic research (FA7, 2008). - **Antigravity (Google DeepMind)** — Codec systems engineering, SIMD vectorization, and WebAssembly architecture. +- **Claude (Anthropic)** — Code review, container hardening and CRC-32 integrity, byte-level specification, test suite, CMake/CI, and benchmark tooling. - **David A. Huffman** (1925–1999) — In enduring tribute to the father of minimum redundancy coding. --- diff --git a/SPEC.md b/SPEC.md index c51198d..d686d93 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2,76 +2,204 @@ The official file extension is **`.phf`**. The recommended MIME type is **`image/phf`** or **`image/x-phf`**. ---- +This document describes every byte a decoder needs. The reference implementation is +`libphf/src/phf.c` and `libphf/src/huff.c`; where this text and the code disagree, that is +a bug to be reported. + +Conventions: `u8`/`u16`/`u32` are unsigned integers; **LE** = little-endian, **BE** = +big-endian. Bitstreams are read **MSB-first** (the first bit is bit 7 of the first byte). +All pixel arithmetic is modulo 256. -## 1. Binary Container Layout +--- -Every `.phf` file begins with a fixed **20-byte header**, followed immediately by the length-limited canonical N-Huffman compressed entropy payload: +## 1. File Layout ``` -+-------------------------------------------------------------------+ -| PHF FILE HEADER (20 BYTES) | -+-------------------+---------+----------+-------+----------+-------+ -| Magic 'PHF1' (4B) | Ver(1B) | Chan(1B) | Fl(1B)| Res(1B) | W(4B) | -+-------------------+---------+----------+-------+----------+-------+ -| Height (4B) | Payload Size (4B) | -+-------------------+-----------------------------------------------+ -| COMPRESSED N-HUFFMAN PAYLOAD | -| (Canonical-15 Tables + Order-1 Bitstream + SIMD MED Residuals) | -+-------------------------------------------------------------------+ ++----------------------+-------------------------------+-----------------------+ +| PHF header (20 B) | payload: NHF3 packet | CRC-32 (4 B, optional)| +| | (payload_size bytes) | present if flag bit 3 | ++----------------------+-------------------------------+-----------------------+ ``` -### Header Field Specification: +### 1.1 PHF Header (20 bytes) -| Byte Offset | Size | Type | Field Name | Description | +| Offset | Size | Type | Field | Description | | :---: | :---: | :---: | :--- | :--- | -| `0x00` | 4 bytes | ASCII | `magic` | Magic identifier: `0x50 0x48 0x46 0x31` (`"PHF1"`). | -| `0x04` | 1 byte | uint8 | `version` | Format container version. Current version: `1`. | -| `0x05` | 1 byte | uint8 | `channels` | Channel count (`3` for RGB, `4` for RGBA). | -| `0x06` | 1 byte | uint8 | `flags` | Bitmask configuration:
• Bit 0: Has Alpha (`1` = RGBA, `0` = RGB)
• Bit 1: Spatial MED predictor active (`1` = Yes)
• Bit 2: Color decorrelation active (`1` = R-G, G, B-G) | -| `0x07` | 1 byte | uint8 | `reserved` | Reserved for future alignment / color profiles (must be `0x00`). | -| `0x08` | 4 bytes | uint32 | `width` | Image width in pixels (Little-Endian). | -| `0x0C` | 4 bytes | uint32 | `height` | Image height in pixels (Little-Endian). | -| `0x10` | 4 bytes | uint32 | `payload_size` | Exact byte count of the compressed entropy stream. | +| `0x00` | 4 | ASCII | `magic` | `"PHF1"` (`0x50 0x48 0x46 0x31`). | +| `0x04` | 1 | u8 | `version` | Container version. Must be `1`. | +| `0x05` | 1 | u8 | `channels` | `3` (RGB) or `4` (RGBA). | +| `0x06` | 1 | u8 | `flags` | See 1.2. | +| `0x07` | 1 | u8 | `reserved` | Writers must write `0x00`; readers ignore it. | +| `0x08` | 4 | u32 LE | `width` | Image width in pixels. | +| `0x0C` | 4 | u32 LE | `height` | Image height in pixels. | +| `0x10` | 4 | u32 LE | `payload_size` | Byte length of the NHF3 packet that follows. | + +### 1.2 Flags + +| Bit | Mask | Name | Meaning | +| :---: | :---: | :--- | :--- | +| 0 | `0x01` | `ALPHA` | Set when `channels == 4`. | +| 1 | `0x02` | `MED` | MED spatial prediction is used. | +| 2 | `0x04` | `DECORR` | R−G / B−G color decorrelation is used. | +| 3 | `0x08` | `CRC32` | A 4-byte CRC-32 follows the payload (see 1.4). | +| 4–7 | | | Reserved, must be `0`. | + +A v1 writer produces `0x0E` for RGB and `0x0F` for RGBA. Bits 0–2 are descriptive: the +decoder takes the actual coding modes from the NHF3 packet header (section 2), and they +must agree with it. Files written before bit 3 existed carry no CRC; files from early +encoder builds also carry `0x04`/`0x05` (the MED bit was not set) and are still valid. + +### 1.3 Size Limits + +The NHF3 packet stores 16-bit dimensions, so: `1 <= width, height <= 65535` and +`width * height <= 2^26` (67,108,864 pixels). Encoders reject larger images. + +### 1.4 Validation and Integrity + +A decoder must check, in this order: + +1. `magic`, `version`, and `channels ∈ {3, 4}`. +2. `20 + payload_size <= file_size` (plus 4 when `CRC32` is set). Compute this without + integer overflow, e.g. `payload_size <= file_size - 20`. Bytes after the payload + (and CRC) are ignored. +3. If `CRC32` is set: the u32 LE after the payload must equal the CRC-32 of the + `payload_size` payload bytes. CRC-32 here is the IEEE 802.3 / zlib CRC (reflected, + polynomial `0xEDB88320`, initial value and final XOR `0xFFFFFFFF`). +4. The NHF3 packet's width, height and channel count must equal the PHF header's, + and its P-frame bit must be `0`. --- -## 2. Entropy & Residual Pipeline - -1. **Header Validation:** Confirm `magic == "PHF1"`, `version == 1`, and file size equals $20 + \text{payload\_size}$. -2. **Entropy Decoding:** The compressed payload is read via a 15-bit canonical Huffman bitstream reader across 256 order-1 context state tables per channel plane. -3. **Inverse Spatial Prediction (MED):** - For each reconstructed residual pixel at coordinate $(x, y)$, spatial prediction $\hat{X}$ is calculated from causal neighbors: - $$\hat{X} = \text{clamp}(A + B - C, \min(A, B), \max(A, B))$$ - where $A = P(x-1, y)$, $B = P(x, y-1)$, and $C = P(x-1, y-1)$. - The original pixel value is recovered via: - $$X = (\hat{X} + \text{residual}) \pmod{256}$$ -4. **Color Re-correlation:** - - $\text{Green} = G'$ - - $\text{Red} = (R' + G) \pmod{256}$ - - $\text{Blue} = (B' + G) \pmod{256}$ +## 2. NHF3 Packet (the payload) + +### 2.1 Packet Header (`11 + 4·ch` bytes) + +| Offset | Size | Type | Field | Description | +| :---: | :---: | :---: | :--- | :--- | +| 0 | 4 | ASCII | `magic` | `"NHF3"`. | +| 4 | 1 | u8 | `version` | `1`. | +| 5 | 1 | u8 | `mode` | bit 0: P-frame (must be 0 in PHF); bits 1–2: predictor (`0` none, `1` left, `2` MED, `3` palette); bit 3: color decorrelation. PHF v1 writers use `0x0C` (MED + decorrelation). | +| 6 | 2 | u16 BE | `width` | | +| 8 | 2 | u16 BE | `height` | | +| 10 | 1 | u8 | `ch` | Channel count (3 or 4 in PHF). | +| 11 | 4·ch | u32 BE[ch] | `plane_len` | Byte length of each plane stream. | + +The plane streams follow, concatenated in plane order. The sum of `plane_len` plus the +header must equal `payload_size` exactly. Each plane can be decoded independently once its +bytes are available. + +PHF v1 writers only produce predictor `2` with decorrelation. The reference decoder also +accepts predictors `0`/`1` from the NHF3 engine; the palette mode (`3`) is not part of this +specification, and independent decoders may reject it. + +### 2.2 Planes and Color Transform + +With decorrelation, the planes are, in order: + +| Plane | Content | +| :---: | :--- | +| 0 | `R − G` | +| 1 | `G` | +| 2 | `B − G` | +| 3 | `A` (RGBA only, not transformed) | + +Without decorrelation, the planes are R, G, B (and A) unchanged. Reconstruction: +`G = plane1`, `R = plane0 + G`, `B = plane2 + G` (mod 256). + +### 2.3 Spatial Prediction + +Each plane `P` (size `w × h`, row-major) is replaced by a residual plane `E`. Let +`X = P(x, y)`, `a = P(x−1, y)` (left), `b = P(x, y−1)` (up), `c = P(x−1, y−1)` (up-left): + +| Position | Prediction `X̂` | +| :--- | :--- | +| `(0, 0)` | `0` | +| `y = 0, x > 0` | `a` | +| `x = 0, y > 0` | `b` | +| otherwise, predictor `2` (MED) | `min(a,b)` if `c >= max(a,b)`; `max(a,b)` if `c <= min(a,b)`; else `a + b − c` | +| otherwise, predictor `1` (left) | `a` | + +`E(x, y) = (X − X̂) mod 256` and the decoder recovers `X = (X̂ + E(x, y)) mod 256`, in +raster order. Predictor `0` leaves the plane unchanged. --- -## 3. Streaming & Network Transmission +## 3. Plane Stream (order-1 N-Huffman) -The PHF container is intentionally designed for zero-copy, pipelined network transmission (HTTP chunked streams, WebSockets, WebRTC DataChannels, and UNIX domain sockets). +A plane stream encodes the `n = w·h` residual bytes `E[0..n−1]` (raster order). The +**context** of each symbol is the previous symbol in the stream. The stream ends with zero +bits up to the next byte boundary. -### 3.1 Instant 20-Byte Header Probe -Unlike chunk-based containers (e.g. PNG chunk trees or TIFF tag directories) that require multiple reads across variable offsets: -- A stream receiver reads exactly the initial **20 bytes** via `phf_probe_memory()` to obtain: - - Width ($W$) and Height ($H$) - - Channel count ($C$) - - Required destination pixel memory: $W \times H \times C$ bytes - - Total incoming payload size: $20 + \text{payload\_size}$ -- Buffer pre-allocation occurs instantly with zero memory reallocations while the network payload continues streaming in. +### 3.1 Layout -### 3.2 Planar Stream Independence -The compressed payload contains independent sequential partitions for each color plane: -- Planar layout: Plane 0 ($G$), Plane 1 ($R - G$), Plane 2 ($B - G$), and optional Plane 3 ($\alpha$). -- A multi-threaded streaming consumer can decompress incoming planes concurrently as their respective byte streams arrive over the network, minimizing total frame display latency. +``` +first : 8 bits first symbol E[0] +rle : 1 bit 1 = run-length mode (3.4) +if rle: run class codes for t = 0, then t = 1: + single : 1 bit + if single: cls : 5 bits (every run uses class cls, 0 code bits) + else: code table (3.2), only symbols 0..31 allowed +has_fb : 1 bit 1 = a shared "fallback" code table follows +if has_fb: code table (3.2) over symbols 0..255 +contexts : 256 descriptors, for c = 0..255: + used : 1 bit + if used: kind : 2 bits + 0 = SINGLE : sym : 8 bits (the next symbol is always sym, 0 bits) + 1 = OWN : code table (3.2) for this context + 2 = FALLBACK : use the fallback table (requires has_fb) + 3 = invalid +symbols : 3.3 (plain) or 3.4 (rle) +``` + +A context `c` that is not `used` must never occur during decoding. + +### 3.2 Code Table + +A table gives a code length (1–15) for each symbol that has a code: + +``` +k − 1 : 8 bits k = number of coded symbols (decoders require k >= 2) +k entries, in increasing symbol order: + gap : Elias-gamma symbol − previous symbol (previous starts at −1) + len : 4 bits 1..15 +``` + +Elias-gamma for `v >= 1`: `m = floor(log2 v)` zero bits, then `v` in `m + 1` bits (the +leading bit is the 1). Readers reject `m > 8` and symbols above 255. + +The lengths must form a **complete** prefix code (Kraft sum `Σ 2^(−len) = 1`). Codes are +**canonical** as in DEFLATE: shorter codes first, ties by increasing symbol value, each +code being the previous code + 1 (shifted left when the length grows). Codes are read +MSB-first. + +### 3.3 Plain Mode (`rle = 0`) + +`E[0] = first`. For `i = 1..n−1`, with `ctx = E[i−1]`: if `ctx` is SINGLE, `E[i]` is its +`sym`; otherwise decode one symbol with the context's table (OWN or fallback). + +### 3.4 Run-Length Mode (`rle = 1`) + +The plane is a sequence of `(symbol, run)` pairs whose runs sum to exactly `n`. Within the +sequence, consecutive symbols always differ. + +1. The first pair's symbol is `first`. +2. Every later symbol is decoded as in 3.3, using the previous **pair's** symbol as the + context. +3. After each symbol `s`, its run is read with run table `t = (s != 0) ? 1 : 0`: a class + `k` (0–31, either the fixed `cls` or decoded with the table), then `k` raw bits `x`; + `run = 2^k + x`. A run that would pass `n` is an error. + +--- -### 3.3 Inter-Frame Delta Protocol (Video Extension) -When deployed in real-time display streaming (e.g., Remote Desktop, medical telemetry, or camera monitoring), the underlying engine supports temporal prediction against the prior frame buffer ($P$-frames): -- Zero-motion tiles require 0 entropy bits. -- Enables steady 60 fps lossless video transmission over standard Gigabit Ethernet networks without compression artifacts. +## 4. Streaming Notes + +- **Header probe:** the first 20 bytes give width, height, channel count, the required + output size (`width · height · stride`) and the total file size, so a receiver can + allocate buffers before the rest arrives (`phf_probe_memory()`). +- **Independent planes:** after `20 + 11 + 4·ch` bytes the receiver knows where each plane + starts and ends, and each plane decodes on its own. The reference API + (`phf_decode_memory()`) currently needs the complete file; incremental decoding is a + possible future API, not something v1 provides. +- **Inter-frame (P-frame) coding** exists in the underlying NHF3 engine (`nhf3_encode` with + a previous frame), but it is **not** part of the PHF v1 container: PHF files are always + stand-alone images. diff --git a/apps/cli/phf_tool.py b/apps/cli/phf_tool.py index 6344bac..4b962f1 100644 --- a/apps/cli/phf_tool.py +++ b/apps/cli/phf_tool.py @@ -11,18 +11,60 @@ import cv2 import numpy as np -# Load phf.dll from libphf HERE = os.path.dirname(os.path.abspath(__file__)) -DLL_PATH = os.path.abspath(os.path.join(HERE, "..", "..", "libphf", "phf.dll")) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) -if not os.path.exists(DLL_PATH): - # Try next to script or in cwd - DLL_PATH = os.path.abspath("phf.dll") +if sys.platform == "win32": + LIB_NAMES = ["phf.dll"] +elif sys.platform == "darwin": + LIB_NAMES = ["libphf.dylib"] +else: + LIB_NAMES = ["libphf.so"] -if not os.path.exists(DLL_PATH): - raise FileNotFoundError(f"Could not locate phf.dll. Run build.bat in libphf first. Looked in: {DLL_PATH}") +PHF_FLAG_ALPHA = 0x01 +PHF_FLAG_MED = 0x02 +PHF_FLAG_DECORR = 0x04 +PHF_FLAG_CRC32 = 0x08 -libphf = ctypes.CDLL(DLL_PATH) +PHF_ERRORS = { + -1: "invalid argument", + -2: "not a PHF file (bad magic)", + -3: "unsupported PHF version", + -4: "dimension mismatch", + -5: "file is truncated", + -6: "stride smaller than channel count", + -7: "corrupt payload", + -8: "checksum mismatch (file is corrupted)", + -9: "out of memory", + -10: "I/O error", +} + + +def find_library(): + """PHF_LIB env var, then CMake build dirs, then libphf/ (build.bat), then cwd.""" + env = os.environ.get("PHF_LIB") + if env: + return [env] + dirs = [ + os.path.join(ROOT, "build"), + os.path.join(ROOT, "build", "Release"), + os.path.join(ROOT, "libphf"), + os.getcwd(), + ] + return [os.path.join(d, n) for d in dirs for n in LIB_NAMES] + + +def load_library(): + candidates = find_library() + for path in candidates: + if os.path.exists(path): + return ctypes.CDLL(path) + sys.exit("Error: could not locate the libphf shared library. Build it with\n" + " cmake -S . -B build && cmake --build build --config Release\n" + "or set PHF_LIB to its path. Looked in:\n " + "\n ".join(candidates)) + + +libphf = load_library() # Define ctypes prototypes class PHFInfo(ctypes.Structure): @@ -46,65 +88,85 @@ class PHFInfo(ctypes.Structure): libphf.phf_free.argtypes = [ctypes.c_void_p] libphf.phf_free.restype = None + +def error_text(rc): + return f"{PHF_ERRORS.get(rc, 'unknown error')} (code {rc})" + + +def load_rgb(input_path): + """Returns (pixels, channels) as a contiguous 8-bit RGB/RGBA array, or (None, reason).""" + img = cv2.imread(input_path, cv2.IMREAD_UNCHANGED) + if img is None: + return None, "could not read image" + if img.dtype != np.uint8: + # PHF stores 8 bits per channel; truncating would silently lose data. + return None, f"{img.dtype} samples are not supported (PHF is 8 bits per channel)" + if img.ndim == 2: + # Grayscale is stored as RGB (R = G = B); decoding yields a 3-channel image. + return np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)), 3 + if img.shape[2] == 4: + return np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)), 4 + if img.shape[2] == 3: + return np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), 3 + return None, f"unsupported channel count {img.shape[2]}" + + def encode_image(input_path, output_path=None): if not os.path.exists(input_path): print(f"Error: File not found: {input_path}") return False - + if output_path is None: base, _ = os.path.splitext(input_path) output_path = base + ".phf" - img_bgr = cv2.imread(input_path, cv2.IMREAD_UNCHANGED) - if img_bgr is None: - print(f"Error: Could not read image {input_path}") + rgb, channels = load_rgb(input_path) + if rgb is None: + print(f"Error: {input_path}: {channels}") return False - - has_alpha = (img_bgr.ndim == 3 and img_bgr.shape[2] == 4) - channels = 4 if has_alpha else 3 - h, w = img_bgr.shape[:2] - - if channels == 4: - rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGRA2RGBA) - else: - rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) - - rgb = np.ascontiguousarray(rgb) + h, w = rgb.shape[:2] raw_bytes = w * h * channels - + print(f"Encoding: {input_path}") print(f" Dimensions: {w}x{h} ({channels} channels) | Raw: {raw_bytes / (1024*1024):.2f} MB") - + out_sz = ctypes.c_size_t(0) t0 = time.perf_counter() ptr = libphf.phf_encode_memory(rgb.ctypes.data, w, h, channels, ctypes.byref(out_sz)) enc_time = time.perf_counter() - t0 - + if not ptr: - print("Error: PHF encoding failed.") + print("Error: PHF encoding failed (max 65535 px per side and 67,108,864 px total).") return False - + phf_bytes = ctypes.string_at(ptr, out_sz.value) libphf.phf_free(ptr) - + with open(output_path, "wb") as f: f.write(phf_bytes) - + comp_bytes = len(phf_bytes) ratio = raw_bytes / comp_bytes savings = (1.0 - comp_bytes / raw_bytes) * 100.0 throughput = (raw_bytes / (1024*1024)) / enc_time - + print(f" -> Saved: {output_path}") print(f" -> Size: {comp_bytes / (1024*1024):.2f} MB (Ratio: {ratio:.2f}x, Space Savings: {savings:.1f}%)") print(f" -> Speed: {enc_time*1000:.1f} ms ({throughput:.1f} MB/s)") return True + +def probe(phf_bytes): + info = PHFInfo() + rc = libphf.phf_probe_memory(phf_bytes, len(phf_bytes), ctypes.byref(info)) + return rc, info + + def decode_image(input_path, output_path=None): if not os.path.exists(input_path): print(f"Error: File not found: {input_path}") return False - + if output_path is None: base, _ = os.path.splitext(input_path) output_path = base + ".png" @@ -112,89 +174,115 @@ def decode_image(input_path, output_path=None): with open(input_path, "rb") as f: phf_bytes = f.read() - info = PHFInfo() - rc = libphf.phf_probe_memory(phf_bytes, len(phf_bytes), ctypes.byref(info)) + rc, info = probe(phf_bytes) if rc != 0: - print(f"Error: Invalid or corrupt PHF file (error code {rc})") + print(f"Error: Invalid PHF file: {error_text(rc)}") return False w, h, ch = info.width, info.height, info.channels + if ch not in (3, 4): + print(f"Error: Invalid PHF file: unsupported channel count {ch}") + return False dst = np.zeros((h, w, ch), dtype=np.uint8) - + print(f"Decoding: {input_path}") print(f" Dimensions: {w}x{h} ({ch} channels) | PHF Size: {len(phf_bytes)/(1024*1024):.2f} MB") - + t0 = time.perf_counter() dec_rc = libphf.phf_decode_memory(phf_bytes, len(phf_bytes), dst.ctypes.data, w, h, ch) dec_time = time.perf_counter() - t0 - + if dec_rc != 0: - print(f"Error: Decode failed with code {dec_rc}") + print(f"Error: Decode failed: {error_text(dec_rc)}") return False - + raw_bytes = w * h * ch throughput = (raw_bytes / (1024*1024)) / dec_time - + if ch == 4: bgr = cv2.cvtColor(dst, cv2.COLOR_RGBA2BGRA) else: bgr = cv2.cvtColor(dst, cv2.COLOR_RGB2BGR) - - cv2.imwrite(output_path, bgr) + + if not cv2.imwrite(output_path, bgr): + print(f"Error: Could not write {output_path}") + return False print(f" -> Saved: {output_path}") print(f" -> Speed: {dec_time*1000:.1f} ms ({throughput:.1f} MB/s)") return True + def info_image(input_path): if not os.path.exists(input_path): print(f"Error: File not found: {input_path}") - return - + return False + with open(input_path, "rb") as f: phf_bytes = f.read() - info = PHFInfo() - rc = libphf.phf_probe_memory(phf_bytes, len(phf_bytes), ctypes.byref(info)) + rc, info = probe(phf_bytes) if rc != 0: - print(f"Error: Not a valid PHF file (error {rc})") - return - + print(f"Error: Not a valid PHF file: {error_text(rc)}") + return False + raw_sz = info.width * info.height * info.channels - ratio = raw_sz / len(phf_bytes) - savings = (1.0 - len(phf_bytes) / raw_sz) * 100.0 - + ratio = raw_sz / len(phf_bytes) if raw_sz else 0.0 + savings = (1.0 - len(phf_bytes) / raw_sz) * 100.0 if raw_sz else 0.0 + + flag_names = [name for bit, name in ((PHF_FLAG_ALPHA, "alpha"), (PHF_FLAG_MED, "MED"), + (PHF_FLAG_DECORR, "decorrelation"), (PHF_FLAG_CRC32, "CRC-32")) + if info.flags & bit] + + # Full decode to report whether the file is actually intact. + integrity = "unsupported channel count" + ok = False + if info.channels in (3, 4): + dst = np.empty((info.height, info.width, info.channels), dtype=np.uint8) + dec_rc = libphf.phf_decode_memory(phf_bytes, len(phf_bytes), dst.ctypes.data, + info.width, info.height, info.channels) + ok = dec_rc == 0 + if ok: + integrity = "decodes OK, CRC-32 verified" if info.flags & PHF_FLAG_CRC32 \ + else "decodes OK (no checksum in file: corruption cannot be ruled out)" + else: + integrity = f"FAILED: {error_text(dec_rc)}" + print("=" * 50) print("PHF File Information:") print("=" * 50) print(f"File: {input_path}") print(f"Resolution: {info.width} x {info.height} pixels") print(f"Channels: {info.channels} ({'RGBA' if info.channels == 4 else 'RGB'})") + print(f"Flags: 0x{info.flags:02x} ({', '.join(flag_names) or 'none'})") print(f"Raw Size: {raw_sz / (1024*1024):.2f} MB ({raw_sz} bytes)") print(f"PHF Size: {len(phf_bytes) / (1024*1024):.2f} MB ({len(phf_bytes)} bytes)") print(f"Compression: {ratio:.2f}x ({savings:.1f}% space savings)") - print(f"Lossless: 100% Bit-Exact (Strict)") + print(f"Integrity: {integrity}") print("=" * 50) + return ok + def main(): - if len(sys.argv) < 2: + if len(sys.argv) < 3: print("PHF Utility v1.0 — Pereira-Huffman Format") print("Usage:") print(" python phf_tool.py encode [output.phf]") print(" python phf_tool.py decode [output.png]") print(" python phf_tool.py info ") - return + return 2 cmd = sys.argv[1].lower() - if cmd == "encode" and len(sys.argv) >= 3: - out = sys.argv[3] if len(sys.argv) >= 4 else None - encode_image(sys.argv[2], out) - elif cmd == "decode" and len(sys.argv) >= 3: - out = sys.argv[3] if len(sys.argv) >= 4 else None - decode_image(sys.argv[2], out) - elif cmd == "info" and len(sys.argv) >= 3: - info_image(sys.argv[2]) + out = sys.argv[3] if len(sys.argv) >= 4 else None + if cmd == "encode": + ok = encode_image(sys.argv[2], out) + elif cmd == "decode": + ok = decode_image(sys.argv[2], out) + elif cmd == "info": + ok = info_image(sys.argv[2]) else: print(f"Unknown command: {cmd}") + return 2 + return 0 if ok else 1 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/apps/web/phf-view.js b/apps/web/phf-view.js index 9dfd358..dccbbdd 100644 --- a/apps/web/phf-view.js +++ b/apps/web/phf-view.js @@ -4,7 +4,19 @@ * License: MIT */ -const BUILD_VERSION = "1.0.1"; +const BUILD_VERSION = "1.0.2"; + +const PHF_ERRORS = { + [-1]: "invalid argument", + [-2]: "not a PHF file", + [-3]: "unsupported PHF version", + [-4]: "dimension mismatch", + [-5]: "file is truncated", + [-6]: "stride smaller than channel count", + [-7]: "corrupt payload", + [-8]: "checksum mismatch (file is corrupted)", + [-9]: "out of memory", +}; let phfModuleInstance = null; let phfModulePromise = null; @@ -56,11 +68,22 @@ export async function decodePHF(arrayBuffer) { const height = dv.getUint32(12, true); const payloadSize = dv.getUint32(16, true); - const srcPtr = mod._malloc(n); - mod.HEAPU8.set(u8, srcPtr); + if (version !== 1) throw new Error(`Unsupported PHF version ${version}`); + if (channels !== 3 && channels !== 4) throw new Error(`Unsupported channel count ${channels}`); + // Same limits as the encoder (PHF_MAX_DIM / PHF_MAX_PIXELS); also keeps + // outBytes far below 2^32 so it cannot wrap when passed to _malloc. + if (width === 0 || height === 0 || width > 65535 || height > 65535 || width * height > 67108864) { + throw new Error(`Invalid PHF dimensions ${width}x${height}`); + } const outBytes = width * height * 4; // Always decode to 4-channel RGBA for Canvas - const dstPtr = mod._malloc(outBytes); + const srcPtr = mod._malloc(n); + const dstPtr = srcPtr ? mod._malloc(outBytes) : 0; + if (!srcPtr || !dstPtr) { + if (srcPtr) mod._free(srcPtr); + throw new Error("PHF decoding failed: out of memory"); + } + mod.HEAPU8.set(u8, srcPtr); const t0 = performance.now(); // Call into WASM decoder with stride = 4 (Canvas RGBA) @@ -70,7 +93,7 @@ export async function decodePHF(arrayBuffer) { if (rc !== 0) { mod._free(srcPtr); mod._free(dstPtr); - throw new Error(`PHF decoding failed with error code: ${rc}`); + throw new Error(`PHF decoding failed: ${PHF_ERRORS[rc] || "unknown error"} (code ${rc})`); } const decodedU8 = new Uint8Array(mod.HEAPU8.buffer, dstPtr, outBytes); diff --git a/apps/web/sample.phf b/apps/web/sample.phf index 7b3c693720a621b448fc4a0af47e0f6fe2405588..c7983e91e84de11a5c42f900c6270d22d0e111ce 100644 GIT binary patch delta 56 zcmeD9t=IEgk3GP{&5)6quaUi#ow1djsg<3%m7S%Powb#nt(6@p$kEEq*~-qfm7TkB KFN?v4S|$LkBo1x> delta 51 zcmeDAt=I8ek3GP{&5)6qrIEdrow1djsg<3%m7S%Powb#nt(6@p$kEEq*~-qfm7TkB FF92s948Z^Z diff --git a/apps/web/viewer.html b/apps/web/viewer.html index c6edc4f..46c4522 100644 --- a/apps/web/viewer.html +++ b/apps/web/viewer.html @@ -200,7 +200,7 @@

File St