Files
spring-ai/multimodal/src/main/java/com/ankurm/multimodal/ReceiptImages.java
T

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);
}
}
}