+package eu.svjatoslav.crtbasic.drivers;
+
+import eu.svjatoslav.crtbasic.video.VgaDevice;
+import eu.svjatoslav.crtbasic.video.VgaFont;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Dumps a graphics-mode screen as recognized text. Programs that render
+ * their own text with pixel primitives (truth.bas printText) bypass the
+ * text console, so a PNG screenshot is the only output — this reverses
+ * each 8x8 (or 8xN) cell back to a character by matching its pixel
+ * bitmap against the built-in CP437 VGA font, giving tests a
+ * diffable, byte-stable rendering of the screen.
+ *
+ * <p>Per cell the most frequent palette index is taken as the
+ * background; the remaining pixels form the glyph bitmap. Bitmaps that
+ * match no font glyph (custom LINE-drawn symbols, e.g. truth.bas
+ * operator glyphs) are emitted as a stable private-use code point
+ * (U+E000..U+EFFF) derived from the bitmap hash, so dumps of identical
+ * screens stay byte-identical across runs while different bitmaps
+ * virtually never collide.
+ */
+public final class TextDump {
+
+ private TextDump() {
+ // Utility class.
+ }
+
+ /**
+ * Writes the screen contents as text rows of recognized cells.
+ *
+ * @param vga the video device to read pixels from
+ * @param output destination text file (UTF-8)
+ * @param cellHeight cell height in pixels (8 for programs that draw
+ * their own 8x8 text; 14/16 for console rows)
+ */
+ public static void dump(final VgaDevice vga, final Path output, final int cellHeight)
+ throws IOException {
+ final VgaFont font = VgaFont.forCellHeight(cellHeight);
+ final Map<Long, Character> byBitmap = glyphMap(font);
+ final int width = vga.mode().pixelWidth();
+ final int height = vga.mode().pixelHeight();
+ final StringBuilder text = new StringBuilder();
+ for (int cellY = 0; cellY + cellHeight <= height; cellY += cellHeight) {
+ for (int cellX = 0; cellX + 8 <= width; cellX += 8) {
+ text.append(recognize(vga, cellX, cellY, cellHeight, byBitmap));
+ }
+ text.append('\n');
+ }
+ Files.writeString(output, text.toString());
+ }
+
+ /** Recognizes one cell: background = most frequent color, rest = glyph. */
+ private static char recognize(final VgaDevice vga, final int cellX, final int cellY,
+ final int cellHeight, final Map<Long, Character> byBitmap) {
+ final int[] counts = new int[256];
+ for (int y = 0; y < cellHeight; y++) {
+ for (int x = 0; x < 8; x++) {
+ counts[vga.point(cellX + x, cellY + y) & 0xFF]++;
+ }
+ }
+ int background = 0;
+ for (int i = 1; i < counts.length; i++) {
+ if (counts[i] > counts[background]) {
+ background = i;
+ }
+ }
+ long bitmap = 0;
+ for (int y = 0; y < cellHeight; y++) {
+ for (int x = 0; x < 8; x++) {
+ if ((vga.point(cellX + x, cellY + y) & 0xFF) != background) {
+ bitmap |= 1L << (y * 8 + x);
+ }
+ }
+ }
+ if (bitmap == 0) {
+ return ' ';
+ }
+ final Character glyph = byBitmap.get(bitmap);
+ if (glyph != null) {
+ return glyph;
+ }
+ // Unknown (custom-drawn) glyph: stable private-use code point.
+ return (char) (0xE000 + (Long.hashCode(bitmap) & 0x0FFF));
+ }
+
+ /**
+ * Bitmap-to-character map of the whole font. Printable ASCII is
+ * registered first and wins bitmap collisions (some extended glyphs
+ * duplicate ASCII ones).
+ */
+ private static Map<Long, Character> glyphMap(final VgaFont font) {
+ final Map<Long, Character> byBitmap = new HashMap<>();
+ for (int glyph = 32; glyph < 127; glyph++) {
+ byBitmap.putIfAbsent(bitmapOf(font, glyph), (char) glyph);
+ }
+ for (int glyph = 0; glyph < VgaFont.GLYPH_COUNT; glyph++) {
+ byBitmap.putIfAbsent(bitmapOf(font, glyph), (char) glyph);
+ }
+ return byBitmap;
+ }
+
+ private static long bitmapOf(final VgaFont font, final int glyph) {
+ long bitmap = 0;
+ for (int y = 0; y < font.glyphHeight(); y++) {
+ for (int x = 0; x < 8; x++) {
+ if (font.pixelSet(glyph, x, y)) {
+ bitmap |= 1L << (y * 8 + x);
+ }
+ }
+ }
+ return bitmap;
+ }
+}