import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import javax.imageio.ImageIO; /** * Derive the manual's figures: scale/crop real screenshots, and build an annotated * schematic of the live screen. Kept as a tool so the figures can be regenerated * after a UI change instead of being stale hand-edited PNGs. * * Usage: MakeFigures */ public class MakeFigures { static File outDir; public static void main(String[] args) throws Exception { outDir = new File(args[0]); File imgs = new File(args[1]); outDir.mkdirs(); // --- real screenshots: scale to a readable width and crop the chrome-free part // cropped to the rows: the full-screen shots are mostly empty on a 9:20 display scale(new File(imgs, "01_settings_raw.jpg"), "05_settings.png", 780, 0, 60, 1440, 1720); scale(new File(imgs, "05_analysis_raw.jpg"), "06_analysis.png", 700, 0, 60, 1440, 3040); scale(new File(imgs, "06_gallery_raw.jpg"), "07_gallery.png", 780, 0, 60, 1440, 1260); // --- reference photos (already the real saved files) scale(new File(imgs, "MAG160C_20260912_145010.jpg"), "03_photo_landscape.png", 900, 0, 0, 0, 0); scale(new File(imgs, "MAG160C_20260912_141530.jpg"), "04_photo_portrait.png", 560, 0, 0, 0, 0); // --- marker anatomy: zoom on the MAX reticle of the landscape photo BufferedImage lp = ImageIO.read(new File(imgs, "MAG160C_20260912_141530.jpg")); markerAnatomy(lp); // --- live screen schematic (the real screen cannot be screenshotted for the // manual: its OSD text is pre-rotated to the current grip, so a landscape-held // phone yields sideways labels in the picture) liveSchematic(); } // ---------------------------------------------------------------- helpers static BufferedImage scaled(BufferedImage s, int w) { int h = Math.max(1, (int) Math.round(s.getHeight() * (w / (double) s.getWidth()))); BufferedImage o = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); Graphics2D g = o.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(s, 0, 0, w, h, null); g.dispose(); return o; } static void scale(File src, String out, int w, int x, int y, int cw, int ch) throws Exception { BufferedImage s = ImageIO.read(src); if (cw > 0) s = s.getSubimage(x, y, cw, ch); BufferedImage o = scaled(s, w); ImageIO.write(o, "png", new File(outDir, out)); System.out.println("wrote " + out + " " + o.getWidth() + "x" + o.getHeight()); } static Font font(int size, int style) { // a CJK-capable family; falls back to the platform default if absent return new Font("Microsoft YaHei", style, size); } /** Zoomed crop around the MAX reticle, with labelled callouts. */ static void markerAnatomy(BufferedImage photo) { // Pt1 on the portrait reference photo (720x960): the reticle sits at // (249,147) and the label to its right, so this window holds the whole glyph // plus its readout. int cx = 300, cy = 150; int cw = 420, ch = 260; int x = Math.max(0, cx - cw / 2), y = Math.max(0, cy - ch / 2); BufferedImage crop = photo.getSubimage(x, Math.min(y, photo.getHeight() - ch), cw, ch); int zoom = 3; BufferedImage big = new BufferedImage(cw * zoom, ch * zoom, BufferedImage.TYPE_INT_RGB); Graphics2D g = big.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); g.drawImage(crop, 0, 0, cw * zoom, ch * zoom, null); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setFont(font(26, Font.BOLD)); // callouts: the reticle, the label, the leader int rx = (cx - x) * zoom, ry = (cy - y) * zoom; callout(g, "① 方形准星", rx - 150, ry - 120, rx, ry, new Color(0x4FC3F7)); callout(g, "② MAX/MIN 读数", rx + 150, ry + 120, rx + 210, ry, new Color(0x81C784)); g.setColor(new Color(0xFFD54F)); g.setFont(font(24, Font.BOLD)); g.drawString("③ 引线(读数连到自己的准星)", 24, ch * zoom - 60); g.drawString("④ 白字 + 黑描边(无底板)", 24, ch * zoom - 24); g.dispose(); try { ImageIO.write(big, "png", new File(outDir, "02_markers.png")); System.out.println("wrote 02_markers.png " + big.getWidth() + "x" + big.getHeight()); } catch (Exception e) { e.printStackTrace(); } } static void callout(Graphics2D g, String text, int lx, int ly, int tx, int ty, Color c) { g.setColor(c); g.setStroke(new BasicStroke(3f)); g.drawLine(lx, ly, tx, ty); g.fillOval(tx - 7, ty - 7, 14, 14); FontMetrics fm = g.getFontMetrics(); int w = fm.stringWidth(text); int bx = lx < tx ? lx - w - 12 : lx + 12; g.setColor(new Color(0, 0, 0, 190)); g.fillRoundRect(bx - 6, ly - fm.getAscent() - 4, w + 12, fm.getHeight(), 8, 8); g.setColor(c); g.drawString(text, bx, ly); } /** * The live screen as a labelled diagram. Positions mirror the real layout * (top control bar glued to the portrait top edge, image below it, capture row, * bottom navigation). */ static void liveSchematic() throws Exception { int W = 720, H = 1280; BufferedImage im = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); Graphics2D g = im.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setColor(new Color(0x101014)); g.fillRect(0, 0, W, H); // top bar int barH = 96; g.setColor(new Color(0x5A5A50)); g.fillRect(0, 24, W, barH); String[] top = {"FFC", "1×", "追踪", "铁虹", "画中画"}; for (int i = 0; i < top.length; i++) { int cx = 78 + i * 142; g.setColor(new Color(0xD8D8C8)); g.setStroke(new BasicStroke(4f)); g.drawOval(cx - 16, 24 + barH / 2 - 26, 32, 32); g.setFont(font(22, Font.PLAIN)); FontMetrics fm = g.getFontMetrics(); g.drawString(top[i], cx - fm.stringWidth(top[i]) / 2, 24 + barH - 8); } // image area with a marker, mimicking the real look int imgX = 8, imgY = 24 + barH + 16, imgW = W - 16, imgH = 830; GradientPaint gp = new GradientPaint( imgX, imgY, new Color(0x1A0733), imgX + imgW, imgY + imgH, new Color(0xB25A00)); g.setPaint(gp); g.fillRect(imgX, imgY, imgW, imgH); g.setColor(new Color(0x5E1580)); g.fillOval(imgX + 40, imgY + 250, 380, 320); g.setColor(new Color(0xFFE066)); g.fillOval(imgX + 380, imgY + 520, 260, 240); // a probe marker drawn the way the app draws it int mx = imgX + 420, my = imgY + 400; g.setColor(Color.WHITE); g.setStroke(new BasicStroke(3f)); g.drawRect(mx - 12, my - 12, 24, 24); g.drawLine(mx - 30, my, mx - 12, my); g.drawLine(mx + 12, my, mx + 30, my); g.drawLine(mx, my - 30, mx, my - 12); g.drawLine(mx, my + 12, mx, my + 30); g.drawLine(mx + 30, my, mx + 62, my); g.setFont(font(30, Font.BOLD)); drawOutlined(g, "Pt1 25.0℃", mx + 62, my + 10); // a max marker int ax = imgX + 90, ay = imgY + 170; g.setStroke(new BasicStroke(3f)); g.setColor(Color.WHITE); g.drawRect(ax - 12, ay - 12, 24, 24); g.drawLine(ax + 12, ay, ax + 30, ay); g.drawLine(ax, ay - 30, ax, ay - 12); g.drawLine(ax, ay + 12, ax, ay + 30); g.drawLine(ax - 30, ay, ax - 12, ay); g.drawLine(ax + 30, ay, ax + 62, ay); g.setFont(font(30, Font.BOLD)); drawOutlined(g, "MAX 41.5℃", ax + 62, ay + 10); // centre readout, upper-left of the image g.setFont(font(30, Font.BOLD)); drawOutlined(g, "中心 26.4℃", imgX + 20, imgY + 40); // colour bar along the right edge int barX = imgX + imgW - 34, barW = 22; for (int i = 0; i < 120; i++) { float t = i / 119f; g.setColor(new Color( (int) (255 * Math.min(1, t * 1.6)), (int) (255 * Math.max(0, t * 1.6 - 0.6)), (int) (255 * Math.max(0, 1 - t * 2.2)))); g.fillRect(barX, imgY + 40 + i * 6, barW, 7); } // capture row int rowY = imgY + imgH + 30; g.setColor(new Color(0x1B1B20)); g.fillRect(0, rowY - 10, W, 210); circle(g, 200, rowY + 60, 44, new Color(0x555555), "相册"); circle(g, 360, rowY + 60, 62, Color.WHITE, "拍照"); circle(g, 520, rowY + 60, 44, new Color(0xE53935), "录像"); // bottom navigation int navY = H - 118; g.setColor(new Color(0x2A2A22)); g.fillRect(0, navY, W, 118); String[] nav = {"实时", "相册", "分析", "设置"}; for (int i = 0; i < nav.length; i++) { int cx = 90 + i * 180; g.setColor(i == 0 ? new Color(0xD8C88A) : new Color(0x9A9A9A)); g.setStroke(new BasicStroke(4f)); g.drawOval(cx - 16, navY + 22, 32, 32); g.setFont(font(24, Font.PLAIN)); FontMetrics fm = g.getFontMetrics(); g.drawString(nav[i], cx - fm.stringWidth(nav[i]) / 2, navY + 92); } // numbered callouts g.setFont(font(26, Font.BOLD)); badge(g, 1, 100, 40); badge(g, 2, 242, 40); badge(g, 3, 384, 40); badge(g, 4, 526, 40); badge(g, 5, 420, rowY + 20); badge(g, 6, 130, navY + 24); g.dispose(); ImageIO.write(im, "png", new File(outDir, "01_live_schematic.png")); System.out.println("wrote 01_live_schematic.png " + W + "x" + H); } static final String[] CIRCLED = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"}; static void badge(Graphics2D g, int n, int x, int y) { String s = CIRCLED[n - 1]; g.setColor(new Color(0xE53935)); g.fillOval(x - 20, y - 20, 40, 40); g.setColor(Color.WHITE); FontMetrics fm = g.getFontMetrics(); g.drawString(s, x - fm.stringWidth(s) / 2, y + 10); } static void circle(Graphics2D g, int cx, int cy, int r, Color c, String label) { g.setColor(c); g.fillOval(cx - r, cy - r, r * 2, r * 2); if (c == Color.WHITE) { g.setColor(new Color(0x222222)); g.setStroke(new BasicStroke(3f)); g.drawOval(cx - r, cy - r, r * 2, r * 2); } g.setColor(Color.WHITE); g.setFont(font(24, Font.PLAIN)); FontMetrics fm = g.getFontMetrics(); g.drawString(label, cx - fm.stringWidth(label) / 2, cy + r + 34); } /** White text with a dark outline — the same trick the app's markers use. */ static void drawOutlined(Graphics2D g, String s, int x, int y) { Shape shape = g.getFont().createGlyphVector(g.getFontRenderContext(), s).getOutline(x, y); g.setColor(new Color(0, 0, 0, 220)); g.setStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.draw(shape); g.setColor(Color.WHITE); g.fill(shape); } }