Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

indexcard

Note: This one is mostly Claude-code vibecoded...

A small Java library for generating text-oriented "index card" images from HTML and CSS. You describe a card as a snippet of HTML plus a stylesheet, and indexcard renders it to an in-memory java.awt.image.BufferedImage that you can serve, store, or post-process however you like.

Rendering is done with the pure-Java Flying Saucer engine, so there are no native dependencies and no external browser — but note that means the supported styling is CSS 2.1 (block/inline layout, float, tables, borders, backgrounds, fonts), not modern CSS3 (no flexbox or grid).

Requirements

  • Java 21 or later

Installation

Artifact coordinates are com.mchange:indexcard.

Maven:

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>indexcard</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

Gradle:

implementation 'com.mchange:indexcard:0.0.1-SNAPSHOT'

Mill:

mvn"com.mchange:indexcard:0.0.1-SNAPSHOT"

Quick start

import com.mchange.indexcard.Generator;

import java.awt.image.BufferedImage;
import java.nio.file.Path;

public class Demo {
    public static void main(String[] args) throws Exception {
        String html =
            "<h1>Index Card</h1>" +
            "<p>Hello, <b>world</b>. A bit of text and a rounded border.</p>";

        String css =
            "#indexcard {" +
            "  width: 320px; height: 190px;" +
            "  background: #fffbe6;" +
            "  border: 2px solid #333; border-radius: 12px;" +
            "  padding: 14px; box-sizing: border-box;" +
            "  font-family: sans-serif;" +
            "}" +
            "h1 { font-size: 20px; margin: 0 0 8px 0; }" +
            "p  { font-size: 14px; margin: 0; line-height: 1.4; }";

        BufferedImage card = Generator.generate(html, css);
        Generator.writePng(card, Path.of("card.png"));
    }
}

Your HTML becomes the contents of a <div id="indexcard"> inside the body of a dummy document, and your CSS becomes that document's stylesheet — so styling the #indexcard element (as above) styles the card as a whole. The innerHtml need not be well-formed XML; it is normalized before rendering.

API

All methods are static, on com.mchange.indexcard.Generator.

Method Purpose
BufferedImage generate(String innerHtml, String styleCss) Render a card to an image at native (1×) resolution.
BufferedImage generate(String innerHtml, String styleCss, double scale) Render a card supersampled by scale for crisper output (see Resolution).
byte[] toPng(BufferedImage img) Encode an image as PNG bytes (e.g. to serve over HTTP).
void writePng(BufferedImage img, OutputStream out) Write an image as PNG to a stream (the stream is not closed).
void writePng(BufferedImage img, Path path) Write an image as PNG to a file.
BufferedImage centerImageOnTransparentCanvas(BufferedImage original, int width, int height) Place an image centered on a fixed-size transparent canvas (see Sizing).

The returned image always uses an alpha channel (TYPE_INT_ARGB), so anything your CSS leaves unpainted — outside rounded corners, or with no background — is transparent. Text is rendered with antialiasing.

Serving a card over HTTP, for example, is just:

byte[] png = Generator.toPng(Generator.generate(html, css));
response.setContentType("image/png");
response.getOutputStream().write(png);

Sizing

You control the dimensions of the image entirely through CSS — typically by setting width and height on #indexcard. The image is laid out in a wide viewport and then cropped to the painted (non-transparent) pixels.

For a card with a background and/or a border, the whole declared box is painted, so the image comes out at exactly the width × height you asked for.

There is one caveat: a card with neither a background nor a border — just text floating on a transparent canvas — has only its glyphs painted, so the image is cropped tight to the bounding box of the text. Any declared width/height and surrounding padding is trimmed away as transparent margin. If you want a fixed-size image in that case, render normally and then center the result on a canvas of the size you intended:

BufferedImage tight = Generator.generate(html, css);          // cropped to text
BufferedImage fixed = Generator.centerImageOnTransparentCanvas(tight, 320, 190);

Resolution

CSS dimensions are interpreted in CSS pixels, so a card sized width: 320px in CSS produces a 320-pixel-wide image at native resolution. For a small or narrow card that can look low-resolution, especially on high-DPI screens.

To get crisper output, keep the CSS sizes small and pass a scale factor: layout still happens in CSS pixels (your 320px stays logically 320px wide), but the image is rasterized onto a buffer scale times larger in each dimension, with text and edges re-rendered at that higher resolution.

// A 320x190 CSS card rendered onto a 960x570 image (3x), same layout, crisper.
BufferedImage hi = Generator.generate(html, css, 3.0);

This is preferable to simply enlarging everything in CSS, which would also change wrapping and layout. scale only affects the pixel resolution of the result.

CSS quirks

Because rendering uses Flying Saucer, styling is limited to CSS 2.1, and there are a few engine-specific behaviors worth knowing.

text-align: right (or center) ignores padding under box-sizing: border-box

If an element has box-sizing: border-box and its text is right- or center-aligned, Flying Saucer aligns the text to the border edge rather than the content edge — so the element's padding on that side is ignored, and the text sits flush against the border. Left-aligned text is unaffected.

The fix is to not use box-sizing: border-box on the affected element (the default content-box aligns correctly):

/* Problem: the right-aligned credits end up flush against the border. */
* { box-sizing: border-box; }
#indexcard { width: 380px; border: 2px solid black; padding: 5px; }
#indexcard .credits { text-align: right; }

/* Fix: drop border-box; right-aligned text now respects the padding. */
#indexcard { width: 366px; border: 2px solid black; padding: 5px; }
#indexcard .credits { text-align: right; }

Note that with the default content-box, width is the content width, so the card's outer size is width + 2×padding + 2×border. To hit a specific outer width W, set width: (W − 2×border − 2×padding).

text-align: right is unreliable

Even without box-sizing: border-box, Flying Saucer does not position text-align: right (or center) text reliably. Right-aligned lines may fail to share a common right edge, may fall short of or collide with the container's border, and may ignore its right padding — and the effect is inconsistent within a single block (some lines behave, others don't). It is especially pronounced in blocks containing long, URL-like tokens, but the misalignment is present more generally.

Worse, right-alignment can also prevent long content from wrapping: Flying Saucer mis-measures the available width in right-align mode, keeps a long URL on a single line, and lets it overflow the border — where the same URL wraps cleanly within the card under text-align: left.

There is no reliable CSS workaround to make right-alignment come out clean. Things that seem like they should help do not:

  • Reducing the font size does not help — greedy wrapping still produces edge-filling lines.
  • Adding padding-right (or margin-right) to the element only changes where text wraps; the lines are still aligned inconsistently.
  • Splitting each line into its own element does not make the right edges line up.

If you need clean, predictable output, use text-align: left. Left-alignment positions reliably: consistent left edges, padding respected on both sides. If you want the text to sit toward the right of the card, put it in a narrower block positioned on the right (e.g. via margin-left) but keep text-align: left inside that block.

Long unbreakable tokens (e.g. URLs) can overflow

Flying Saucer only breaks long strings at certain characters (such as / and .) and does not support the CSS3 wrapping controls (overflow-wrap, word-wrap, word-break), nor <wbr> or zero-width spaces. A token with no break opportunities that is wider than its content box will overflow rather than wrap. If you display arbitrary URLs, prefer to shorten or elide them (e.g. strip the https:// scheme, or truncate the middle) rather than relying on wrapping.

Building from source

The project builds with Mill via the bundled bootstrap script (./mill), so you don't need Mill installed:

./mill compile     # compile the library
./mill test        # run the test suite
./mill publishLocal # install to your local repository

License

Apache License 2.0.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages