Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JXVi2GMQ7bR5EmbUFdDj7N
110 lines
4.3 KiB
Java
110 lines
4.3 KiB
Java
package com.ankurm.multimodal;
|
|
|
|
import java.awt.Color;
|
|
import java.awt.Font;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.RenderingHints;
|
|
import java.awt.geom.AffineTransform;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
import javax.imageio.ImageIO;
|
|
|
|
/**
|
|
* Draws a receipt as a PNG so the whole pipeline has a real image to chew on. Deterministic: the
|
|
* same receipt and the same {@link Look} always produce the same bytes (the noise uses a seeded
|
|
* {@link Random}).
|
|
*/
|
|
public final class ReceiptImages {
|
|
|
|
/** How the "photo" was taken. {@code scale} shrinks it, {@code noise} speckles it, {@code degrees} tilts it. */
|
|
public record Look(String name, double scale, double noise, double degrees) {
|
|
|
|
public static final Look CLEAN = new Look("clean", 1.0, 0.0, 0.0);
|
|
public static final Look SMALL = new Look("small (1/3 size)", 0.33, 0.0, 0.0);
|
|
public static final Look NOISY = new Look("noisy", 1.0, 0.25, 0.0);
|
|
public static final Look TILTED = new Look("tilted 4 degrees", 1.0, 0.0, 4.0);
|
|
}
|
|
|
|
private ReceiptImages() {
|
|
}
|
|
|
|
public static List<String> text(Receipt r) {
|
|
List<String> t = new ArrayList<>();
|
|
t.add(r.merchant());
|
|
t.add(r.date().toString());
|
|
t.add("CURRENCY: " + r.currency());
|
|
t.add("--------------------------------");
|
|
for (Receipt.Line l : r.items()) {
|
|
t.add(String.format("%-14s %d x %s %s", l.description(), l.quantity(), l.unitPrice().toPlainString(),
|
|
l.lineTotal().toPlainString()));
|
|
}
|
|
t.add("--------------------------------");
|
|
t.add("SUBTOTAL " + r.subtotal().toPlainString());
|
|
t.add("TAX " + r.tax().toPlainString());
|
|
t.add("TOTAL " + r.total().toPlainString());
|
|
return t;
|
|
}
|
|
|
|
public static byte[] png(Receipt r, Look look) {
|
|
List<String> lines = text(r);
|
|
int base = 30;
|
|
int w = 640;
|
|
int h = 60 + lines.size() * (base + 12);
|
|
BufferedImage page = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D g = page.createGraphics();
|
|
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
g.setColor(Color.WHITE);
|
|
g.fillRect(0, 0, w, h);
|
|
g.setColor(Color.BLACK);
|
|
g.setFont(new Font(Font.MONOSPACED, Font.PLAIN, base));
|
|
int y = 50;
|
|
for (String s : lines) {
|
|
g.drawString(s, 20, y);
|
|
y += base + 12;
|
|
}
|
|
g.dispose();
|
|
BufferedImage out = page;
|
|
if (look.degrees() != 0) {
|
|
BufferedImage rot = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D rg = rot.createGraphics();
|
|
rg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
rg.setColor(Color.WHITE);
|
|
rg.fillRect(0, 0, w, h);
|
|
rg.transform(AffineTransform.getRotateInstance(Math.toRadians(look.degrees()), w / 2.0, h / 2.0));
|
|
rg.drawImage(page, 0, 0, null);
|
|
rg.dispose();
|
|
out = rot;
|
|
}
|
|
if (look.scale() != 1.0) {
|
|
int nw = (int) Math.round(w * look.scale());
|
|
int nh = (int) Math.round(h * look.scale());
|
|
BufferedImage small = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D sg = small.createGraphics();
|
|
sg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
sg.drawImage(out, 0, 0, nw, nh, null);
|
|
sg.dispose();
|
|
out = small;
|
|
}
|
|
if (look.noise() > 0) {
|
|
Random rnd = new Random(42);
|
|
for (int x = 0; x < out.getWidth(); x++) {
|
|
for (int yy = 0; yy < out.getHeight(); yy++) {
|
|
if (rnd.nextDouble() < look.noise()) {
|
|
out.setRGB(x, yy, rnd.nextBoolean() ? 0x000000 : 0xFFFFFF);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
|
ImageIO.write(out, "png", bos);
|
|
return bos.toByteArray();
|
|
} catch (IOException e) {
|
|
throw new IllegalStateException(e);
|
|
}
|
|
}
|
|
}
|