From 1802ed13ee5162afabb4c4f598f96a665d87ccc9 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 21 Jul 2026 16:33:33 +0000 Subject: [PATCH 05/13] fix(vendor): decode kitty graphics PNG transmissions before storing processKittyGraphics() (upstream code) accepted f=100 (PNG) transmissions and stored the still-compressed PNG bytes directly via ImagePool::create(), tagged with whatever pixel size the wire supplied -- which validateKittyTransmission() correctly does NOT require for PNG (the file states its own size), so senders that omit s=/v= for a PNG transmission (e.g. chafa --format=kitty on a .jpg/.png) leave the stored Image at 0x0. The rasterizer trusts Image::size() literally and reads width*height*4 bytes as raw RGBA from a buffer that is actually compressed PNG data, producing a plausible-looking sliver of decoded-as-raw noise for the first row or so before the degenerate geometry cuts it off -- reproduced with `chafa --format=kitty` on a JPEG cover image (works fine via --format=iterm/sixel, which both go through decodePng() already). Fix: when command.format is Png, decode via decodePng() (the same helper renderITerm2InlineImage()/uploadImage(name, ...) already use for their own PNG paths) before calling imagePool().create(), so the stored Image always holds real RGBA pixels at their real decoded dimensions, never raw compressed bytes. Verified separately (not a Screen.cpp bug): mpv --vo=kitty's "text leaking onto the screen" symptom is a known upstream mpv issue (OSD/status-text writes racing with image-escape-sequence writes, unsynchronized on mpv's side -- see https://github.com/ismail-yilmaz/Bobcat/issues/64), reproduced on multiple other terminals (xterm, wezterm, Bobcat) with mpv's sixel VO and confirmed by the user to also affect its kitty VO here; `mpv --vo=kitty --really-quiet` works around it. Not a contour defect; a raw byte-level PTY capture of mpv's kitty output was verified byte-for-byte well-formed (clean APC framing, valid base64, correct m=1/m=0 chunking, all 800 chunks/14 frames reassemble cleanly against a faithful simulation of Screen.cpp's exact reassembly logic) before ruling this out. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LzTmVgP2ruMz987VJ78k77 (cherry picked from commit 8a627d5c73af7ff9599fab9875db2007deb705e2) --- src/vtbackend/Screen.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/vtbackend/Screen.cpp b/src/vtbackend/Screen.cpp index cb7cd777..57eb557a 100644 --- a/src/vtbackend/Screen.cpp +++ b/src/vtbackend/Screen.cpp @@ -5535,7 +5535,7 @@ void Screen::processKittyGraphics(std::string_view body) } return ImageFormat::RGBA; }(); - auto const pixelSize = + auto pixelSize = ImageSize { Width::cast_from(command.pixelWidth), Height::cast_from(command.pixelHeight) }; if (format != ImageFormat::PNG) @@ -5551,7 +5551,28 @@ void Screen::processKittyGraphics(std::string_view body) } } - auto image = _terminal->imagePool().create(format, pixelSize, std::move(pixmap)); + // PNG transmissions (f=100) carry no reliable s=/v=: the file states its own size, and senders + // (e.g. chafa) routinely omit the wire dimensions entirely, leaving pixelSize at 0x0. The renderer + // downstream trusts Image::size() literally, so storing the still-compressed PNG bytes under that + // wrong size makes it read PNG bytes as if they were raw RGBA pixels -- decode here first, exactly + // as renderITerm2InlineImage/uploadImage(name, ...) already do for their own PNG paths, so the + // stored Image always holds real RGBA pixels at their real decoded size. + auto storedFormat = format; + if (format == ImageFormat::PNG) + { + auto decodedSize = pixelSize; + auto decodedData = decodePng(pixmap, decodedSize); + if (!decodedData) + { + replyKittyGraphics(command, "EINVAL:could not decode image"); + return; + } + pixmap = std::move(*decodedData); + pixelSize = decodedSize; + storedFormat = ImageFormat::RGBA; + } + + auto image = _terminal->imagePool().create(storedFormat, pixelSize, std::move(pixmap)); if (!image) { replyKittyGraphics(command, "EINVAL:could not decode image");