Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Two variants are available:
And then we have utility modules (the "built on top"):
- **[`ansiparser`](ansiparser/README.md)** — compact ANSI escape sequence parser
- **[`colors`](colors/README.md)** — terminal colour palette querying and setting
- **[`image`](image/README.md)** — terminal image rendering and protocol detection
- **[`mousetrack`](mousetrack/README.md)** — terminal mouse-tracking helpers and event parser
- **[`termcap`](termcap/README.md)** — terminal capability detection

Expand Down Expand Up @@ -108,13 +109,14 @@ if (bg != null) {

## Modules

Three artifacts are published independently:
Several artifacts are published independently:

| Artifact | Description |
|----------|-------------|
| [`miniterm`](miniterm/README.md) | Legacy terminal implementation, Java 8+ |
| [`miniterm-ffm`](miniterm-ffm/README.md) | Modern FFM-based terminal implementation, Java 22+ |
| [`ansiparser`](ansiparser/README.md) | Compact ANSI escape sequence parser, Java 8+ |
| [`image`](image/README.md) | Terminal image rendering and protocol detection, Java 8+ |
| [`mousetrack`](mousetrack/README.md) | Terminal mouse-tracking helpers and event parser, Java 8+ |
| [`termcap`](termcap/README.md) | Terminal capability detection, Java 8+ |
| [`colors`](colors/README.md) | Terminal colour palette querying and setting via OSC sequences, Java 8+ |
Expand Down
192 changes: 192 additions & 0 deletions examples/ShowImage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.codejive.miniterm:miniterm${miniterm.ffm:}:${miniterm.version:0.1.5}
//DEPS org.codejive.miniterm:image:${miniterm.version:0.1.5}

package examples;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import javax.imageio.ImageIO;
import org.codejive.miniterm.Terminal;
import org.codejive.miniterm.image.ImageEncoder;
import org.codejive.miniterm.image.ImageEncoders;

/**
* Demo application showing how to use the terminal image encoding framework.
*
* <p>This example demonstrates rendering images to the terminal using different encoders (Sixel,
* Kitty, iTerm2, and block-based Unicode rendering).
*
* <p>Usage: {@code ShowImage [--image=<path>] [--encoder=<name>] [--all]}
*
* <p>Supported encoder names: sixel, kitty, iterm2, block-full, block-half, block-quadrant,
* block-sextant, block-octant.
*/
public class ShowImage {

public static void main(String[] args) throws Exception {
try (Terminal terminal = Terminal.create()) {
BufferedImage image = loadImage(args);

// Define target size in terminal rows/columns
int targetWidth = 20; // 20 columns wide
int targetHeight = 10; // 10 rows tall

terminal.write("=== Image Encoder Demo ===\n");

boolean fitImage = true;

String encoderName = getEncoderArg(args);

if (encoderName != null) {
// Use a specific encoder requested via --encoder=
ImageEncoder.Provider provider = findProvider(encoderName);
if (provider == null) {
terminal.write("Unknown encoder: " + encoderName + "\n");
terminal.write(
"Available: sixel, kitty, iterm2, block-full, block-half,"
+ " block-quadrant, block-sextant, block-octant\n");
return;
}
terminal.write("Using encoder: " + provider.name() + "\n\n");
terminal.write("Rendering with " + provider.name() + " encoder:\n");
renderImage(provider.create(image, targetWidth, targetHeight, fitImage), terminal);
terminal.write("\n\n");
} else {
// Detect the best encoder for the current terminal
ImageEncoder.Provider bestProvider = ImageEncoders.best();
ImageEncoder detectedEncoder =
bestProvider.create(image, targetWidth, targetHeight, fitImage);
terminal.write("Detected encoder: " + bestProvider.name() + "\n\n");

// Try rendering with the detected encoder
terminal.write("Rendering with " + bestProvider.name() + " encoder:\n");
renderImage(detectedEncoder, terminal);
terminal.write("\n\n");

// Optionally try all available encoders
if (shouldTestAllEncoders(args)) {
terminal.write("\n--- Testing all encoders ---\n\n");

for (ImageEncoder.Provider provider : ImageEncoders.providers()) {
testEncoder(
provider.name(),
provider.create(image, targetWidth, targetHeight, fitImage),
terminal);
}
}
}

terminal.write("\nDemo complete!\n");
}
}

private static String getEncoderArg(String[] args) {
for (String arg : args) {
if (arg.startsWith("--encoder=")) {
return arg.substring("--encoder=".length());
}
}
return null;
}

private static BufferedImage loadImage(String[] args) throws IOException {
String imagePath = getImageArg(args);
if (imagePath == null) {
return createTestImage(200, 150);
}

BufferedImage image = ImageIO.read(new File(imagePath));
if (image == null) {
throw new IOException("Unsupported or unreadable image: " + imagePath);
}
return image;
}

private static String getImageArg(String[] args) {
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith("--image=")) {
return arg.substring("--image=".length());
}
if ("--image".equals(arg)) {
if (i + 1 >= args.length) {
throw new IllegalArgumentException("Missing value for --image");
}
return args[i + 1];
}
}
return null;
}

private static ImageEncoder.Provider findProvider(String name) {
String normalized = normalizeProviderName(name);
List<ImageEncoder.Provider> all = ImageEncoders.providers();
for (ImageEncoder.Provider provider : all) {
String providerKey = normalizeProviderName(provider.name());
if (providerKey.equals(normalized)) {
return provider;
}
}
return null;
}

private static String normalizeProviderName(String value) {
return value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "");
}

private static void testEncoder(String name, ImageEncoder encoder, Appendable output)
throws IOException {
output.append(name).append(" encoder:\n");
renderImage(encoder, output);
output.append("\n\n");
}

private static void renderImage(ImageEncoder encoder, Appendable output) throws IOException {
encoder.render(output);
}

/**
* Creates a simple test image with a gradient and some shapes.
*
* @param width the image width
* @param height the image height
* @return the created test image
*/
private static BufferedImage createTestImage(int width, int height) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();

// Draw gradient background
for (int y = 0; y < height; y++) {
float hue = (float) y / height;
Color color = Color.getHSBColor(hue, 0.8f, 0.9f);
g.setColor(color);
g.fillRect(0, y, width, 1);
}

// Draw some shapes
g.setColor(Color.WHITE);
g.fillOval(width / 4, height / 4, width / 2, height / 2);

g.setColor(Color.BLACK);
g.drawString("Test Image", width / 3, height / 2);

g.dispose();
return image;
}

private static boolean shouldTestAllEncoders(String[] args) {
for (String arg : args) {
if ("--all".equals(arg) || "-a".equals(arg)) {
return true;
}
}
return false;
}
}
Binary file added examples/duke.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 102 additions & 0 deletions image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# image

`image` is a Java 8+ terminal image rendering utility, part of the [java-miniterm](../README.md) project.

It can render `BufferedImage` objects to terminal graphics protocols such as Kitty, iTerm2, and Sixel, with a Unicode block fallback for terminals that do not support a native graphics protocol. The module automatically detects the best protocol for the current terminal and exposes a small, configurable encoder API.

## Usage

The simplest entry point is `ImageEncoders.best()`, which picks the highest-priority provider currently supported by the environment:

```java
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import org.codejive.miniterm.Terminal;
import org.codejive.miniterm.image.ImageEncoder;
import org.codejive.miniterm.image.ImageEncoders;

BufferedImage image = ImageIO.read(new java.io.File("photo.png"));
ImageEncoder encoder = ImageEncoders.best().create(image, 40, 12, false);

try (Terminal terminal = Terminal.create()) {
encoder.render(terminal);
}
```

`ImageEncoder` is stateful: the image and initial target size are fixed when the encoder is created, while the output size and fit mode can be adjusted afterwards.

```java
encoder.targetSize(60, 20).fitImage(true);
encoder.render(terminal);
```

### Detect supported providers

```java
for (ImageEncoder.Provider provider : ImageEncoders.supportedProviders()) {
System.out.println(provider.name() + " -> " + provider.resolution());
}
```

This returns providers in priority order, with the best option first. `supportedProviders()` checks common terminal environment variables and chooses the most appropriate protocol for the current session.

## Supported protocols

| Protocol | Typical terminals | Notes |
|---|---|---|
| `Kitty` | Kitty, Ghostty, Konsole, WezTerm | Modern, efficient PNG graphics protocol |
| `iTerm2` | iTerm2, WezTerm, VS Code, Mintty | Inline images using OSC 1337 |
| `Sixel` | Konsole, Windows Terminal, mlterm, foot, others | Bitmap graphics via DCS/Sixel |
| `Block` | Any terminal with Unicode support | Fallback renderer using block characters |

The block encoders are exposed as several variants (`FULL`, `HALF`, `QUADRANT`, `SEXTANT`, `OCTANT`) to trade fidelity for compatibility.

## Encoding model

Each provider implements the same `ImageEncoder` API:

- `targetWidth()` / `targetHeight()` — target dimensions in terminal columns and rows
- `targetSize(int width, int height)` — resize the rendered output
- `fitImage()` / `fitImage(boolean)` — preserve aspect ratio or stretch to fill the target box
- `render(Appendable output)` — emit terminal escape sequences

The rendering work is cached. When you change the target size or fit mode, the encoder invalidates its cached transformation and re-renders lazily on the next call.

## Adding the dependency

`image` emits ANSI escape sequences and therefore requires `ansiparser` on the classpath at runtime. The module depends on it as an optional library in Maven so you can keep the dependency explicit in your own build.

### JBang

```java
//DEPS org.codejive.miniterm:image:0.1.5
//DEPS org.codejive.miniterm:ansiparser:0.1.5
```

### Maven

```xml
<dependency>
<groupId>org.codejive.miniterm</groupId>
<artifactId>image</artifactId>
<version>0.1.5</version>
</dependency>
<dependency>
<groupId>org.codejive.miniterm</groupId>
<artifactId>ansiparser</artifactId>
<version>0.1.5</version>
</dependency>
```

### Gradle

```kotlin
implementation("org.codejive.miniterm:image:0.1.5")
implementation("org.codejive.miniterm:ansiparser:0.1.5")
```

## Building

```bash
./mvnw clean install
```
Loading