Skip to content

Add graphics in terminal support: - Sixel and iTerm2 protocols#2973

Open
MatanZ wants to merge 10 commits intotermux:masterfrom
MatanZ:sixel4
Open

Add graphics in terminal support: - Sixel and iTerm2 protocols#2973
MatanZ wants to merge 10 commits intotermux:masterfrom
MatanZ:sixel4

Conversation

@MatanZ
Copy link
Copy Markdown
Contributor

@MatanZ MatanZ commented Sep 4, 2022

This commit implements graphics in the terminal, including two different protocols for placing images:

  1. Sixel, and
  2. iTerm2

I ran this for a few days, and it seems to me that some people tried it from #142, and saw no crashes, so I hope it is reasonable safe.

  • In TerminalEmulator, interpret sixel sequences, and send them to
    TerminalBuffer for constructing a bitmap.
  • Sixel sequences may be longer than 8192 characters, so break them in
    natural places ($,-,#), rather than collecting all in the buffer.
  • The bitmap is sliced to character cell sized slices, and each the
    the style attribute is used to store which bitmap slice is displayed
    in place of this character.
  • In TerminalRenderer the style is interpreted, and drawn using
    drawBitmap, instead of drawText.

Support iTerm2 inline image protocol (OSC 1337):

  • Using the same bitmap display infrastructure introduced for sixels.
  • Collects the image data outside of the OSC buffer.
  • Ignoring some parameters.

Small emulator changes:

  • Also eat APC sequences, not echoing to screen.
  • Fix CSI 14 t to give actual size
  • Add CSI 16 t
  • Add 4 (sixel) to device attributes

For those who tried this branch already, I force pushed to it in order to remove changes that may not be acceptable.

The main known issue is that if graphics sequence is interrupted (in specific places), the emulator remains stuck waiting for the sequence to end, thus ignoring all input. This requires terminal reset from the menu.

@aicynide
Copy link
Copy Markdown

aicynide commented Sep 5, 2022

You are a hero my brother

@TermuxMonet
Copy link
Copy Markdown

It works! but gifs are being duplicated on the terminal when expanding and collapsing the keyboard

Regular images are being displayed fine

Screenshot_20220905-194040_Termux

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Sep 6, 2022

It works! but gifs are being duplicated on the terminal when expanding and collapsing the keyboard

This is not a bug. img2sixel displays animations by sending each frame as a different sixel with a command to send the cursor to position (1,1) between them (CSI H). This causes each frame to overwrite the previous one.

When you remove the keyboard, the screen gets taller, and termux handles this by scrolling down, so the previous frame which was at position (1,1), is now lower, so it is not covered by the following frames.

@TermuxMonet
Copy link
Copy Markdown

oh, so everything's working fine then

@TermuxMonet
Copy link
Copy Markdown

TermuxMonet commented Sep 6, 2022

Here's a small guide for anyone else who wants to test this PR

For displaying images and gifs using Sixel, do pkg install libsixel and use img2sixel image.png

For displaying images using iTerm2, download the imgcat script, and use it with the command ./imgcat image.png

@sylirre
Copy link
Copy Markdown
Member

sylirre commented Sep 6, 2022

Has issues with big images. For example when using imgcat without limiting width Termux app may get crashed due to out-of-memory exception:

09-06 19:10:37.740 10702 10702 E Termux  : java.lang.OutOfMemoryError: Failed to allocate a 521628640 byte allocation with 74375880 free bytes and 232MB until OOM, target footprint 99167840, growth limit 268435456
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalBuffer.resizeBitmap(TerminalBuffer.java:48)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalBuffer.addImage(TerminalBuffer.java:199)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.doOscSetTextParameters(TerminalEmulator.java:2304)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.doOsc(TerminalEmulator.java:2043)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:577)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:553)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:502)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:345)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.os.Handler.dispatchMessage(Handler.java:106)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.os.Looper.loopOnce(Looper.java:201)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.os.Looper.loop(Looper.java:288)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.app.ActivityThread.main(ActivityThread.java:7898)
09-06 19:10:37.740 10702 10702 E Termux  :      at java.lang.reflect.Method.invoke(Native Method)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

Test image (7779x4191): https://user-images.githubusercontent.com/107305601/188685258-87eb0704-12c5-4b43-a47a-a1deae99e208.jpg

Device config: Pixel 5, Android 13, 8 GiB RAM.

@TermuxMonet
Copy link
Copy Markdown

TermuxMonet commented Sep 6, 2022

Has issues with big images. For example when using imgcat without limiting width Termux app may get crashed due to out-of-memory exception:

09-06 19:10:37.740 10702 10702 E Termux  : java.lang.OutOfMemoryError: Failed to allocate a 521628640 byte allocation with 74375880 free bytes and 232MB until OOM, target footprint 99167840, growth limit 268435456
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalBuffer.resizeBitmap(TerminalBuffer.java:48)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalBuffer.addImage(TerminalBuffer.java:199)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.doOscSetTextParameters(TerminalEmulator.java:2304)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.doOsc(TerminalEmulator.java:2043)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:577)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:553)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:502)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:345)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.os.Handler.dispatchMessage(Handler.java:106)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.os.Looper.loopOnce(Looper.java:201)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.os.Looper.loop(Looper.java:288)
09-06 19:10:37.740 10702 10702 E Termux  :      at android.app.ActivityThread.main(ActivityThread.java:7898)
09-06 19:10:37.740 10702 10702 E Termux  :      at java.lang.reflect.Method.invoke(Native Method)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
09-06 19:10:37.740 10702 10702 E Termux  :      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

Test image (7779x4191): https://user-images.githubusercontent.com/107305601/188685258-87eb0704-12c5-4b43-a47a-a1deae99e208.jpg

Device config: Pixel 5, Android 13, 8 GiB RAM.

Image displays fine on termux-monet with android:largeHeap="true" flag. Try adding this flag to your AndroidManifest.xml, it should fix oom exception. largeHeap flag does let applications to use more RAM.
And also, don't forget to disable PhantomProcessKiller, since you're on Android 13.

Device Config: Xiaomi POCO F1, Android 12L, 6GB RAM

Screenshot_20220906-133931_Termux

@cogburnd02
Copy link
Copy Markdown

I believe if MatanZ gets this imported and issue 142 gets solved, then MatanZ gets my $100 bounty on Bountysource for that issue. It will be well worth it!! :-D

@sylirre
Copy link
Copy Markdown
Member

sylirre commented Sep 6, 2022

Thanks, but anyway there is some proper workaround needed to avoid OOM. Larger heap indeed can solve it for a particular case. Not all devices have lots of memory and part is always used by other apps including system, max heap size also vary between device models.

Unfortunately I can't propose the best way to implement that, but I think refusing to display supersized bitmaps is better than crashing all sessions. For example if dimensions are too big, user can get a toast message describing the issue.

Command line programs should not be able to crash terminal or cause resource overload by just sending some control sequence (no matter whether it is sixel or something else).

@TermuxMonet
Copy link
Copy Markdown

TermuxMonet commented Sep 6, 2022

Thanks, but anyway there is some proper workaround needed to avoid OOM. Larger heap indeed can solve it for a particular case. Not all devices have lots of memory and part is always used by other apps including system, max heap size also vary between device models.

Unfortunately I can't propose the best way to implement that, but I think refusing to display supersized bitmaps is better than crashing all sessions. For example if dimensions are too big, user can get a toast message describing the issue.

Command line programs should not be able to crash terminal or cause resource overload by just sending some control sequence (no matter whether it is sixel or something else).

I don't believe that making the terminal refuse to display oversized images is necessary, since most of command line programs always will be able to crash the application when overloaded or abused.

That would only limit how the user should use his terminal, creating a "fake error" that doesn't even exist for some users, like the people who has enough RAM for displaying those images.

Those people who can display the images wouldn't be able to.

But if you guys decide that's the best thing to do, i propose placing a limit only for devices with less than 4GB RAM.

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Sep 6, 2022

Here's a small guide for anyone else who wants to test this PR

For displaying images and gifs using Sixel, do pkg install libsixel and use img2sixel image.png

For displaying images using iTerm2, download the imgcat script, and use it with the command ./imgcat image.png

Just so I can get more testing from the nice people who test my code:

  • There are other sixel encoders, for example ImageMagick. Simple example: convert image.jpg sixel:-
  • gnuplot can also output sixel, but I am not sure if it uses its own encoder, or one of the above. Use set terminal sixelgd.
  • imgcat has some parameters to control scaling of the image, but you don't even need it. iterm2 protocol is as simple as: echo -en '\e]1337;File=inline=1;keepAspectRatio=0;height=70%:' ; base64 -w 0 /sdcard/z1.jpg ; echo -e '\e\\' . You can have width parameter in addition to height.

@TermuxMonet
Copy link
Copy Markdown

Tested with imagemagick, and it's displaying fine on my side

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Sep 6, 2022

Thanks, but anyway there is some proper workaround needed to avoid OOM. Larger heap indeed can solve it for a particular case. Not all devices have lots of memory and part is always used by other apps including system, max heap size also vary between device models.

Unfortunately I can't propose the best way to implement that, but I think refusing to display supersized bitmaps is better than crashing all sessions. For example if dimensions are too big, user can get a toast message describing the issue.

Command line programs should not be able to crash terminal or cause resource overload by just sending some control sequence (no matter whether it is sixel or something else).

I hope this solves the problem. I put a try{}catch() block around each bitmap operation that allocates memory. This should be similar to what you suggest - ignoring large images, without hard coding a definition of large.

I believe it is better without a toast notification. Usually when a terminal cannot (or does not want to) perform a certain operation it just ignores it, without notifying the user. Maybe a line in the log.

@agnostic-apollo
Copy link
Copy Markdown
Member

@MatanZ Please use dedicated class for long sixel logic in TerminalEmulator and TerminalBuffer.

Trying to catch OutOfMemoryError and freeing resources "should" work. For normal images, one can display bitmap at lowed res, not sure if something like that can be done with sixel, instead of showing nothing at all.

Checking current memory before even trying to display large image could be done too.

https://developer.android.com/reference/android/app/ActivityManager#getMemoryInfo(android.app.ActivityManager.MemoryInfo)

https://developer.android.com/topic/performance/graphics/load-bitmap#read-bitmap

Maybe a line in the log.

This is the way. No toasts. Being done in other places too.

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Sep 7, 2022

@MatanZ brother kitty image protocol when?

The way graphics display is implemented, it is not possible to implement kitty protocol. It is possible to implement very simplified ("put image here"), but image and placement management, as well as combining text and images is impossible.

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Sep 7, 2022

@MatanZ Please use dedicated class for long sixel logic in TerminalEmulator and TerminalBuffer.

Refactored some code to two new classes. I don't see anything in TerminalEmulator that belongs in another class, or which may become clearer.

Trying to catch OutOfMemoryError and freeing resources "should" work. For normal images, one can display bitmap at lowed res, not sure if something like that can be done with sixel, instead of showing nothing at all.

Checking current memory before even trying to display large image could be done too.

https://developer.android.com/reference/android/app/ActivityManager#getMemoryInfo(android.app.ActivityManager.MemoryInfo)
https://developer.android.com/topic/performance/graphics/load-bitmap#read-bitmap

I am not sure this is a good idea. I would have to add assumptions about how much memory is used by the android for various bimap manipulations (decode, scale, resize). I'll settle for trying anything the user asks for, and catching the errors. I did not crash termux with this, with all the above, and various other large images.

@agnostic-apollo
Copy link
Copy Markdown
Member

Thanks. Will take another look later myself during merging.

You can also let OutOfMemoryError happen and then try to display lower res once and see if OutOfMemoryError triggers or not and abort if it did. This way would increase chances for user to get to see something than nothing at all. Of course that would add code, so just a suggestion.

@sylirre
Copy link
Copy Markdown
Member

sylirre commented Sep 7, 2022

Seems like there is a problem with some GIFs. Notice the artifacts at the left part. Same as #142 (comment)?

Gif file doesn't seem to be corrupted.

screen-20220908-005128.2.mp4

This doesn't happen with another gif:

screen-20220908-005944.2.mp4

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Sep 8, 2022

I cannot see those videos. And can you please include the actual files used, and the commands that cause the problem?

@sylirre
Copy link
Copy Markdown
Member

sylirre commented Sep 8, 2022

Perhaps the issue with Termux img2sixel utility, not with application.

I can reproduce it when connected over SSH to Termux from Konsole on laptop. But I can't reproduce the issue when animation is played via img2sixel directly in Konsole.

Screenshot with the problem (notice the block with lines on the left side):
Screenshot_20220908-115530

You can get the file from https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Rotating_earth_%28large%29.gif/274px-Rotating_earth_%28large%29.gif


libsixel package version differences:

  • Termux
    img2sixel 1.10.3
    
    configured with:
      libcurl: no
      libpng: no
      libjpeg: no
      gdk-pixbuf2: no
      GD: no
    
  • Laptop (OpenSUSE Tumbleweed)
    img2sixel 1.10.3
    
    configured with:
      libcurl: yes
      libpng: no
      libjpeg: yes
      gdk-pixbuf2: yes
      GD: yes
    

The difference in a build time configuration. Though maybe OpenSUSE also applies some patches to the package.

sylirre added a commit to termux/termux-packages that referenced this pull request Sep 8, 2022
Fixes issue with some GIFs.

See my comment at termux/termux-app#2973 (comment)

Also enable libcurl support.
@sylirre
Copy link
Copy Markdown
Member

sylirre commented Sep 8, 2022

Commit termux/termux-packages@b72be3c resolves img2sixel issue with animation.

termux-pacman-bot added a commit to termux-pacman/termux-packages that referenced this pull request Sep 8, 2022
Fixes issue with some GIFs.

See my comment at termux/termux-app#2973 (comment)

Also enable libcurl support.
@sylirre

This comment was marked as off-topic.

@sylirre

This comment was marked as off-topic.

@sylirre
Copy link
Copy Markdown
Member

sylirre commented Sep 9, 2022

If some package doesn't work - open a new issue in https://github.com/termux/termux-packages/issues.

@leap0x7b
Copy link
Copy Markdown

  • ✅ Works
  • ℹ️ Has problems/implementation issues
  • ❌ Doesn't work

Sixel coverage:

  • img2sixel
    Screenshot_20220917-173331.png
  • ✅ ImageMagick convert
    Screenshot_20220917-173412.png
  • ✅ Chafa
    Screenshot_20220917-173612.png
  • ✅ lsix
    Screenshot_20220917-173430.png
  • ✅ neofetch
    Screenshot_20220917-173458.png

Conclusion: All applications that uses and/or supports Sixel works properly

iTerm2 coverage:

  • ✅ Basic echo method
    Screenshot_20220917-173833.png
  • ✅ iTerm2 imgcat
    Screenshot_20220917-173740.png
  • ❌ Chafa
    When using chafa --format iterm2, Termux just straight up crashes
  • ℹ️ neofetch
    The image for some reason is in the bottom which shouldn't be
    Screenshot_20220917-174017.png
    After all of the information is fully displayed, the image for some reason suddenly vanished
    Screenshot_20220917-174031.png

Conclusion: Not all applications that uses and/or supports iTerm2 works properly

@TermuxMonet
Copy link
Copy Markdown

  • Chafa iTerm2 (chafa --format iterm2) crash report:

Report Info

User Action: crash report
Sender: TermuxActivity
Report Timestamp: 2022-09-17 11:30:33.505 UTC

Crash Details

Crash Thread: Thread[main,5,main]
Crash Timestamp: 2022-09-17 11:30:29.822 UTC

Crash Message:

Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference

Stacktrace

java.lang.NullPointerException: Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference
	at android.graphics.Bitmap.createScaledBitmap(Bitmap.java:798)
	at com.termux.terminal.TerminalBitmap.<init>(TerminalBitmap.java:93)
	at com.termux.terminal.TerminalBuffer.addImage(TerminalBuffer.java:593)
	at com.termux.terminal.TerminalEmulator.doOscSetTextParameters(TerminalEmulator.java:2307)
	at com.termux.terminal.TerminalEmulator.doOsc(TerminalEmulator.java:2046)
	at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:577)
	at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:553)
	at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:502)
	at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:345)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loopOnce(Looper.java:201)
	at android.os.Looper.loop(Looper.java:288)
	at android.app.ActivityThread.main(ActivityThread.java:7875)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

@agnostic-apollo
Copy link
Copy Markdown
Member

agnostic-apollo commented Apr 9, 2026

Hi @MatanZ, thanks for implementing sixels and iTerm images. Sorry, it took this long to look over this, didn't get time (not that I had time right now).

I have added my changes in 948ffa4 and 430ae28 primarily, but have kept your old ones in a7f2872 for now after rebasing against master and fixing conflicts. I will squash both sixel commits before merging pull, leaving it now to track old and new changes. While I have reviewed/fixed/improved changes for bitmaps/sixel/iterm, I have not fully looked at the rendering/scaling bitmap parts, but they seem to be working fine, will look into them in future when I have more time and issues arise.

Users should thoroughly test the terminal for both sixel and iterm images, and also other text editors, etc in case something got broken. I have done by own testing for the entire protocols, but not a variety of images (primarily tested attached interstellar image). Once testing has been done and @MatanZ reviews the changes, I will merge the pull and make a new Termux app version release for F-Droid users.

You can grab APK build for current push from https://github.com/termux/termux-app/actions/runs/24187595506?pr=2973

Commit message for 430ae28 is below.


Added: Add terminal support for Bitmaps, Sixel (DCS q) and iTerm Image (OSC 1337)

Support for displaying bitmaps inside the terminal has been added via TerminalBitmap which can be created from image byte[] or sixel bitmap. The bitmaps are sliced to character cell sized slices. The TerminalBuffer stores a map for bitmap number to the TerminalBitmap loaded in the terminal. The bitmap number and coordinates are encoded in the long TerminalRow.mStyle for the TerminalRow character of a column by TerminalBitmap#buildOrThrow() by getting encoded value from TextStyle.encodeTerminalBitmap(). The TerminalRenderer.render() then checks during rendering terminal output whether a character at a row/coloumn index is a bitmap instead of text by calling TextStyle.isTerminalBitmap(), then draws it using Canvas.drawBitmap() instead of Canvas.drawText().

Sixel images can be created with Sixel Device Control String command sent via DCS q s..s ST or DCS P1; P2; P3; q s..s ST.

  • The TerminalEmulator interprets sixel sequences, and sends them to TerminalBuffer for constructing a TerminalSixel. Once the sixel command has been completely processed, a TerminalBitmap is created from the TerminalSixel. If an error occurred during processing (like OOM), then remaining sixel command is completely read, but is ignored and no sixel is drawn (done by setting mTerminalSixel to null so that TerminalBuffer.sixelReadData() ignores further commands).
  • Since a sixel sequence can be very long to render a full image and can have length greater than TERMINAL_CONTROL_ARGS__MAX_LENGTH (16384), the entire sequence is not stored in the mTerminalControlArgs buffer before being processed as it will result in an overflow error, instead as soon as length crosses TERMINAL_CONTROL_ARGS__MAX_LENGTH / 2 and a complete sixel sub command (#, !, or ") has been received, it is immediately processed, and then further commands are read after emptying buffer.
  • If "rough" horizontal and vertical size of image is received at start of sixel data string with a Raster Attributes command, like done by img2sixel command, then sixel commands args buffer capacity (mTerminalControlArgs) is increased and sixel bitmap in TerminalSixel is resized at start, instead of having to keep resizing buffer/bitmap as more sixel data is received, which has a performance hit due to memory reallocations and copying.
  • The 4 (sixel) value has been added to CSI Primary Device Attributes terminal response.

The img2sixel command can be used to display sixel images after installing with libsixel package with pkg install libsixel, like with img2sixel --width=1000px image.jpg.

To manually send an escape sequence, check the digiater.nl link below, but it is too cumbersome to create images large enough to be easy viewable in the terminal.

See Also:

iTerm images can be created with 1337 Operating System Control command.

  • Both File= and MultipartFile= (chunk based) protocols are supported. The inline parameter should be 1 to display inline images in the terminal. Downloading images to Downloads folder with the value 0 will be ignored as that is not supported.
  • The escape sequences/image data cannot be greater than TERMINAL_CONTROL_ARGS__MAX_LENGTH (16384) bytes if sent via (File=) protocol, otherwise it will be ignored with an overflow error. For larger images, send images via MultipartFile= protocol in chunks with FilePart=, the imgcat utility uses that with 200-byte chunks if --legacy flag is not passed.
  • The TerminalEmulator interprets iTerm images sequences and creates an ITermImage to process parameters and store the base64 encoded image sent. Once all the data has been received, which can be over multiple OSC commands for MultipartFile= protocol, the encoded image is decoded to a byte[], which is then passed to TerminalBuffer, which creates a TerminalBitmap for the image.

The imgcat utility can be used for sending images, like with imgcat --width 1000px image.jpg (MultipartFile=) or imgcat --width 1000px --legacy image.jpg (File=).

To manually send an escape sequence, run echo -en '\e]1337;File=inline=1;keepAspectRatio=0;width=1000px;:' ; base64 -w 0 ./image.jpg ; echo -e '\e\\' (File=).

See Also:


284x177

interstellar

bitmap-tests

1920x1200

interstellar1

bitmap-large-tests

@Kreijstal
Copy link
Copy Markdown

finally I can get termux again

@robertkirkman
Copy link
Copy Markdown
Member

robertkirkman commented Apr 9, 2026

If someone who already has Termux installed wants to test this without necessarily having to back up, erase and then later reinstall the regular Termux, they can use this two.termux with this PR applied that I built.

I also built a com.termux one with similar settings, but have not linked it because agnostic-apollo already linked one above so if you don't already have a com.termux installed you should use their link.

In order to avoid speculation from anyone who notices that the build I linked also currently contains this other PR,

I would like to acknowledge that and also emphasize that the termux-generator links I send are not for replacing or competing with the official Termux as a fork with more PRs merged, so that detail should not be interpreted that way. I send them because I want PRs that "request testers" to be accessible to anyone who only has 1 device and for whom uninstalling and reinstalling the regular Termux constantly while testing PRs is frustrating or inconvenient.

@robertkirkman
Copy link
Copy Markdown
Member

robertkirkman commented Apr 10, 2026

I have tested this in comparison to the konsole package running in Termux:X11, and for the most part it looks great,

Screenshot_20260410-021158_Termux_X11

However, I do seem to have found a sixel image test case that konsole currently seems to be able to handle better than this PR.

When I use img2sixel arrow.jpg on this large-resolution arrow.jpg I found, this PR doesn't seem to be able to display any portion of it, while konsole displays the portion that it's able to fit into its window.

Screenshot_20260410-021502_Termux_X11

arrow

however, if I reduce the resolution of arrow.jpg to something more reasonable using an image editor, I'm able to display it in both konsole and this PR:

Screenshot_20260410-022526_two termux

arrow_small

That seems somewhat reasonable; there has to be a size limit at some point, and maybe the size limit of konsole is just greater than that of this PR.

@agnostic-apollo
Copy link
Copy Markdown
Member

Likely this is engaging.

https://github.com/termux/termux-app/pull/2973/changes#diff-f729a9bbec25dbca63392265f8852d3ec8d4f0afa848fccf3b2db837c457ab98R29

@robertkirkman
Copy link
Copy Markdown
Member

robertkirkman commented Apr 10, 2026

I see, I think 2048 is a reasonable limit for sixel.

I've tested imgcat (iterm2) now as well, and interestingly, I've noticed that imgcat is able to display the same image at full resolution with no problems,

Screenshot_20260410-035115_two termux

however, imgcat is also faster. It takes much less time to display the large image than img2sixel, even when img2sixel is limited to a working size with --width=2000.

If iterm2 is just more efficient than sixel, it's probably best for the size limit of sixel to be smaller than the size limit of iterm2 (as it currently is)

@agnostic-apollo
Copy link
Copy Markdown
Member

agnostic-apollo commented Apr 10, 2026

There are some discussions at following links about size limits, I choose 2048x2048 (which uses 16MB memory) as max sixel bitmap dimensions as android devices are generally more stricter with RAM allocation for apps than linux distros and will trigger an OOM, especially with all the other data and processes running in termux. Only issue seems to be that sixel command should still have been read completely without being printed to screen, and no image displayed, just like iterm.

contour-terminal/contour#680
contour-terminal/contour#656 (comment)

@agnostic-apollo
Copy link
Copy Markdown
Member

iterm (or possibly imgcat) is around 20 times faster than sixel, I did calculations and rendering same image with iterm was around 7-10ms on average, compared to around 200ms or more for sixel.

@robertkirkman
Copy link
Copy Markdown
Member

Only issue seems to be that sixel command should still have been read completely without being printed to screen, and no image displayed, just like iterm.

It displays no output for a small range of sizes around 2049-2100, then after that it starts displaying this large text artifact in the terminal, but shows similar messages in logcat both cases:

image

@agnostic-apollo
Copy link
Copy Markdown
Member

Can you post logcat errors for that?

@agnostic-apollo
Copy link
Copy Markdown
Member

agnostic-apollo commented Apr 10, 2026

The sixel repeat command Pn value 4500 is greater than max repeat value 2048 at index 3410 of sixel input.

Ah, a repeat value of 4500 is considered as an invalid sixel sequence and further input is not read, and that gets printed to screen. Will have to think on whether to continue reading or keep as is, probably former as 4500 is a valid integer sent by client, we just dont accept it. Other sixel command limits would need to be looked too.

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Apr 10, 2026

Hi @MatanZ, thanks for implementing sixels and iTerm images. Sorry, it took this long to look over this, didn't get time (not that I had time right now).

iTerm images can be created with 1337 Operating System Control command.

* Both `File=` and `MultipartFile=` (chunk based) protocols are supported. The `inline` parameter should be `1` to display inline images in the terminal. Downloading images to Downloads folder with the value `0` will be ignored as that is not supported.

* The escape sequences/image data cannot be greater than `TERMINAL_CONTROL_ARGS__MAX_LENGTH` (`16384`) bytes if sent via (`File=`) protocol, otherwise it will be ignored with an overflow error. For larger images, send images via `MultipartFile=` protocol in chunks with `FilePart=`, the `imgcat` utility uses that with 200-byte chunks if `--legacy` flag is not passed.

I understand supporting MultipartFile=. It was not in iterm2 when I wrote this patch. But intentionaly crippling File= seems to me pointless. There are a lot of older tools that only support 'File=' method. They work with my patch, but not with yours. Furthermore, limiting chunks to 16K, when iterm2 uses 1MB, with no way to query the terminal for this value, mean that programs need to test terminal type and have a database of terminal capabilities, which is not a reasonable approach.

I suggest removing the arbitrary 16K limit for both File= and MultipartFile= methods.

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Apr 10, 2026

There are some discussions at following links about size limits, I choose 2048x2048 (which uses 16MB memory) as max sixel bitmap dimensions as android devices are generally more stricter with RAM allocation for apps than linux distros and will trigger an OOM, especially with all the other data and processes running in termux. Only issue seems to be that sixel command should still have been read completely without being printed to screen, and no image displayed, just like iterm.

contour-terminal/contour#680 contour-terminal/contour#656 (comment)

Agan, I prefer to avoid unnecessary arbitrary limits. Termux does run on screens wider than 2048 pixels, and programs might want to use all that. Images need not be square, so one might want to draw a 2300x50 header for the page.

If you want to reduce the chance of OOM, maybe limit actual area, instead of each dimension separately.

Not that there are no problems with supporting large sixels. lsix for example, uses maximum sixel size rather than actual screen width to decide its display width.

@robertkirkman
Copy link
Copy Markdown
Member

I prefer to avoid unnecessary arbitrary limits. Termux does run on screens wider than 2048 pixels, and programs might want to use all that.

Do you happen to know whether Konsole and other terminal emulators are really fully unlimited, or do they just have a size limit that's really big?

@MatanZ
Copy link
Copy Markdown
Contributor Author

MatanZ commented Apr 10, 2026

There actually need to be limits, for reporting in XTSMGRAPHICS escape sequence.

In foot it is 10000 pixels. In konsole, 16384.

@robertkirkman
Copy link
Copy Markdown
Member

robertkirkman commented Apr 10, 2026

Maybe if the limit were set to something around 8192 in Termux, that would be an acceptable compromise? That is somewhat in between the limit agnostic-apollo wanted, and the limits of other terminals. Estimating based on Agnostic-apollo's previous calculation, I think that would put the maximum memory use somewhere in between 100 and 200 MB, which isn't actually much even on Android. (assuming only attempting to display one large image at a time)

…age (`OSC 1337`)

Support for displaying bitmaps inside the terminal has been added via `TerminalBitmap` which can be created from image `byte[]` or sixel bitmap. The bitmaps are sliced to character cell sized slices. The `TerminalBuffer` stores a map for bitmap number to the `TerminalBitmap` loaded in the terminal. The bitmap number and coordinates are encoded in the `long` `TerminalRow.mStyle` for the `TerminalRow` character of a column by `TerminalBitmap#buildOrThrow()` by getting encoded value from `TextStyle.encodeTerminalBitmap()`. The `TerminalRenderer.render()` then checks during rendering terminal output whether a character at a row/coloumn index is a bitmap instead of text by calling `TextStyle.isTerminalBitmap()`, then draws it using `Canvas.drawBitmap()` instead of `Canvas.drawText()`.

Sixel images can be created with Sixel Device Control String command sent via `DCS q s..s ST` or `DCS P1; P2; P3; q s..s ST`.

- The `TerminalEmulator` interprets sixel sequences, and sends them to `TerminalBuffer` for constructing a `TerminalSixel`. Once the sixel command has been completely processed, a `TerminalBitmap` is created from the `TerminalSixel`. If an error occurred during processing (like OOM), then remaining sixel command is completely read, but is ignored and no sixel is drawn (done by setting `mTerminalSixel` to `null` so that `TerminalBuffer.sixelReadData()` ignores further commands).
- Since a sixel sequence can be very long to render a full image and can have length greater than `TERMINAL_CONTROL_ARGS__MAX_LENGTH` (`16384`), the entire sequence is not stored in the `mTerminalControlArgs` buffer before being processed as it will result in an overflow error, instead as soon as length crosses `TERMINAL_CONTROL_ARGS__MAX_LENGTH / 2` and a complete sixel sub command (`#`, `!`, or `"`) has been received, it is immediately processed, and then further commands are read after emptying buffer.
- If "rough" horizontal and vertical size of image is received at start of sixel data string with a `Raster Attributes` command, like done by `img2sixel` command, then sixel commands args buffer capacity (`mTerminalControlArgs`) is increased and sixel bitmap in `TerminalSixel` is resized at start, instead of having to keep resizing buffer/bitmap as more sixel data is received, which has a performance hit due to memory reallocations and copying.
- The `4` (sixel) value has been added to `CSI` `Primary Device Attributes` terminal response.

The `img2sixel` command can be used to display sixel images after installing with `libsixel` package with `pkg install libsixel`, like with `img2sixel --width=1000px image.jpg`.

To manually send an escape sequence, check the `digiater.nl` link below, but it is too cumbersome to create images large enough to be easy viewable in the terminal.

See Also:
- https://vt100.net/docs/vt3xx-gp/chapter14.html
- https://en.wikipedia.org/wiki/Sixel
- https://www.digiater.nl/openvms/decus/vax90b1/krypton-nasa/all-about-sixels.text

iTerm images can be created with `1337` Operating System Control command.

- Both `File=` and `MultipartFile=` (chunk based) protocols are supported. The `inline` parameter should be `1` to display inline images in the terminal. Downloading images to Downloads folder with the value `0` will be ignored as that is not supported.
- The escape sequences/image data cannot be greater than `TERMINAL_CONTROL_ARGS__MAX_LENGTH` (`16384`) bytes if sent via (`File=`) protocol, otherwise it will be ignored with an overflow error. For larger images, send images via `MultipartFile=` protocol in chunks with `FilePart=`, the `imgcat` utility uses that with 200-byte chunks if `--legacy` flag is not passed.
- The `TerminalEmulator` interprets iTerm images sequences and creates an `ITermImage` to process parameters and store the base64 encoded image sent. Once all the data has been received, which can be over multiple `OSC` commands for `MultipartFile=` protocol, the encoded image is decoded to a `byte[]`, which is then passed to `TerminalBuffer`, which creates a `TerminalBitmap` for the image.

The `imgcat` utility can be used for sending images, like with `imgcat --width 1000px image.jpg` (`MultipartFile=`) or `imgcat --width 1000px --legacy image.jpg` (`File=`).

To manually send an escape sequence, run `echo -en '\e]1337;File=inline=1;keepAspectRatio=0;width=1000px;:' ; base64 -w 0 ./image.jpg ; echo -e '\e\\'` (`File=`).

See Also:
- https://iterm2.com/documentation-images.html
- https://iterm2.com/utilities/imgcat
- https://github.com/gnachman/iTerm2-shell-integration/blob/d1d4012068c3c6761d5676c28ed73e0e2df2b715/utilities/imgcat
The `TerminalBitmap.MAX_BITMAP_SIZE` defines the max size of a Terminal bitmap for its pixels. Each pixel is stored on 4 bytes for a `Bitmap.Config.ARGB_8888` bitmap color config. The value should normally be between `100-200MB` depending on device and Android version. Check the variable docs for more info.

The sixel image size cannot be greater than `TerminalBitmap.MAX_BITMAP_SIZE`. The repeat value for sixel Graphics Repeat Introducer command cannot be greater than `TerminalSixel.SIXEL__MAX_REPEAT` (`8192`).

The iTerm image data sent for `File=` and `MultipartFile=` protocols cannot be greater than `TerminalBitmap.MAX_BITMAP_SIZE` bytes.
@agnostic-apollo
Copy link
Copy Markdown
Member

@MatanZ and @robertkirkman agreed that the limits were low, I have increased them with 5743c5d. I looked into what limits Android itself was using for different components, and I have increased them to support 7680x4320 (8K UHD) images depending on Android version and device, more info in TerminalBitmap.MAX_BITMAP_SIZE variable docs at https://github.com/MatanZ/termux-app/blob/5743c5dee233ddec4e1dc6fc1ebaa752e7105b49/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java#L42-L123. If an OOM happens, image rendering will just be ignored too anyways.

a repeat value of 4500 is considered as an invalid sixel sequence and further input is not read, and that gets printed to screen.

This has been fixed too. Repeat value cannot be greater than 8192 now, or rendering will be ignored.

You can grab APK build for current push from https://github.com/termux/termux-app/actions/runs/24365026780?pr=2973

@robertkirkman
Copy link
Copy Markdown
Member

@robertkirkman
Copy link
Copy Markdown
Member

a repeat value of 4500 is considered as an invalid sixel sequence and further input is not read, and that gets printed to screen.

This has been fixed too. Repeat value cannot be greater than 8192 now, or rendering will be ignored.

I have tested this with Samsung Galaxy S8+ SM-G955F, and I can confirm that it is now able to display the large (4500x4500) arrow image! It takes a few seconds to load, but I know that the delay is caused by inefficiency inherent to the sixel protocol, since Konsole also takes a long time to display the same sixel on the same device.

Just to stress test this, I have also tested this on Samsung Galaxy S III SPH-L710 (released 2012, 2GB RAM). Here are results of that test:

It has no problem displaying the small image, and loads it in under 1 second.

Screenshot_20260414-011007

However, attempting to display the large arrow image (4500x4500) results in the activity freezing for about 7 minutes,

Screenshot_20260414-005755

after that screen, it progresses if I just wait, but after the command completes, it creates a large space where the image should be displayed, without displaying the image itself:

Screenshot_20260414-010717 Screenshot_20260414-010751

logcat shows this:

Screenshot_20260414-010835

I think this is reasonable to expect. Since sixel is shown by preexisting implementation Konsole to be intensive and inefficient with large resolution images by nature, very old devices should not be expected to be capable of displaying very large sixel images.

@agnostic-apollo
Copy link
Copy Markdown
Member

Thanks for testing robert. Yeah, loading large images on older devices is not recommended and is bound to cause issues. Termux isn't responding errors are expected too since bitmaps are created and rendered on main UI thread, high end devices could have issues too with very large images if it takes too long.

@j4james
Copy link
Copy Markdown

j4james commented Apr 14, 2026

If you really wanted to, you could theoretically support large images, even on an older device, but you'd need to implement sixel in the same way as the original hardware terminals. These were devices from the 1980s, with a tiny amount of memory, but they could handle infinitely large images (albeit slowly). It's just that they treated sixel as a streaming protocol, rather than an image protocol, so the content was displayed as it was received, rather than trying to buffer the whole thing into memory, and only rendering it at the end.

When you receive a row of sixels that is 4500 pixels wide, and your display is only 800 pixels wide, you just render the first 800 columns and ignore the rest - you don't buffer all 4500 columns, most of which will never be seen. And as new sixel rows are added to the image at the bottom of the screen, you're going to have older parts of the image scrolling off the top. So you don't have to retain those older rows if you're low on memory, in the same way that you don't necessarily keep every row of text in the scrollback.

Not suggesting you have to implement it this way though. Just something to consider if you're keen to improve support for low-memory devices.

@agnostic-apollo
Copy link
Copy Markdown
Member

And as new sixel rows are added to the image at the bottom of the screen, you're going to have older parts of the image scrolling off the top. So you don't have to retain those older rows if you're low on memory, in the same way that you don't necessarily keep every row of text in the scrollback.

But modern terminals including termux have a scrollback history. If entire image is not stored, you cannot scroll back to older rows. Termux supports upto 50000 rows, and defaults to 2000 rows, which is large enough for many many sequential images, a 1920 height image uses only 45lines on a 2856 height display. Older devices also dont have a display resolution high enough that it would matter having to support 4K+ images. Users should just remain within their means.

@j4james
Copy link
Copy Markdown

j4james commented Apr 14, 2026

But modern terminals including termux have a scrollback history. If entire image is not stored, you cannot scroll back to older rows.

I'm not saying you should never store any of the image in the scrollback - just that you can choose to drop parts of an image that have scrolled out of view if you're low on memory. Having part of a large image missing from the scrollback is surely better than not seeing any image at all?

But feel free to ignore me. It was just an idea.

@agnostic-apollo
Copy link
Copy Markdown
Member

Its likely more easily doable to free bitmaps that are at start of scrollback history if an OOM occurs when reading a new bitmap to be added to end of scrollback history, instead of freeing parts of an image. Bitmaps that have been completely scrolled out of scrollback history are already removed, note that scrollback history is larger than the current terminal view of the screen.

Your idea is actually good, especially for older terminals, but is more likely to complicate things for termux's case. And your optimization wouldn't work for iTerm images anyways as entire base64 encoded image would need to be read before its converted to a bitmap. If sixels is an issue in future, one could create a new bitmap for each row of a sixel input, and bitmap lines scrolled out could be freed, and older bitmap lines at start of scrollback history could be freed too if an OOM occurs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support for CSI 14 Feature Request: sixel graphics mode