Repository navigation
Rgb24 JPEG decode/encode in LibJpegWrap and JpegCodec (for v1.5.0) - #2
Conversation
Native (LibJpegWrap.cpp, non-WASM): DecodeJpegToRgb24 (JCS_RGB, full resolution, libjpeg-turbo default upsampling) and EncodeRgb24ToJpeg (jpeg_set_defaults + jpeg_set_quality baseline, 4:2:0 default or 4:4:4, row stride honoured); errors recover through setjmp/longjmp. linux-x64 LibJpegWrap.so rebuilt in ubuntu:22.04 (GLIBC <= 2.14 symbols, NEEDED libjpeg.so.8). linux-arm64 and win-x64 binaries are unchanged (no cross toolchain); the managed Rgb24 calls throw NotSupportedException there. Managed: IJpegCodec/JpegCodec gain DecodeRgb24 (buffer + pooled overloads) and EncodeRgb24(frame, quality, [output], JpegSubsampling); new JpegSubsampling enum. Tests: decode of a cjpeg reference equals djpeg's Rgb24 bytes exactly; round trip PSNR (4:2:0 q90 > 30 dB, 4:4:4 q92 > 40 dB); 4:4:4 beats 4:2:0 on a chroma checker; padded stride, buffer-too-small, corrupt input, format/quality validation; Gray8/I420 decode of the reference unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wH5aL3TZhJNvLqa8YTku8
rafalmaciag
left a comment
There was a problem hiding this comment.
REQUEST CHANGES: two small, cheap fixes on the public API surface; the rest is solid.
Verified at 35689a8: Release build 0 warnings; tests 121 passed / 7 failed (the 7 are the HttpFrameGrabberTests/MjpegProviderTests live-stream socket tests, pre-existing); all 14 Rgb24 tests pass. linux-x64 .so: ldd -> libjpeg.so.8, max GLIBC_2.14, GCC 11.4 (22.04); nm -D shows DecodeJpegToRgb24/EncodeRgb24ToJpeg added, no old export removed (CreateBgraDecoder/DecoderDecodeBGRA/CloseBgraDecoder now also exported, additive). WASM/arm64/win-x64 untouched. P/Invoke signatures match the exports. Native longjmp recovery works: probed with ctypes, a bad SOF precision, truncated header and truncated scan all return cleanly from DecodeJpegToRgb24.
Blocking
- Pooled
DecodeRgb24(ReadOnlyMemory<byte>)(JpegCodec.cs:334-339) gets dimensions viaGetJpegImageInfo, whose nativeGetJpegInfo(LibJpegWrap.cpp:804) still usesjpeg_std_error->exit(). Reproduced: the reference JPEG with SOF precision set to 5 makesGetJpegImageInfoprint "Unsupported JPEG data precision 5" and kill the process (exit 1); the same bytes intoDecodeJpegToRgb24return 0 cleanly. So the pooled overload does not meet "corrupt JPEG -> clean failure" (the test only covers the buffer overload). Fix: giveGetJpegInfothe newjump_error_mgr(also fixes the pre-existing DecodeI420/Decode pooled paths), or get the dimensions fromDecodeJpegToRgb24(it fillsinfobefore the size check). Add a pooled corrupt-header test. EncodeRgb24(ValidateRgb24Encode, JpegCodec.cs ~399) never checksframe.Data.Length >= Stride*(Height-1) + Width*3; native then reads past the pinned buffer (EncodeRgb24, LibJpegWrap.cpp:~788). Public package -> a wrong header becomes an out-of-bounds read. Add the check (in long arithmetic) and a test.
Nits (follow-up OK)
- LibJpegWrap.cpp: whole file converted CRLF -> LF (diff shows 1681 lines; 125 real). Restore CRLF or add a .gitattributes in a separate commit.
- EncodeRgb24 error path:
jpeg_mem_destonly updates*outbufferin term_destination; after empty_mem_output_buffer has grown the buffer,memis stale andfree(mem)would double-free. Unreachable today (all managed-reachable errors fire in jpeg_start_compress), but writing straight into the caller buffer with the existingjpeg_memory_dest(plus a longjmp-safe empty_output_buffer) removes the malloc+memcpy and the hazard. - Truncated scan data decodes "successfully" (warnings silenced); consider failing when
jerr.pub.num_warnings > 0, or document it. - Pooled decode
header.Width * header.Height * 3and Stride*Height are int math; use checked/long. - README: list the four new IJpegCodec members in the API block (README.md ~146-170) and state the breaking change for implementers (it is only in the PR body). Add
<exception cref="NotSupportedException">(non-linux-x64) andInvalidOperationExceptionto the doc comments. Gray8AndI420Decode_..._AreUnchangedcompares the two decodes with each other, not with a master-built baseline; fine as a smoke test, not a regression pin.
…b24 checks data length
- GetJpegInfo (behind GetJpegImageInfo, used by every pooled decode) now uses the
longjmp error handler: a malformed header (e.g. SOF precision 12) returns 0 and the
managed side throws InvalidOperationException instead of libjpeg calling exit().
Test: pooled DecodeRgb24 of a JPEG with SOF precision 12 throws; with the previous
.so the same test aborts the test host ("Unsupported JPEG data precision 12").
- EncodeRgb24 throws ArgumentException before the P/Invoke when frame data is shorter
than Stride * (Height - 1) + Width * 3 (both overloads; test).
- Gray8/I420 test compares against outputs stored from master add0195.
- Doc comments name NotSupportedException / InvalidOperationException / ArgumentException;
README states the four new IJpegCodec members are breaking for custom implementations.
- linux-x64 LibJpegWrap.so rebuilt the same way (ubuntu:22.04); exports, NEEDED and
GLIBC floor (2.14) unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wH5aL3TZhJNvLqa8YTku8
|
Review fixes are in 4c4e8d1:
Follow-ups are in #3. |
rafalmaciag
left a comment
There was a problem hiding this comment.
APPROVE: re-checked at 4c4e8d1. (1) GetJpegInfo (LibJpegWrap.cpp:804-819) now uses jump_error_mgr under the same #ifndef EMSCRIPTEN guard; my ctypes probe (reference JPEG with SOF precision 5) that exited the process before now returns 0 and the process lives; the new pooled DecodeRgb24 bad-precision test covers it. (2) ValidateRgb24Encode (JpegCodec.cs:399-409) checks Data.Length >= Stride*(Height-1)+Width*3 in long arithmetic, used by both overloads before any P/Invoke; the test exercises both. Release build 0 warnings; tests 123 passed / 7 failed (the 7 pre-existing HttpFrameGrabber/MjpegProvider live-stream socket tests). .so: exports identical to the previous head, NEEDED libjpeg.so.8, max GLIBC_2.14. New TestData blobs contain no LF/CR bytes, so no EOL-conversion risk. Remaining nits (CRLF->LF churn in LibJpegWrap.cpp, the jpeg_mem_dest stale-pointer free on the encode error path, silenced warnings on truncated scans, int size math) belong to #3.
Why
The simulator is dropping SixLabors.ImageSharp (Split License, owner ruling 2026-10-26) for this package. Until now
JpegCodeccould only decode to Gray8/I420 and encode I420/Gray8, so it had no Rgb24 path.What
Native (
LibJpegWrap.cpp; not compiled for Emscripten, so the WASM build stays BGRA-only)DecodeJpegToRgb24:JCS_RGB, full resolution, libjpeg-turbo's default (fancy) chroma upsampling.EncodeRgb24ToJpeg(rgb, w, h, stride, quality, subsampling, out, outSize):jpeg_set_defaults+jpeg_set_quality(q, TRUE)(baseline, integer DCT).subsamplingis 0 = 4:2:0 (libjpeg's default) or 1 = 4:4:4.setjmp/longjmp(libjpeg's documented pattern;error_exitmust not return), and the warning output is silenced. Encode usesjpeg_mem_destand copies into the caller's buffer, so it returns 0 if that buffer is too small.Managed
IJpegCodec/JpegCodec:FrameHeader DecodeRgb24(jpeg, output),FrameImage DecodeRgb24(jpeg)(pooled),int EncodeRgb24(in frame, quality, output, JpegSubsampling = Ratio420),FrameImage EncodeRgb24(in frame, quality, JpegSubsampling = Ratio420)(pooled; uses the worst-casetjBufSizebound).JpegSubsampling { Ratio420, Ratio444 }.Quality/DctMethodand are safe to call concurrently.IJpegCodecthemselves: four new members. In this repo only the test mock implements it, and the mock is updated.libjpeg-turbo8runtime dependency. I also fixed the Usage example, which calledEncodeon an Rgb24 frame; that call throws.Native binaries
ubuntu:22.04againstlibjpeg-turbo8-dev2.1.2, then stripped. Highest GLIBC symbol is 2.14. NEEDED:libjpeg.so.8,libstdc++.so.6,libgcc_s.so.1,libc.so.6.lddinside the simulator runtime image (rocketwelder/simulator:10f8d00, Ubuntu 24.04) resolveslibjpeg.so.8tolibjpeg-turbo8, which is already installed there.EntryPointNotFoundExceptioninto a clearNotSupportedException. A follow-up rebuild of the arm64.so(on a Jetson or with a cross toolchain) would remove that limit.Tests (
JpegCodecRgb24Tests, 14 new)TestData/was made with libjpeg-turbo 2.1.2's own tools:cjpeg -quality 90of a 48x32 synthetic image, anddjpeg -ppmof that JPEG.DecodeRgb24matches the djpeg bytes exactly, for both overloads.HttpFrameGrabberTests/MjpegProviderTestslive-stream socket tests, and they fail the same way on master (107 passed / 7 failed). Release build: 0 warnings.Release
The csproj
<Version>is untouched becausepublish-nuget.ymltakes the version from the tag. Tagv1.5.0after merge.🤖 Generated with Claude Code
https://claude.ai/code/session_014wH5aL3TZhJNvLqa8YTku8