Skip to content

[terminal] Good Image Protocol (GIP) implementation#317

Open
christianparpart wants to merge 11 commits intomasterfrom
feature/good-image-protocol
Open

[terminal] Good Image Protocol (GIP) implementation#317
christianparpart wants to merge 11 commits intomasterfrom
feature/good-image-protocol

Conversation

@christianparpart
Copy link
Copy Markdown
Member

@christianparpart christianparpart commented Jul 11, 2021

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 Sequence Operation Description
DCS u ... ST Upload Store a named image (RGB/RGBA/PNG) in the image pool
DCS r ... ST Render Place a stored image on the grid by name
DCS s ... ST Oneshot Upload + render in a single sequence (unnamed)
DCS d ... ST Release Remove a named image from the pool

Key Features

  • Named image pool with LRU eviction — upload once, render many times without retransmission
  • Three compositing layers: Below (L=0, watermark/background), Replace (L=1, default/Sixel-compatible), Above (L=2, overlay)
  • Resize policies: no-resize, fit (default), fill, stretch
  • Alignment: 9-position grid (top-start through bottom-end, default middle-center)
  • Image formats: RGB, RGBA, PNG (terminal-decoded)
  • DA1 capability advertisement: Code ;11 in device attributes response
  • Status reply: Optional s flag on render returns CSI > 0 i (success) or CSI > 1 i (failure)
  • Base64 transport: Binary data encoded for reliable VT sequence transmission (max 16 MB)

Layer Semantics

Layer Value Text interaction Use case
Below L=0 Text coexists on top; image preserved on text write Background / watermark
Replace L=1 Text destroys image (Sixel-compatible) Default image display
Above L=2 Text coexists underneath; image preserved on text write Overlay / HUD

Erase operations (ED, EL) clear images regardless of layer.

CLI: contour cat

contour cat [--resize fit|fill|stretch|none] [--align middle-center|...] \
            [--size COLSxROWS] [--layer 0|1|2|below|replace|above] IMAGE_FILE

Emits a GIP oneshot sequence to display the image at the current cursor position.

@github-actions github-actions bot added documentation Improvements or additions to documentation frontend Contour Terminal Emulator (GUI frontend) OpenGL test Unit tests VT: Backend Virtual Terminal Backend (libterminal API) labels Jul 11, 2021
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from a5d308a to fd370a7 Compare July 11, 2021 08:43
@christianparpart christianparpart changed the base branch from master to feature/random July 11, 2021 08:43
@christianparpart christianparpart force-pushed the feature/random branch 3 times, most recently from a0be723 to dfacb3d Compare July 17, 2021 09:12
Base automatically changed from feature/random to master July 17, 2021 09:59
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from fd370a7 to 5825bef Compare July 17, 2021 20:49
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from 5825bef to 99af6be Compare July 29, 2021 13:29
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch 2 times, most recently from a4450f7 to e0aff93 Compare March 20, 2022 21:37
@github-actions github-actions bot added CMake and removed documentation Improvements or additions to documentation labels Mar 20, 2022
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch 3 times, most recently from e6b8214 to 06fff93 Compare April 21, 2022 20:40
@christianparpart christianparpart marked this pull request as ready for review April 21, 2022 20:43
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch 4 times, most recently from af0c49f to 03ab5c9 Compare April 22, 2022 23:27
@christianparpart christianparpart marked this pull request as draft June 22, 2022 21:08
@christianparpart christianparpart force-pushed the master branch 2 times, most recently from 1a1c769 to b93a36e Compare August 7, 2022 08:54
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from 03ab5c9 to 00bafa0 Compare September 18, 2022 18:04
@github-actions github-actions bot removed the OpenGL label Sep 18, 2022
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MessageParser for parsing DCS message format with base64 encoding support
  • Adds contour cat CLI 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.

christianparpart added a commit that referenced this pull request Feb 19, 2026
- 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>
christianparpart added a commit that referenced this pull request Feb 19, 2026
- 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>
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from 1a3f583 to e78ab55 Compare February 19, 2026 12:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

christianparpart added a commit that referenced this pull request Feb 19, 2026
- 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>
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from e78ab55 to 348929e Compare February 19, 2026 13:02
christianparpart added a commit that referenced this pull request Feb 19, 2026
- 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>
christianparpart added a commit that referenced this pull request Feb 19, 2026
- 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>
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from 88575d9 to 3a62dfc Compare February 19, 2026 13:23
- 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>
@christianparpart christianparpart force-pushed the feature/good-image-protocol branch from 3a62dfc to 9dc4a9f Compare February 19, 2026 17:06
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI GitHub Actions & CI CMake frontend Contour Terminal Emulator (GUI frontend) test Unit tests VT: Backend Virtual Terminal Backend (libterminal API) VT: rasterizer Rendering of the terminal into a pixmap using `terminal_renderer` library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Good" image protocol

2 participants