[terminal] Good Image Protocol (GIP) implementation#317
[terminal] Good Image Protocol (GIP) implementation#317christianparpart wants to merge 11 commits intomasterfrom
Conversation
a5d308a to
fd370a7
Compare
a0be723 to
dfacb3d
Compare
fd370a7 to
5825bef
Compare
5825bef to
99af6be
Compare
a4450f7 to
e0aff93
Compare
e6b8214 to
06fff93
Compare
af0c49f to
03ab5c9
Compare
1a1c769 to
b93a36e
Compare
03ab5c9 to
00bafa0
Compare
6b61cef to
3078e28
Compare
There was a problem hiding this comment.
Pull request overview
This PR implements the Good Image Protocol (GIP), a DCS-based protocol for displaying raster images in terminal emulators. The implementation follows the formal specification at https://github.com/contour-terminal/terminal-good-image-protocol and provides a reference implementation with comprehensive features.
Changes:
- Implements GIP protocol with four DCS operations: upload (u), render (r), oneshot (s), and release (d)
- Adds three-layer compositing system (Below/Replace/Above) for controlling image-text interaction
- Introduces
MessageParserfor parsing DCS message format with base64 encoding support - Adds
contour catCLI command for displaying images in terminals - Improves libunicode dependency resolution with system/CPM/error fallback
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vtbackend/MessageParser.{h,cpp} | New HTTP-like message parser for DCS sequences with base64 support |
| src/vtbackend/MessageParser_test.cpp | Comprehensive unit tests for message parser |
| src/vtbackend/GoodImageProtocol_test.cpp | Extensive GIP protocol tests covering upload, render, layers, and edge cases |
| src/vtbackend/Screen.{h,cpp} | GIP hook handlers, image upload/render/release logic, layer support |
| src/vtbackend/Image.{h,cpp} | ImageLayer enum, layer support in RasterizedImage, shouldPreserveImageOnTextWrite |
| src/vtbackend/Functions.h | GIP function declarations (GIUPLOAD, GIRENDER, GIDELETE, GIONESHOT) |
| src/vtbackend/VTType.{h,cpp} | DA1 capability advertisement for GIP (code 11) |
| src/vtbackend/Terminal.h | ImageDecoderCallback for PNG decoding |
| src/vtbackend/cell/{SimpleCell,CompactCell}.h | Layer-aware image preservation on text write |
| src/vtrasterizer/ImageRenderer.{h,cpp} | Below-text and above-text rendering queues for layer support |
| src/contour/display/TerminalDisplay.{h,cpp} | PNG decoder implementation using Qt |
| src/contour/display/OpenGLRenderer.cpp | PNG format handling (assertion for unreachable code) |
| src/contour/ContourApp.{h,cpp} | contour cat CLI command with image display via GIP oneshot |
| src/vtbackend/CMakeLists.txt | Added MessageParser sources and tests |
| src/contour/CMakeLists.txt | Removed QML debug compile-time defines |
| cmake/ContourThirdParties.cmake | Improved libunicode dependency handling |
| CMakeLists.txt | Removed extra blank lines |
| metainfo.xml | Release notes for GIP implementation |
| .github/actions/spelling/expect.txt | Added GIP-related terms to spell check allowlist |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix "seperated" typos in MessageParser.h Doxygen comments - Replace old Apache boilerplate with SPDX headers in MessageParser files - Refactor toNumber() to return std::optional<int> with overflow protection - Use value_or() for safe optional dereferences in GIP render/oneshot handlers - Add size() > 1 guard before base64 decode of header values and body - Verify ifs.read() success in readFile() with gcount check - Remove dead decodeImage() method from TerminalDisplay Signed-off-by: Christian Parpart <christian@parpart.family>
- Fix "seperated" typos in MessageParser.h Doxygen comments - Replace old Apache boilerplate with SPDX headers in MessageParser files - Refactor toNumber() to return std::optional<int> with overflow protection - Use value_or() for safe optional dereferences in GIP render/oneshot handlers - Add size() > 1 guard before base64 decode of header values and body - Verify ifs.read() success in readFile() with gcount check - Remove dead decodeImage() method from TerminalDisplay Signed-off-by: Christian Parpart <christian@parpart.family>
1a3f583 to
e78ab55
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix "seperated" typos in MessageParser.h Doxygen comments - Replace old Apache boilerplate with SPDX headers in MessageParser files - Refactor toNumber() to return std::optional<int> with overflow protection - Use value_or() for safe optional dereferences in GIP render/oneshot handlers - Add size() > 1 guard before base64 decode of header values and body - Verify ifs.read() success in readFile() with gcount check - Remove dead decodeImage() method from TerminalDisplay Signed-off-by: Christian Parpart <christian@parpart.family>
e78ab55 to
348929e
Compare
- Screen.cpp: Return nullopt for empty strings in toNumber() (per @Copilot) - Screen.cpp: Remove stale TODO comment on toImageResizePolicy fallback (per @Copilot) - Screen.cpp: Log error on failed PNG decode for upload and oneshot (per @Copilot) - ContourApp.cpp: Check file size against 16 MB GIP limit before sending (per @Copilot) Signed-off-by: Christian Parpart <christian@parpart.family>
The QT_QML_DEBUG, QMLJSDEBUGGER, and QT_DECLARATIVE_DEBUG defines were unconditionally set for Debug builds, causing Qt's QQmlDebuggingEnabler to print "QML debugging is enabled..." during static initialization — before main() runs. This affects all invocations including headless CLI commands like `contour cat`. QML debugging remains available at runtime via -qmljsdebugger=... or the QT_QML_DEBUG environment variable. Signed-off-by: Christian Parpart <christian@parpart.family>
Signed-off-by: Christian Parpart <christian@parpart.family>
Remove the GOOD_IMAGE_PROTOCOL compile-time gate and make GIP always available. Complete the protocol implementation with: - ImageLayer enum (Below/Replace/Above) for z-ordering images relative to text, plumbed through Screen, RasterizedImage, and ImageRenderer - ImageDecoderCallback on Terminal for decoding PNG to RGBA, with the Qt-based decoder registered in TerminalDisplay::setSession() - PNG decoding in both named uploadImage() and oneshot renderImage() paths, so images sent as PNG are properly decoded before upload - Auto-compute grid size from image pixel dimensions when the sender specifies c=0,r=0 (zero grid extent), using cellPixelSize() - GoodImageProtocol DA1 attribute (code 11) advertised in device attributes response - ImageRenderer routes tiles to below-text or above-text queues based on ImageLayer - MessageParser MaxBodyLength increased to 16 MB - Render status reply changed to CSI > N i format Signed-off-by: Christian Parpart <christian@parpart.family>
…ling Replace the stub `contour image` command with a fully functional `contour cat` command for displaying images via the Good Image Protocol: - Implement parseSize(), parseImageAlignment(), parseImageResize(), and parseImageLayer() with proper string-to-enum conversion - Add --layer option for z-ordering (below/replace/above) - Open image files in binary mode to prevent data corruption - Call crispy::base64::finish() to flush residual base64 bytes with proper padding after encoding - Validate file existence before attempting display - Use reinterpret_cast instead of C-style casts Signed-off-by: Christian Parpart <christian@parpart.family>
Add comprehensive test coverage for all GIP DCS operations: - Upload: RGB, RGBA, missing name, invalid format - Render: by name, nonexistent name, status success/failure responses - Oneshot: render with and without layer parameter - Release: by name, nonexistent name (no-op) - DA1: verify GIP code 11 in device attributes response - Layer: Below/Replace/Above z-ordering via L parameter - Edge cases: zero grid size, MaxBodyLength constant Signed-off-by: Christian Parpart <christian@parpart.family>
Cell write paths unconditionally cleared image fragments when text was written, breaking Below (layer 0) and Above (layer 2) images that should coexist with text per the GIP spec. Only Replace (layer 1) images should be destroyed by text writes (Sixel-compatible behavior). Add shouldPreserveImageOnTextWrite() helper that checks the image layer and guard all six cell write paths (CompactCell::write x2, setCharacter; SimpleCell::write x2, setCharacter) with this predicate. Reset/erase operations remain unconditionally destructive. Signed-off-by: Christian Parpart <christian@parpart.family>
Signed-off-by: Christian Parpart <christian@parpart.family>
- Fix "seperated" typos in MessageParser.h Doxygen comments - Replace old Apache boilerplate with SPDX headers in MessageParser files - Refactor toNumber() to return std::optional<int> with overflow protection - Use value_or() for safe optional dereferences in GIP render/oneshot handlers - Add size() > 1 guard before base64 decode of header values and body - Verify ifs.read() success in readFile() with gcount check - Remove dead decodeImage() method from TerminalDisplay Signed-off-by: Christian Parpart <christian@parpart.family>
- Screen.cpp: Return nullopt for empty strings in toNumber() (per @Copilot) - Screen.cpp: Remove stale TODO comment on toImageResizePolicy fallback (per @Copilot) - Screen.cpp: Log error on failed PNG decode for upload and oneshot (per @Copilot) - ContourApp.cpp: Check file size against 16 MB GIP limit before sending (per @Copilot) Signed-off-by: Christian Parpart <christian@parpart.family>
88575d9 to
3a62dfc
Compare
- Screen.cpp: Return nullopt for empty strings in toNumber() (per @Copilot) - Screen.cpp: Remove stale TODO comment on toImageResizePolicy fallback (per @Copilot) - Screen.cpp: Log error on failed PNG decode for upload and oneshot (per @Copilot) - ContourApp.cpp: Check file size against 16 MB GIP limit before sending (per @Copilot) Signed-off-by: Christian Parpart <christian@parpart.family>
3a62dfc to
9dc4a9f
Compare
Rename private member and parameter identifiers to match the project's naming conventions enforced by clang-tidy (underscore-prefixed privates, camelBack parameters without leading underscore). Signed-off-by: Christian Parpart <christian@parpart.family>
Implement spec-mandated features identified during cross-check analysis: - I1: Parse update-cursor (u) parameter in render/oneshot handlers; cursor movement now conditional on the flag (default: no move for GIP) - I2: Upload status responses (CSI > 0 i success, CSI > 2 i invalid) when request-status (s) flag is set - I3: Query operation (DCS q ST) returning pool limits via CSI > 8;Pm;Pb;Pw;Ph i - I4: RGB/RGBA data size validation (body must equal w*h*bpp) - I5: SGR background color used for image padding instead of black - I6: Image name validation (ASCII alphanumeric + underscore, 1-512 chars) - I7: Sub-rectangle rendering via imageOffset/imageSubSize passed through to RasterizedImage fragment generation (SIMD and scalar paths) - I8: Oneshot handler now parses s (request-status) and u (update-cursor) Bug fix: MessageParser Base64 decoded bodies were not resized to the actual decoded length, leaving trailing garbage bytes when the original data size was not a multiple of 3. This caused the new data size validation to reject valid uploads. Tests: 9 new test cases covering update-cursor, upload status, name validation, query, and oneshot status. All 32 GIP tests pass (67 assertions), all 419 vtbackend tests pass (4426 assertions). Signed-off-by: Christian Parpart <christian@parpart.family>
Closes #100
Summary
Reference implementation of the Good Image Protocol — a DCS-based protocol for displaying raster images in terminal emulators with support for named image pools, configurable layout, and layer-based compositing.
Protocol Operations
DCS u ... STDCS r ... STDCS s ... STDCS d ... STKey Features
no-resize,fit(default),fill,stretchtop-startthroughbottom-end, defaultmiddle-center);11in device attributes responsesflag on render returnsCSI > 0 i(success) orCSI > 1 i(failure)Layer Semantics
L=0L=1L=2Erase operations (
ED,EL) clear images regardless of layer.CLI:
contour catEmits a GIP oneshot sequence to display the image at the current cursor position.