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 text(Receipt r) { List 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 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); } } }