From c32521fd4b9082df65991e409e19de4e0c258fac Mon Sep 17 00:00:00 2001 From: Rudi Date: Fri, 21 Aug 2026 11:51:37 -0400 Subject: [PATCH] Cleaner rendering --- .gitignore | 1 + .../org/pubvm/j3270/ui/TerminalPanel.class | Bin 24926 -> 25301 bytes .../datastream/QueryReplyBuilder.class | Bin 8580 -> 8573 bytes .../org/pubvm/j3270/ui/TerminalPanel.java | 16 +- lib3270j/build.gradle | 6 + .../datastream/QueryReplyBuilder.java | 109 ++-- .../org/lib3270j/graphics/GocaConstants.java | 49 +- .../org/lib3270j/graphics/GocaDecoder.java | 34 +- .../org/lib3270j/graphics/GraphicsPlane.java | 490 +++++++++++------- .../graphics/ProgramSymbolManager.java | 17 - .../lib3270j/graphics/ProgramSymbolSet.java | 46 +- .../datastream/QueryReplyBuilderTest.java | 46 ++ .../graphics/ProgramSymbolManagerTest.java | 36 +- 13 files changed, 516 insertions(+), 334 deletions(-) diff --git a/.gitignore b/.gitignore index 1cbe887..86629c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +j3270.log.* *.log .DS_Store *.jar diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class index a1ae124cc1a98e676a9e79c8325d2c7f737b9aa4..97a146a4ec2a413e8100672eaa7f6122c3987c19 100644 GIT binary patch delta 5546 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 PyzIGzzJQPM3B3OWW@Zn> delta 2445 zcmZ8i4N#QV6+P!I>@LghvMb24zyfP9ge8iiSVTejQBgsS*49L0iqHsK^&m^O z-GNFc#$mi}SJ^RDPE6E2${#b?iE8b#q_)qn*e$L&Y(=vT+nm^r9eRUxcoX5BUA>^T zu{OVUc|(51;`+r)M>(()FWT^u6T7g0aDN*ntdG|gJ=hFNFt_i z7^w(H$wx$fJxYE<vk1j%_2+^K{vP>ZN_nP;nQEba%vXg87#pjG++moN!A+0_HuFV zNnFJWe1Mg>ji>MhR^cvI(}6V%;%Rh<1v_|$4jWSB8h^xD<1+0_+{fq^4-!NEST9Tu zbg?IxW_a7idEa<=4>z6Cjh-yG;eH!)BDmh|d4LuAk~w~5j+tI#rOF$Wae?`*y$?0X zMQD|B0G>r0o|6AR@UaE>#Nx!CEeZIG#gE&TT>RBih`(8ikoTpf5*sa(@Rel_ z{$VDi9Kkqyp9CS!29FJIk(3O{hjt|n;|X1sSTV>*e}?c1xFdYLi$r`abnQd7@HLmj zP%7c#0NiKt^FU&%#Qb97$Qj1IRM`Q>!Y*-77t;l_qe$jbp+<+`*@(GgoR?B=Lua2P z#cKK?E1mKe8} zq0g!Z`V#(M|3SSg`F4}|ugq6u9fA_AVMt=3csT+C`5p1J2tzm$BUmgE8-=kPEm14M zB$i?t$6`6lh3yr>@(I|_Dq-+M9OWc*a5B!a8W&lEw>bqjI2ApdhL1R%c7B&b_&xc3 zpR1VjFkAQt&v7OGHpR12Go4t^i|FDN`sEE%_`y9RqSF&_8;6gu77<*sGe%+9DuFTMO%he#gIUeblj9if} dm~LLgB@= 0x40 && client.getProgramSymbolManager() != null) { - drawnAsPs = client.getProgramSymbolManager().drawSymbol(g2, cs, ea.ec & 0xFF, x, y, cellWidth, cellHeight, fgColor, bgColor); + org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); + if (slot != null) { + int[] rgb = slot.getRgbPixels(fgColor.getRGB(), bgColor.getRGB()); + java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(slot.getWidth(), slot.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB); + img.setRGB(0, 0, slot.getWidth(), slot.getHeight(), rgb, 0, slot.getWidth()); + g2.drawImage(img, x, y, cellWidth, cellHeight, null); + drawnAsPs = true; + } } if (!drawnAsPs) { @@ -958,7 +965,12 @@ public class TerminalPanel extends JPanel { int gridW = cols * cellWidth; int gridH = rows * cellHeight; client.getGraphicsPlane().resize(gridW, gridH); - g2.drawImage(client.getGraphicsPlane().getImage(), ox, oy, gridW, gridH, null); + int[] rgb = client.getGraphicsPlane().getRgbBuffer(); + if (rgb != null) { + java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB); + img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW); + g2.drawImage(img, ox, oy, gridW, gridH, null); + } } // Draw cursor diff --git a/lib3270j/build.gradle b/lib3270j/build.gradle index 164eaac..d822073 100644 --- a/lib3270j/build.gradle +++ b/lib3270j/build.gradle @@ -3,6 +3,12 @@ plugins { } dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() } jar { diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java index 32b6f70..3cac7ab 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -103,26 +103,34 @@ public class QueryReplyBuilder { // Highlighting appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); - // Reply Modes + // Reply Modes (0x88) appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); - // Distributed Data Management (DFT File Transfer) + if (graphicsMode.isVectorGraphicsEnabled()) { + // Save/Restore (0x8C) + appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); + } + + // Distributed Data Management (0x95) appendQueryReply(out, QR_DDM, buildDdm(4096)); - // Implicit Partition + if (graphicsMode.isVectorGraphicsEnabled()) { + // Transparency (0x99) + appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); + } + + // Implicit Partition (0xA6) appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); // Vector Graphics QRs if enabled if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); - appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); - appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); - appendQueryReply(out, QR_GRAPHICS, buildGraphics()); - appendQueryReply(out, QR_GIMAGE, buildGImage()); - appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); - appendOemFmt(out); - appendQueryReply(out, QR_GCOLOR, buildGColor()); - appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); + appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8 + appendQueryReply(out, QR_GRAPHICS, buildGraphics()); // 0xB0 + appendQueryReply(out, QR_GIMAGE, buildGImage()); // 0xB1 + appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2 + appendOemFmt(out); // 0xB3 + appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4 + appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6 } log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")"); @@ -304,48 +312,48 @@ public class QueryReplyBuilder { } private byte[] buildCharsets() { - if (graphicsMode.isVectorGraphicsEnabled() || graphicsMode == GraphicsMode.NONE) { - // Standard 3179G / Base character sets (matches HOD QR_CHARSETS_S_STRING, 27 bytes total) - ByteArrayOutputStream out = new ByteArrayOutputStream(23); + if (graphicsMode.isProgrammedSymbolsEnabled()) { + // Programmed Symbols mode (3279 PS with LoadPS 0x0A) + ByteArrayOutputStream out = new ByteArrayOutputStream(65); out.write(0x82); // flags: GE, CGCSGID present out.write(0x00); // more flags - out.write(SW_3279_2); // SDW - default char width (9) - out.write(14); // SDH - default char height (14) - out.write(0x00); // LoadPS format (0x00) - out.write(0x00); - out.write(0x00); - out.write(0x00); - out.write(0x07); // DL = 7 - // Set 0 (Base EBCDIC) - out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4); - // Set 1 (APL/Text) + out.write(SW_3279_2); // SDW (9) + out.write(SH_3279_2); // SDH (12) + out.write(0x0a); // Load PS format types supported: Format 1 and Format 3 + out.write(0x00); // Load PS device type (high) + out.write(0x00); // Load PS device type (low) + out.write(0x00); // reserved + out.write(0x07); // DL = 7 bytes per descriptor + // Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037) + out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25); + // Descriptor 2 (SET 1): APL/GE character set out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); + // Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) + out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + // Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) + out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); return out.toByteArray(); } - // Programmed Symbols mode (3279 PS) - ByteArrayOutputStream out = new ByteArrayOutputStream(65); + // Standard 3179G / Base character sets (matches sf.c / HOD) + ByteArrayOutputStream out = new ByteArrayOutputStream(23); out.write(0x82); // flags: GE, CGCSGID present out.write(0x00); // more flags - out.write(SW_3279_2); // SDW (9) - out.write(14); // SDH (14) - out.write(0x0a); // Load PS format types supported: Format 1 and Format 3 - out.write(0x00); // Load PS device type (high) - out.write(0x00); // Load PS device type (low) - out.write(0x00); // reserved - out.write(0x07); // DL = 7 bytes per descriptor - // Descriptor 1 (SET 0): default character set - out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4); - // Descriptor 2 (SET 1): APL/GE character set + out.write(SW_3279_2); // SDW - default char width (9) + out.write(SH_3279_2); // SDH - default char height (12) + out.write(0x00); // LoadPS format (0x00) + out.write(0x00); + out.write(0x00); + out.write(0x00); + out.write(0x07); // DL = 7 + // Set 0 (Base EBCDIC - Non-loadable, single plane, CP037) + out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25); + // Set 1 (APL/Text) out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); - // Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - // Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); return out.toByteArray(); } @@ -473,10 +481,13 @@ public class QueryReplyBuilder { for (int i = 0; i < 16; i++) { out.write(0x00); out.write(i); - java.awt.Color c = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i]; - out.write(c.getRed()); - out.write(c.getGreen()); - out.write(c.getBlue()); + int argb = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i]; + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = argb & 0xFF; + out.write(r); + out.write(g); + out.write(b); out.write(0x00); // 6th byte in HOD color table } return out.toByteArray(); diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java index 2bcc082..2141736 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java @@ -150,24 +150,35 @@ public final class GocaConstants { public static final int MIX_XOR = 4; public static final int MIX_UNDER = 5; - // Graphic Colors (IBM 3179G / HOD 16-color table) - public static final java.awt.Color[] GOCA_COLORS = new java.awt.Color[] { - new java.awt.Color(0, 255, 0), // 0: Default (Green) - new java.awt.Color(120, 144, 240), // 1: Blue - new java.awt.Color(255, 0, 0), // 2: Red - new java.awt.Color(255, 0, 255), // 3: Pink / Magenta - new java.awt.Color(0, 255, 0), // 4: Green - new java.awt.Color(0, 255, 255), // 5: Turquoise / Cyan - new java.awt.Color(255, 255, 0), // 6: Yellow - new java.awt.Color(255, 255, 255), // 7: Neutral White - new java.awt.Color(0, 0, 0), // 8: Black - new java.awt.Color(0, 0, 128), // 9: Deep Blue - new java.awt.Color(128, 0, 0), // 10: Orange / Dark Red - new java.awt.Color(128, 0, 128), // 11: Purple - new java.awt.Color(0, 128, 0), // 12: Pale Green - new java.awt.Color(0, 128, 128), // 13: Pale Cyan - new java.awt.Color(215, 151, 0), // 14: Mustard - new java.awt.Color(192, 192, 192), // 15: Grey / Light White - new java.awt.Color(73, 36, 0) // 16: Brown + // Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB) + public static final int[] GOCA_COLORS = new int[] { + 0xFF00FF00, // 0: Default (Green) + 0xFF7890F0, // 1: Blue (120, 144, 240) + 0xFFFF0000, // 2: Red (255, 0, 0) + 0xFFFF00FF, // 3: Pink / Magenta (255, 0, 255) + 0xFF00FF00, // 4: Green (0, 255, 0) + 0xFF00FFFF, // 5: Turquoise / Cyan (0, 255, 255) + 0xFFFFFF00, // 6: Yellow (255, 255, 0) + 0xFFFFFFFF, // 7: Neutral White (255, 255, 255) + 0xFF000000, // 8: Black (0, 0, 0) + 0xFF000080, // 9: Deep Blue (0, 0, 128) + 0xFF800000, // 10: Orange / Dark Red (128, 0, 0) + 0xFF800080, // 11: Purple (128, 0, 128) + 0xFF008000, // 12: Pale Green (0, 128, 0) + 0xFF008080, // 13: Pale Cyan (0, 128, 128) + 0xFFD79700, // 14: Mustard (215, 151, 0) + 0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192) + 0xFF492400 // 16: Brown (73, 36, 0) }; + + /** + * Retrieves the 32-bit ARGB color value for a given GOCA color index (0-16). + * Returns Green (0xFF00FF00) if the index is out of range. + */ + public static int getGocaColorArgb(int colorIndex) { + if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) { + return GOCA_COLORS[colorIndex]; + } + return GOCA_COLORS[0]; + } } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java index 21751bf..72571be 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java @@ -1,10 +1,7 @@ package org.lib3270j.graphics; -import java.awt.Color; import java.util.ArrayList; import java.util.List; -import java.awt.geom.AffineTransform; -import java.awt.geom.Path2D; import java.util.logging.Logger; import org.lib3270j.charset.EbcdicTranslator; @@ -22,14 +19,14 @@ public class GocaDecoder { // Drawing state private int curX = 0; private int curY = 0; - private Color curColor = Color.GREEN; + private int curColor = GocaConstants.GOCA_COLORS[0]; private int lineType = GocaConstants.LT_SOLID; private int lineWidth = GocaConstants.LW_NORMAL; private int markerType = GocaConstants.MK_PLUS; private int markerSize = 5; - private Color markerColor = Color.GREEN; + private int markerColor = GocaConstants.GOCA_COLORS[0]; private int pattern = GocaConstants.PT_SOLID; - private Color fillColor = Color.GREEN; + private int fillColor = GocaConstants.GOCA_COLORS[0]; private int charDir = GocaConstants.CD_LR; private double charAngle = 0.0; private int charWidth = 9; @@ -781,8 +778,22 @@ public class GocaDecoder { int code = data[pos + i] & 0xFF; int px = plane.mapX(startX); int py = plane.mapY(startY) - ch; - boolean drawn = programSymbolManager.drawSymbol(plane.getGraphics(), charSet, code, px, py, cw, ch, curColor, null); - if (!drawn) { + ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code); + if (slot != null) { + int[] rgb = slot.getRgbPixels(curColor, 0); + int symW = slot.getWidth(); + int symH = slot.getHeight(); + for (int dy = 0; dy < ch; dy++) { + int sy = (dy * symH) / ch; + for (int dx = 0; dx < cw; dx++) { + int sx = (dx * symW) / cw; + int pixelArgb = rgb[sy * symW + sx]; + if ((pixelArgb >>> 24) != 0) { + plane.setPixel(px + dx, py + dy, pixelArgb); + } + } + } + } else { char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]); plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle); } @@ -810,10 +821,7 @@ public class GocaDecoder { return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF)); } - private Color getColor(int colorIndex) { - if (colorIndex >= 0 && colorIndex < GocaConstants.GOCA_COLORS.length) { - return GocaConstants.GOCA_COLORS[colorIndex]; - } - return Color.GREEN; + private int getColor(int colorIndex) { + return GocaConstants.getGocaColorArgb(colorIndex); } } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java index b752abd..2cb144f 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java @@ -1,20 +1,15 @@ package org.lib3270j.graphics; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Polygon; -import java.awt.RenderingHints; -import java.awt.Stroke; -import java.awt.geom.AffineTransform; -import java.awt.geom.Arc2D; -import java.awt.geom.GeneralPath; -import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.logging.Logger; /** * Offscreen rendering surface for GOCA vector graphics. - * Maintained as an ARGB BufferedImage that overlays the 3270 character cell matrix. + * Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix. + * Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap). */ public class GraphicsPlane { @@ -43,50 +38,45 @@ public class GraphicsPlane { private int canvasWidth = 800; private int canvasHeight = 600; - private BufferedImage image; - private Graphics2D g2d; + private int[] rgbBuffer; private boolean hasContent = false; + private int screenCols = 80; + private int screenRows = 24; + public GraphicsPlane(int width, int height) { - resize(width, height); + this.canvasWidth = Math.max(1, width); + this.canvasHeight = Math.max(1, height); + this.rgbBuffer = new int[canvasWidth * canvasHeight]; } public synchronized void resize(int width, int height) { int w = Math.max(1, width); int h = Math.max(1, height); - if (w == canvasWidth && h == canvasHeight && image != null) { + if (w == canvasWidth && h == canvasHeight && rgbBuffer != null) { return; } + + int[] newBuffer = new int[w * h]; + if (rgbBuffer != null && hasContent) { + // Scale existing content to new dimensions using nearest-neighbor + for (int dy = 0; dy < h; dy++) { + int sy = (dy * canvasHeight) / h; + for (int dx = 0; dx < w; dx++) { + int sx = (dx * canvasWidth) / w; + newBuffer[dy * w + dx] = rgbBuffer[sy * canvasWidth + sx]; + } + } + } + this.canvasWidth = w; this.canvasHeight = h; - - BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D newG2d = newImage.createGraphics(); - newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - newG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - - if (image != null && hasContent) { - newG2d.drawImage(image, 0, 0, w, h, null); - } - - if (this.g2d != null) { - this.g2d.dispose(); - } - - this.image = newImage; - this.g2d = newG2d; + this.rgbBuffer = newBuffer; } public synchronized void clear() { - if (image != null && g2d != null) { - BufferedImage newImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB); - Graphics2D newG2d = newImage.createGraphics(); - newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - if (this.g2d != null) { - this.g2d.dispose(); - } - this.image = newImage; - this.g2d = newG2d; + if (rgbBuffer != null) { + Arrays.fill(rgbBuffer, 0); } hasContent = false; } @@ -95,8 +85,8 @@ public class GraphicsPlane { return hasContent; } - public synchronized BufferedImage getImage() { - return image; + public synchronized int[] getRgbBuffer() { + return rgbBuffer; } public int getCanvasWidth() { @@ -107,13 +97,6 @@ public class GraphicsPlane { return canvasHeight; } - public synchronized Graphics2D getGraphics() { - return g2d; - } - - private int screenCols = 80; - private int screenRows = 24; - public void setScreenDimensions(int cols, int rows) { this.screenCols = cols > 0 ? cols : 80; this.screenRows = rows > 0 ? rows : 24; @@ -148,35 +131,114 @@ public class GraphicsPlane { } /** - * Draws an absolute or relative line. + * Safely plots a pixel at (x, y). */ - public synchronized void drawLine(int x1, int y1, int x2, int y2, Color color, int lineType, int lineWidth) { - if (g2d == null) return; - g2d.setColor(color != null ? color : Color.WHITE); - g2d.setStroke(createStroke(lineType, lineWidth)); - g2d.drawLine(x1, y1, x2, y2); + public synchronized void setPixel(int x, int y, int colorArgb) { + if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { + rgbBuffer[y * canvasWidth + x] = colorArgb; + hasContent = true; + } + } + + /** + * Draws an absolute or relative line using Bresenham's algorithm with line styles and widths. + */ + public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) { + int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; + int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1; + + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int sx = x1 < x2 ? 1 : -1; + int sy = y1 < y2 ? 1 : -1; + int err = dx - dy; + + int curX = x1; + int curY = y1; + int stepIndex = 0; + + while (true) { + if (shouldPlotLinePixel(stepIndex, lineType)) { + drawPixelWithThickness(curX, curY, color, thickness); + } + stepIndex++; + + if (curX == x2 && curY == y2) { + break; + } + + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + curX += sx; + } + if (e2 < dx) { + err += dx; + curY += sy; + } + } hasContent = true; } + private boolean shouldPlotLinePixel(int step, int lineType) { + switch (lineType) { + case GocaConstants.LT_DOT: + return (step % 4) < 2; + case GocaConstants.LT_SHORTDASH: + return (step % 6) < 4; + case GocaConstants.LT_DASHDOT: + int m12 = step % 12; + return m12 < 6 || (m12 >= 8 && m12 < 10); + case GocaConstants.LT_DOUBLEDOT: + int m10 = step % 10; + return m10 < 2 || (m10 >= 4 && m10 < 6); + case GocaConstants.LT_LONGDASH: + return (step % 11) < 8; + case GocaConstants.LT_DASHDOUBLEDOT: + int m18 = step % 18; + return m18 < 8 || (m18 >= 10 && m18 < 12) || (m18 >= 14 && m18 < 16); + case GocaConstants.LT_SOLID: + case GocaConstants.LT_DEFAULT: + default: + return true; + } + } + + private void drawPixelWithThickness(int x, int y, int color, int thickness) { + if (thickness <= 1) { + setPixel(x, y, color); + } else { + for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) { + for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) { + setPixel(x + dx, y + dy, color); + } + } + } + } + /** * Draws a full or partial arc / ellipse. */ public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg, - Color color, int lineType, int lineWidth, boolean isFull) { - if (g2d == null) return; - g2d.setColor(color != null ? color : Color.WHITE); - g2d.setStroke(createStroke(lineType, lineWidth)); + int colorArgb, int lineType, int lineWidth, boolean isFull) { + if (rx <= 0) rx = 1; + if (ry <= 0) ry = 1; - double x = cx - rx; - double y = cy - ry; - double w = rx * 2.0; - double h = ry * 2.0; + int numSteps = Math.max(24, Math.max(rx, ry) * 4); + double startRad = Math.toRadians(startAngleDeg); + double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg); + double stepRad = sweepRad / numSteps; - if (isFull) { - g2d.drawOval((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h)); - } else { - Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngleDeg, sweepAngleDeg, Arc2D.OPEN); - g2d.draw(arc); + int prevX = (int) Math.round(cx + rx * Math.cos(startRad)); + int prevY = (int) Math.round(cy - ry * Math.sin(startRad)); + + for (int i = 1; i <= numSteps; i++) { + double angle = startRad + i * stepRad; + int nextX = (int) Math.round(cx + rx * Math.cos(angle)); + int nextY = (int) Math.round(cy - ry * Math.sin(angle)); + drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth); + prevX = nextX; + prevY = nextY; } hasContent = true; } @@ -184,53 +246,101 @@ public class GraphicsPlane { /** * Draws a Fillet (spline / curve approximation across control points). */ - public synchronized void drawFillet(int[] px, int[] py, int numPoints, Color color, int lineType, int lineWidth) { - if (g2d == null || numPoints < 2) return; - g2d.setColor(color != null ? color : Color.WHITE); - g2d.setStroke(createStroke(lineType, lineWidth)); - - GeneralPath path = new GeneralPath(); - path.moveTo(px[0], py[0]); + public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) { + if (px == null || py == null || numPoints < 2) return; if (numPoints == 2) { - path.lineTo(px[1], py[1]); - } else { - for (int i = 1; i < numPoints - 1; i++) { - double midX = (px[i] + px[i + 1]) / 2.0; - double midY = (py[i] + py[i + 1]) / 2.0; - path.quadTo(px[i], py[i], midX, midY); - } - path.lineTo(px[numPoints - 1], py[numPoints - 1]); + drawLine(px[0], py[0], px[1], py[1], colorArgb, lineType, lineWidth); + return; } - g2d.draw(path); + int prevX = px[0]; + int prevY = py[0]; + + for (int i = 0; i < numPoints - 1; i++) { + double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0; + double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0; + double p1x = px[i]; + double p1y = py[i]; + double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0; + double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0; + + int steps = 20; + for (int s = 1; s <= steps; s++) { + double t = (double) s / steps; + double oneMinusT = 1.0 - t; + double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x; + double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y; + int nextX = (int) Math.round(bx); + int nextY = (int) Math.round(by); + drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth); + prevX = nextX; + prevY = nextY; + } + } hasContent = true; } /** * Fills a closed polygon area with a solid color or hatching pattern. */ - public synchronized void fillArea(int[] px, int[] py, int numPoints, Color fillColor, int pattern, - boolean drawBoundary, Color boundaryColor, int lineType, int lineWidth) { - if (g2d == null || numPoints < 3) return; + public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern, + boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) { + if (px == null || py == null || numPoints < 3) return; - Polygon poly = new Polygon(px, py, numPoints); + int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF; - if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) { - g2d.setColor(fillColor != null ? fillColor : Color.WHITE); - g2d.fill(poly); - } else if (pattern != GocaConstants.PT_EMPTY) { - // Fill with pattern texture - BufferedImage patImg = createPatternTexture(pattern, fillColor != null ? fillColor : Color.WHITE); - java.awt.TexturePaint tp = new java.awt.TexturePaint(patImg, new java.awt.Rectangle(0, 0, 8, 8)); - g2d.setPaint(tp); - g2d.fill(poly); + if (pattern != GocaConstants.PT_EMPTY) { + // Find polygon vertical bounds + int minY = py[0]; + int maxY = py[0]; + for (int i = 1; i < numPoints; i++) { + if (py[i] < minY) minY = py[i]; + if (py[i] > maxY) maxY = py[i]; + } + minY = Math.max(0, minY); + maxY = Math.min(canvasHeight - 1, maxY); + + List nodeX = new ArrayList<>(); + byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0]; + + for (int y = minY; y <= maxY; y++) { + nodeX.clear(); + int j = numPoints - 1; + for (int i = 0; i < numPoints; i++) { + if ((py[i] < y && py[j] >= y) || (py[j] < y && py[i] >= y)) { + int x = px[i] + (int) Math.round((double) (y - py[i]) / (py[j] - py[i]) * (px[j] - px[i])); + nodeX.add(x); + } + j = i; + } + + Collections.sort(nodeX); + + for (int i = 0; i < nodeX.size(); i += 2) { + if (i + 1 >= nodeX.size()) break; + int leftX = Math.max(0, nodeX.get(i)); + int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); + + for (int x = leftX; x <= rightX; x++) { + if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) { + setPixel(x, y, fill); + } else { + int b = patRows[y & 7] & 0xFF; + if (((b >> (7 - (x & 7))) & 1) != 0) { + setPixel(x, y, fill); + } + } + } + } + } } - if (drawBoundary && boundaryColor != null) { - g2d.setColor(boundaryColor); - g2d.setStroke(createStroke(lineType, lineWidth)); - g2d.draw(poly); + if (drawBoundary && boundaryColorArgb != 0) { + for (int i = 0; i < numPoints; i++) { + int next = (i + 1) % numPoints; + drawLine(px[i], py[i], px[next], py[next], boundaryColorArgb, lineType, lineWidth); + } } hasContent = true; } @@ -238,67 +348,92 @@ public class GraphicsPlane { /** * Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle). */ - public synchronized void drawMarker(int x, int y, int markerType, int size, Color color) { - if (g2d == null) return; - g2d.setColor(color != null ? color : Color.WHITE); - g2d.setStroke(new BasicStroke(1.5f)); - + public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) { + int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; int s = Math.max(3, size > 0 ? size : 5); switch (markerType) { case GocaConstants.MK_CROSS: // x - g2d.drawLine(x - s, y - s, x + s, y + s); - g2d.drawLine(x - s, y + s, x + s, y - s); + drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); break; case GocaConstants.MK_PLUS: // + case GocaConstants.MK_DEFAULT: - g2d.drawLine(x - s, y, x + s, y); - g2d.drawLine(x, y - s, x, y + s); + drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); break; case GocaConstants.MK_DIAMOND: // <> - g2d.drawPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4); + drawLine(x, y - s, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x + s, y, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x, y + s, x - s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s, y, x, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); break; case GocaConstants.MK_SQUARE: // [] - g2d.drawRect(x - s, y - s, s * 2, s * 2); + drawLine(x - s, y - s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x + s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x + s, y + s, x - s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s, y + s, x - s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); break; case GocaConstants.MK_6STAR: // 6-point star - g2d.drawLine(x - s, y, x + s, y); - g2d.drawLine(x - s / 2, y - s, x + s / 2, y + s); - g2d.drawLine(x - s / 2, y + s, x + s / 2, y - s); + drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s / 2, y - s, x + s / 2, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s / 2, y + s, x + s / 2, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); break; case GocaConstants.MK_8STAR: // 8-point star - g2d.drawLine(x - s, y, x + s, y); - g2d.drawLine(x, y - s, x, y + s); - g2d.drawLine(x - s, y - s, x + s, y + s); - g2d.drawLine(x - s, y + s, x + s, y - s); + drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); break; case GocaConstants.MK_SDIAMOND: // solid diamond - g2d.fillPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4); + fillArea(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4, + color, GocaConstants.PT_SOLID, false, 0, 0, 0); break; case GocaConstants.MK_SSQUARE: // solid square - g2d.fillRect(x - s, y - s, s * 2, s * 2); + fillArea(new int[]{x - s, x + s, x + s, x - s}, new int[]{y - s, y - s, y + s, y + s}, 4, + color, GocaConstants.PT_SOLID, false, 0, 0, 0); break; case GocaConstants.MK_DOT: // dot - g2d.fillOval(x - 2, y - 2, 4, 4); + for (int dy = -2; dy <= 2; dy++) { + for (int dx = -2; dx <= 2; dx++) { + if (dx * dx + dy * dy <= 4) { + setPixel(x + dx, y + dy, color); + } + } + } break; case GocaConstants.MK_CIRCLE: // circle - g2d.drawOval(x - s, y - s, s * 2, s * 2); - break; default: - g2d.drawOval(x - s, y - s, s * 2, s * 2); + drawArc(x, y, s, s, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true); break; } hasContent = true; } + private static final int[] VSS_OFFSETS = new int[256]; + static { + Arrays.fill(VSS_OFFSETS, -1); + int sym = VectorSymbolData.VSS_SYMBOL_START; // 33 + if (sym < 256) { + VSS_OFFSETS[sym] = 0; + } + for (int i = 0; i < VectorSymbolData.vss_data.length; i++) { + if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF + sym++; + if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) { + VSS_OFFSETS[sym] = i + 1; + } + } + } + } + /** * Draws stroked vector text using IBM Vector Symbol Set (VSS). */ - public synchronized void drawVectorText(int x, int y, String text, Color color, + public synchronized void drawVectorText(int x, int y, String text, int colorArgb, int cellWidth, int cellHeight, int dir, double angle) { - if (g2d == null || text == null || text.isEmpty()) return; - g2d.setColor(color != null ? color : Color.WHITE); - g2d.setStroke(new BasicStroke(1.2f)); + if (text == null || text.isEmpty()) return; + int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; int curX = x; int curY = y; @@ -307,7 +442,7 @@ public class GraphicsPlane { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); - drawVssChar(curX, curY, c, cw, ch); + drawVssChar(curX, curY, c, color, cw, ch); switch (dir) { case GocaConstants.CD_TB: curY += ch; break; @@ -323,24 +458,54 @@ public class GraphicsPlane { hasContent = true; } - private void drawVssChar(int x, int y, char c, int cw, int ch) { - int idx = (int) c; - if (idx < VectorSymbolData.VSS_SYMBOL_START || idx > VectorSymbolData.VSS_SYMBOL_END) { - // Draw simple bounding box or space + private void drawVssChar(int x, int y, char c, int color, int cw, int ch) { + int code = (int) c; + if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) { return; } - // Fallback vector stroke rendering - g2d.drawString(String.valueOf(c), x, y + ch); + int offset = VSS_OFFSETS[code]; + if (offset < 0) { + return; + } + + int ptr = offset; + while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) { + int order = VectorSymbolData.vss_data[ptr] & 0xFF; + if (order == 0xC1) { + int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF; + int numPoints = byteLen / 4; + int dataPtr = ptr + 2; + + if (numPoints >= 2) { + int prevVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF); + int prevVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF); + int prevPx = x + (int) Math.round(((double) prevVx / VectorSymbolData.VSS_WIDTH) * cw); + int prevPy = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - prevVy) / VectorSymbolData.VSS_HEIGHT) * ch); + + for (int p = 1; p < numPoints; p++) { + int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF); + int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF); + int px = x + (int) Math.round(((double) vx / VectorSymbolData.VSS_WIDTH) * cw); + int py = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch); + + drawLine(prevPx, prevPy, px, py, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL); + prevPx = px; + prevPy = py; + } + } + ptr += 2 + byteLen; + } else { + ptr++; + } + } } /** * Draws raw image pixel bitmap. */ - public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, Color fgColor) { - if (g2d == null || imageData == null || width <= 0 || height <= 0) return; - - BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF; + public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { + if (imageData == null || width <= 0 || height <= 0) return; + int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { @@ -349,52 +514,11 @@ public class GraphicsPlane { if (byteIdx < imageData.length) { boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0; if (bit) { - img.setRGB(col, row, fgRgb); + setPixel(x + col, y + row, fgColor); } } } } - - g2d.drawImage(img, x, y, null); hasContent = true; } - - private Stroke createStroke(int lineType, int lineWidth) { - float width = (lineWidth == GocaConstants.LW_THICK) ? 2.5f : 1.2f; - - switch (lineType) { - case GocaConstants.LT_DOT: - return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f); - case GocaConstants.LT_SHORTDASH: - return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{4.0f, 2.0f}, 0.0f); - case GocaConstants.LT_DASHDOT: - return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{6.0f, 2.0f, 2.0f, 2.0f}, 0.0f); - case GocaConstants.LT_DOUBLEDOT: - return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f, 2.0f, 4.0f}, 0.0f); - case GocaConstants.LT_LONGDASH: - return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 3.0f}, 0.0f); - case GocaConstants.LT_DASHDOUBLEDOT: - return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f}, 0.0f); - case GocaConstants.LT_SOLID: - case GocaConstants.LT_DEFAULT: - default: - return new BasicStroke(width); - } - } - - private BufferedImage createPatternTexture(int patternIndex, Color color) { - BufferedImage pat = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB); - byte[] patternRows = (patternIndex >= 0 && patternIndex < PATTERN_DATA.length) ? PATTERN_DATA[patternIndex] : PATTERN_DATA[0]; - int rgb = color.getRGB(); - - for (int r = 0; r < 8; r++) { - int b = patternRows[r] & 0xFF; - for (int c = 0; c < 8; c++) { - if (((b >> (7 - c)) & 1) != 0) { - pat.setRGB(c, r, rgb); - } - } - } - return pat; - } } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java index 273d8fe..791c7a2 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java @@ -1,7 +1,5 @@ package org.lib3270j.graphics; -import java.awt.Color; -import java.awt.Graphics2D; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -65,21 +63,6 @@ public class ProgramSymbolManager { return set.getSlot(index); } - /** - * Draws a programmed symbol character cell if defined. - * Returns true if symbol was drawn, false if not found. - */ - public synchronized boolean drawSymbol(Graphics2D g2d, int lcid, int codePoint, - int x, int y, int cellWidth, int cellHeight, - Color fgColor, Color bgColor) { - ProgramSymbolSet.SymbolSlot slot = getSymbol(lcid, codePoint); - if (slot != null) { - slot.draw(g2d, x, y, cellWidth, cellHeight, fgColor, bgColor); - return true; - } - return false; - } - /** * Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload. */ diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java index 2e4184e..f41eb72 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java @@ -1,11 +1,5 @@ package org.lib3270j.graphics; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; - /** * Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE). * Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite). @@ -67,7 +61,7 @@ public class ProgramSymbolSet { private final int height; private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome) private final boolean isTriplePlane; - private BufferedImage cachedImage; + private int[] cachedRgbArray; private int cachedFgRgb = -1; private int cachedBgRgb = -1; @@ -95,28 +89,25 @@ public class ProgramSymbolSet { } /** - * Renders this symbol to a BufferedImage. + * Computes and returns the 32-bit ARGB pixel array for this symbol. + * The returned array has length (width * height). */ - public synchronized BufferedImage getImage(Color fgColor, Color bgColor) { - int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF; - int bgRgb = bgColor != null ? bgColor.getRGB() : 0x00000000; - - if (cachedImage != null && cachedFgRgb == fgRgb && cachedBgRgb == bgRgb) { - return cachedImage; + public synchronized int[] getRgbPixels(int fgArgb, int bgArgb) { + if (cachedRgbArray != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) { + return cachedRgbArray; } - BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); int[] rgbArray = new int[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int idx = y * width + x; - int val = (idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0; + int val = (pixelData != null && idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0; if (val == 0) { - rgbArray[idx] = bgRgb; + rgbArray[idx] = bgArgb; } else if (!isTriplePlane) { - rgbArray[idx] = fgRgb; + rgbArray[idx] = fgArgb; } else { // Triple-Plane RGB composite: // val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue @@ -128,21 +119,10 @@ public class ProgramSymbolSet { } } - img.setRGB(0, 0, width, height, rgbArray, 0, width); - this.cachedImage = img; - this.cachedFgRgb = fgRgb; - this.cachedBgRgb = bgRgb; - return img; - } - - /** - * Draws the symbol directly onto a Graphics2D surface with aspect scaling. - */ - public void draw(Graphics2D g2d, int x, int y, int cellWidth, int cellHeight, Color fgColor, Color bgColor) { - BufferedImage img = getImage(fgColor, bgColor); - if (img != null) { - g2d.drawImage(img, x, y, cellWidth, cellHeight, null); - } + this.cachedRgbArray = rgbArray; + this.cachedFgRgb = fgArgb; + this.cachedBgRgb = bgArgb; + return rgbArray; } } } diff --git a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java index 222cb79..071a3a9 100644 --- a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java @@ -54,6 +54,52 @@ public class QueryReplyBuilderTest { assertTrue(hasB4); } + @Test + public void testBuildAllQueryRepliesWithBoth() { + qrBuilder.setGraphicsMode(GraphicsMode.BOTH); + byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + assertNotNull(replies); + + // Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A) + boolean hasB0 = false; + boolean hasCharsetsWithLoadPs = false; + for (int i = 0; i < replies.length - 3; i++) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) { + hasB0 = true; + } + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) { + if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) { + hasCharsetsWithLoadPs = true; + } + } + } + assertTrue(hasB0); + assertTrue(hasCharsetsWithLoadPs); + } + + @Test + public void testBuildAllQueryRepliesWithProgrammedSymbols() { + qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS); + byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + assertNotNull(replies); + + // Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present + boolean hasB0 = false; + boolean hasCharsetsWithLoadPs = false; + for (int i = 0; i < replies.length - 3; i++) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) { + hasB0 = true; + } + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) { + if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) { + hasCharsetsWithLoadPs = true; + } + } + } + assertFalse(hasB0); + assertTrue(hasCharsetsWithLoadPs); + } + @Test public void testBuildFilteredQueryRepliesWithSupportedCodes() { byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA }; diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java index 0214ac7..0568190 100644 --- a/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java +++ b/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java @@ -1,9 +1,6 @@ package org.lib3270j.graphics; import org.junit.jupiter.api.Test; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; import static org.junit.jupiter.api.Assertions.*; public class ProgramSymbolManagerTest { @@ -62,12 +59,13 @@ public class ProgramSymbolManagerTest { assertNotNull(pixels); assertEquals(1, pixels[0]); // Row 0 Col 0 is 1 - // Test rendering into BufferedImage - BufferedImage canvas = new BufferedImage(80, 24, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = canvas.createGraphics(); - boolean drawn = manager.drawSymbol(g2, 0x40, 0x41, 0, 0, 10, 20, Color.GREEN, Color.BLACK); - g2.dispose(); - assertTrue(drawn); + // Test rendering into 32-bit ARGB pixels + int fg = 0xFF00FF00; // Green + int bg = 0xFF000000; // Black + int[] rgb = slot.getRgbPixels(fg, bg); + assertNotNull(rgb); + assertEquals(9 * 16, rgb.length); + assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green } @Test @@ -104,16 +102,18 @@ public class ProgramSymbolManagerTest { byte[] pixels = slot.getPixelData(); assertEquals(7, pixels[0] & 0xFF); - // Render image - BufferedImage img = slot.getImage(Color.WHITE, Color.BLACK); - assertNotNull(img); + // Render ARGB pixels + int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0xFF000000); + assertNotNull(rgb); - // Pixel (0,0) should be white composite (Red=255, Green=255, Blue=255) - int rgb = img.getRGB(0, 0); - Color pixelColor = new Color(rgb, true); - assertEquals(255, pixelColor.getRed()); - assertEquals(255, pixelColor.getGreen()); - assertEquals(255, pixelColor.getBlue()); + // Pixel (0,0) should be white composite (0xFFFFFFFF) + int pixelArgb = rgb[0]; + int r = (pixelArgb >> 16) & 0xFF; + int g = (pixelArgb >> 8) & 0xFF; + int b = pixelArgb & 0xFF; + assertEquals(255, r); + assertEquals(255, g); + assertEquals(255, b); } @Test