From 3526e682a11ad4f8aa97fd3efe813db1874f8f87 Mon Sep 17 00:00:00 2001 From: Rudi Date: Tue, 25 Aug 2026 22:22:20 +0000 Subject: [PATCH] Admops fix --- build_all.sh | 60 +++++ gradlew | 2 +- .../java/haus/nightmare/j3270/J3270App.java | 6 +- .../datastream/DataStreamProcessor.java | 10 + .../lib3270j/graphics/GocaDecoder.java | 105 ++++++-- .../lib3270j/graphics/GraphicsPlane.java | 62 ++++- .../lib3270j/graphics/GocaDecoderTest.java | 238 ++++++++++++++++++ 7 files changed, 445 insertions(+), 38 deletions(-) diff --git a/build_all.sh b/build_all.sh index effda27..18198f4 100755 --- a/build_all.sh +++ b/build_all.sh @@ -87,3 +87,63 @@ echo "=== Build Complete ===" echo "Executable JAR created: $BUILD_DIR/j3270.jar" ls -lh "$BUILD_DIR/j3270.jar" +# 5. Compile and run tests if requested +if [ "$1" = "test" ] || [ "$1" = "check" ]; then + echo "=== Running Tests ===" + TEST_BUILD_DIR="$BUILD_DIR/test-classes" + mkdir -p "$TEST_BUILD_DIR" + JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true) + FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP" + + find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true + if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then + find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true + fi + + if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then + echo "Compiling tests..." + "$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt" + rm -f "$BUILD_DIR/test_sources.txt" + + echo "Executing tests with JUnit Platform..." + cat << 'EOF' > "$BUILD_DIR/TestRunner.java" +import org.junit.platform.launcher.*; +import org.junit.platform.launcher.core.*; +import org.junit.platform.launcher.listeners.*; +import org.junit.platform.engine.discovery.*; +import static org.junit.platform.engine.discovery.DiscoverySelectors.*; +import java.io.PrintWriter; + +public class TestRunner { + public static void main(String[] args) { + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors( + selectPackage("haus.nightmare.lib3270j.graphics"), + selectPackage("haus.nightmare.lib3270j.datastream"), + selectPackage("haus.nightmare.lib3270j.screen"), + selectPackage("haus.nightmare.lib3270j.protocol") + ) + .build(); + + Launcher launcher = LauncherFactory.create(); + SummaryGeneratingListener listener = new SummaryGeneratingListener(); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + TestExecutionSummary summary = listener.getSummary(); + summary.printTo(new PrintWriter(System.out)); + summary.printFailuresTo(new PrintWriter(System.err)); + + if (summary.getTotalFailureCount() > 0) { + System.exit(1); + } + } +} +EOF + "$JAVAC_BIN" -cp "$FULL_CP:$TEST_BUILD_DIR" -d "$BUILD_DIR" "$BUILD_DIR/TestRunner.java" + JAVA_BIN_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/java}" + JAVA_BIN_EXEC="${JAVA_BIN_EXEC:-java}" + "$JAVA_BIN_EXEC" -cp "$FULL_CP:$TEST_BUILD_DIR:$BUILD_DIR" TestRunner + fi +fi + diff --git a/gradlew b/gradlew index 7997892..49bef16 100755 --- a/gradlew +++ b/gradlew @@ -1,2 +1,2 @@ #!/bin/sh -exec "$(dirname "$0")/build_all.sh" +exec "$(dirname "$0")/build_all.sh" "$@" diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index cb09e28..791ca24 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -501,9 +501,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate consoleHandler.setFilter(appFilter); globalRoot.addHandler(consoleHandler); - Logger.getLogger("haus.nightmare").setLevel(logLevel); - Logger.getLogger("haus.nightmare.j3270").setLevel(logLevel); - Logger.getLogger("haus.nightmare.lib3270j").setLevel(logLevel); + Logger.getLogger("haus.nightmare").setLevel(Level.ALL); + Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL); + Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL); try { java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 270f9cb..b270835 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -59,6 +59,7 @@ public class DataStreamProcessor { this.outputBuffer = new byte[32768]; this.outputPos = 0; this.gocaDecoder.setProgramSymbolManager(programSymbolManager); + this.graphicsPlane.setProgramSymbolManager(programSymbolManager); } public QueryReplyBuilder getQueryReplyBuilder() { @@ -838,6 +839,13 @@ public class DataStreamProcessor { int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4); int orderLen = Math.max(0, fieldLen - (orderOffset - pos)); + StringBuilder hexDump = new StringBuilder(); + for (int i = 0; i < Math.min(32, fieldLen); i++) { + hexDump.append(String.format("%02x ", data[pos + i] & 0xFF)); + } + log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]", + sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim())); + graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); if (flags == 0x80) { // SPAN_FIRST @@ -856,6 +864,7 @@ public class DataStreamProcessor { } byte[] fullStream = gocaAccumulator.toByteArray(); gocaAccumulator.reset(); + log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length)); if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) { gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length); } else { @@ -864,6 +873,7 @@ public class DataStreamProcessor { notifyScreenUpdated(); } else { // SPAN_ONLY (0xC0) gocaAccumulator.reset(); + log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen)); if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) { gocaDecoder.processProcedureOrders(data, orderOffset, orderLen); } else { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java index ea7facc..413ea8c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java @@ -20,7 +20,7 @@ public class GocaDecoder { private int curX = 0; private int curY = 0; private int curColor = GocaConstants.GOCA_COLORS[0]; - private int bgMix = 2; // BMX_OVERPAINT default + private int bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec) private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black private int lineType = GocaConstants.LT_SOLID; private int lineWidth = GocaConstants.LW_NORMAL; @@ -28,6 +28,7 @@ public class GocaDecoder { private int markerSize = 5; private int markerColor = GocaConstants.GOCA_COLORS[0]; private int pattern = GocaConstants.PT_SOLID; + private int patternSet = 0; private int fillColor = GocaConstants.GOCA_COLORS[0]; private int charDir = GocaConstants.CD_LR; private double charAngle = 0.0; @@ -99,7 +100,7 @@ public class GocaDecoder { private final java.util.List activeSegmentsInOrder = new java.util.ArrayList<>(); private int currentSegId = 0; - // Graphic Cursor (Light-Pen) state + // Graphics Cursor state private boolean graphicsCursorActive = false; private int graphicCursorX = 0; private int graphicCursorY = 0; @@ -110,6 +111,9 @@ public class GocaDecoder { public void setProgramSymbolManager(ProgramSymbolManager psm) { this.programSymbolManager = psm; + if (this.plane != null) { + this.plane.setProgramSymbolManager(psm); + } } public GraphicsPlane getGraphicsPlane() { @@ -203,7 +207,7 @@ public class GocaDecoder { public synchronized void resetAttributes() { curColor = getColor(0); - bgMix = 2; // BMX_OVERPAINT + bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec) bgColor = GocaConstants.GOCA_COLORS[8]; // Black lineType = GocaConstants.LT_SOLID; lineWidth = GocaConstants.LW_NORMAL; @@ -211,6 +215,7 @@ public class GocaDecoder { markerSize = 5; markerColor = curColor; pattern = GocaConstants.PT_SOLID; + patternSet = 0; fillColor = curColor; charDir = GocaConstants.CD_LR; charAngle = 0.0; @@ -232,30 +237,26 @@ public class GocaDecoder { * * IMPORTANT ARCHITECTURE & PARSING SAFETY NOTE: * GOCA orders follow IBM GA23-0059 and Host On-Demand (HOD) architecture rules: - * 1. 1-byte standalone orders (NOP, ERASE, etc.) -> length 1. - * 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> 1 or 2 bytes (with 0x00 trailing byte). - * 3. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes. - * 4. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39, + * 1. 1-byte standalone orders (NOP, ERASE, GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> length 1. + * 2. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes. + * 3. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39, * GSPT 0x28, GSMT 0x29, GSMS_SET 0x3C) can be transmitted either as: * - Short 2-byte form: [opcode] [value] (e.g. GBAR with flags 0x80 -> '68 80') * - Self-defining 3-byte form: [opcode] [length=0x01] [value] (e.g. '68 01 80') * CRITICAL: NEVER allow 1-byte attribute orders (like GBAR 0x68 or GSCC 0x3B) to fall through to the * variable-length formula `(data[idx + 1] & 0xFF) + 2`. If GBAR flags (e.g. 0x80 for boundary) are read as * a length field, the decoder will skip 130 bytes, corrupting and skipping all subsequent drawing orders! - * 5. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.) + * 4. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.) * have a 1-byte length byte at data[idx + 1], making total length = payloadLen + 2. */ private int getOrderLength(byte[] data, int idx, int end) { int order = data[idx] & 0xFF; - if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00) { + if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || + order == GocaConstants.G_GEAR || order == GocaConstants.G_ENDSEGM || + order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_GEIMG || + order == GocaConstants.G_GPOP || order == GocaConstants.G_GERASE) { return 1; } - if (order == GocaConstants.G_GEAR || - order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE || - order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP || - order == GocaConstants.G_GERASE) { - return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1; - } if (idx + 1 >= end) { return -1; } @@ -399,23 +400,30 @@ public class GocaDecoder { break; } case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) + int segId = 0; + int flag0 = 0; + int flag1 = 0; if (idx + 5 < end) { - int segId = ((inputData[idx + 2] & 0xFF) << 24) | - ((inputData[idx + 3] & 0xFF) << 16) | - ((inputData[idx + 4] & 0xFF) << 8) | - (inputData[idx + 5] & 0xFF); + segId = ((inputData[idx + 2] & 0xFF) << 24) | + ((inputData[idx + 3] & 0xFF) << 16) | + ((inputData[idx + 4] & 0xFF) << 8) | + (inputData[idx + 5] & 0xFF); currentSegId = segId; SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new); activeSegmentsInOrder.remove(sb); activeSegmentsInOrder.add(sb); } - if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) { + if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF; + if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF; + logger.info(String.format("GOCA BEGSEGM: segId=%d flag0=0x%02x flag1=0x%02x callDepth=%d", segId, flag0, flag1, callDepth)); + if (idx + 7 < end && (flag1 & 0x06) == 0) { resetAttributes(); } idx += orderLen; break; } case GocaConstants.G_ENDSEGM: { // End Segment (0x71) + logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId)); if (currentSegId != 0) { SegmentBounds sb = segmentBoundsMap.get(currentSegId); if (sb != null) { @@ -486,6 +494,7 @@ public class GocaDecoder { } else { targetSegId = -1; } + logger.info(String.format("GOCA GCALL: targetSegId=%d callDepth=%d", targetSegId, callDepth)); if (targetSegId != -1) { byte[] targetSeg = segmentStore.get(targetSegId); if (targetSeg != null) { @@ -498,6 +507,8 @@ public class GocaDecoder { decodeStreamDirect(targetSeg, 0, targetSeg.length); callDepth--; currentSegId = savedSegId; + } else { + logger.warning("GOCA GCALL: target segment not found in store: " + targetSegId); } } } @@ -530,7 +541,9 @@ public class GocaDecoder { case GocaConstants.G_GSCOL: { // Set Color (0x0A) int colIdx = inputData[idx + 1] & 0xFF; curColor = getColor(colIdx); - fillColor = curColor; + if (!inArea) { + fillColor = curColor; + } idx += orderLen; break; } @@ -538,7 +551,9 @@ public class GocaDecoder { if (payloadLen >= 2 && idx + 3 < end) { int colIdx = inputData[idx + 3] & 0xFF; curColor = getColor(colIdx); - fillColor = curColor; + if (!inArea) { + fillColor = curColor; + } } idx += orderLen; break; @@ -570,12 +585,14 @@ public class GocaDecoder { break; } case GocaConstants.G_GSPS: { // Set Pattern Set (0x08) - pattern = inputData[idx + 1] & 0xFF; + patternSet = inputData[idx + 1] & 0xFF; + logger.info("GOCA GSPS: patternSet=" + patternSet); idx += orderLen; break; } case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28) pattern = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); + logger.info("GOCA GSPT: pattern=" + pattern); idx += orderLen; break; } @@ -605,12 +622,14 @@ public class GocaDecoder { } case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D) bgMix = inputData[idx + 1] & 0xFF; + logger.info("GOCA GSBMX: bgMix=" + bgMix); idx += orderLen; break; } case GocaConstants.G_GBAR: { // Begin Area (0x68) int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); - boolean drawBoundary = (flags & 0x80) != 0 || (flags & 0x40) != 0 || (flags == 0); + boolean drawBoundary = (flags & 0x80) != 0; + logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary)); beginArea(drawBoundary); idx += orderLen; break; @@ -837,6 +856,8 @@ public class GocaDecoder { this.areaPointsY.clear(); this.areaPolygons.clear(); this.currentPolyPts = 0; + logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d", + drawBoundary, fillColor, patternSet, pattern)); } private void endArea() { @@ -846,6 +867,7 @@ public class GocaDecoder { currentPolyPts = 0; } if (areaPointsX.size() < 3 || areaPolygons.isEmpty()) { + logger.info(String.format("GOCA endArea: dropped (pts=%d, polys=%d)", areaPointsX.size(), areaPolygons.size())); inArea = false; areaPointsX.clear(); areaPointsY.clear(); @@ -867,7 +889,10 @@ public class GocaDecoder { polyCounts[i] = areaPolygons.get(i); } - plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, + logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d", + n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix)); + + plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor); inArea = false; areaPointsX.clear(); @@ -1108,6 +1133,22 @@ public class GocaDecoder { trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2)); trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2)); + if (inArea) { + int steps = Math.max(8, (int) Math.round(Math.abs(sweepAngleDeg) / 15.0)); + double startRad = Math.toRadians(startAngleDeg); + double sweepRad = Math.toRadians(sweepAngleDeg); + for (int i = 0; i <= steps; i++) { + double a = startRad + (sweepRad * i) / steps; + int ax = centerX + (int) Math.round(Math.cos(a) * semiAxis1); + int ay = centerY + (int) Math.round(Math.sin(a) * semiAxis2); + if (i == 0 && currentPolyPts == 0) { + addAreaLineStart(ax, ay); + } else { + addAreaPoint(ax, ay); + } + } + } + plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg, curColor, lineType, lineWidth, isFull); @@ -1125,6 +1166,13 @@ public class GocaDecoder { if (fromCurPos) { ptsX.add(plane.mapXDouble(curX)); ptsY.add(plane.mapYDouble(curY)); + if (inArea) { + if (currentPolyPts == 0) { + addAreaLineStart(curX, curY); + } else { + addAreaPoint(curX, curY); + } + } } while (pos + 4 <= end) { @@ -1132,6 +1180,13 @@ public class GocaDecoder { int y = readCoord(data, pos + 2); ptsX.add(plane.mapXDouble(x)); ptsY.add(plane.mapYDouble(y)); + if (inArea) { + if (ptsX.size() == 1 && currentPolyPts == 0) { + addAreaLineStart(x, y); + } else { + addAreaPoint(x, y); + } + } curX = x; curY = y; pos += 4; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java index 055f739..5af74e5 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -44,6 +44,15 @@ public class GraphicsPlane { private int screenCols = 80; private int screenRows = 24; + private ProgramSymbolManager programSymbolManager; + + public void setProgramSymbolManager(ProgramSymbolManager psm) { + this.programSymbolManager = psm; + } + + public ProgramSymbolManager getProgramSymbolManager() { + return programSymbolManager; + } public GraphicsPlane(int width, int height) { this.canvasWidth = Math.max(1, width); @@ -338,7 +347,7 @@ public class GraphicsPlane { } private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) { - int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; + int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1; int dx = Math.abs(x2 - x1); @@ -517,12 +526,23 @@ public class GraphicsPlane { int fillColorArgb, int pattern, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, int bgMix, int bgColorArgb) { + fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb); + } + + /** + * Fills an area with symbol-set-aware and pattern-symbol-aware rasterization. + */ + public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys, + int fillColorArgb, int patternSet, int pattern, + boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, + int bgMix, int bgColorArgb) { if (px == null || py == null || numPoints < 3) return; - int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF; + int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int bg = bgColorArgb; - if (pattern != GocaConstants.PT_EMPTY) { + boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER); + if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) { // Find polygon vertical bounds across all points int minY = py[0]; int maxY = py[0]; @@ -534,7 +554,18 @@ public class GraphicsPlane { maxY = Math.min(canvasHeight - 1, maxY); List nodeX = new ArrayList<>(); - byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0]; + byte[] patRows = null; + ProgramSymbolSet.SymbolSlot psSlot = null; + if (patternSet >= 0x40 && programSymbolManager != null) { + psSlot = programSymbolManager.getSymbol(patternSet, pattern); + } + if (psSlot == null) { + if (pattern >= 0 && pattern < PATTERN_DATA.length) { + patRows = PATTERN_DATA[pattern]; + } else { + patRows = PATTERN_DATA[0]; + } + } for (int y = minY; y <= maxY; y++) { nodeX.clear(); @@ -567,13 +598,26 @@ public class GraphicsPlane { int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); for (int x = leftX; x <= rightX; x++) { - if (pattern == GocaConstants.PT_SOLID || pattern == 0 || pattern >= 16) { + if (psSlot != null) { + int psW = psSlot.getWidth(); + int psH = psSlot.getHeight(); + int psX = (psW > 0) ? (x % psW) : 0; + int psY = (psH > 0) ? (y % psH) : 0; + byte[] psPix = psSlot.getPixelData(); + int pIdx = psY * psW + psX; + boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); + if (bit) { + setPixel(x, y, fill); + } else if (bgMix == GocaConstants.MIX_OVER) { + setPixel(x, y, bg); + } + } else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { setPixel(x, y, fill); } else { int b = patRows[y & 7] & 0xFF; if (((b >> (7 - (x & 7))) & 1) != 0) { setPixel(x, y, fill); - } else if (bgMix != 0) { // BMX_OVERPAINT (opaque background) + } else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background) setPixel(x, y, bg); } } @@ -610,7 +654,7 @@ public class GraphicsPlane { * Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision. */ public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) { - int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; + int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; double s = Math.max(3.0, size > 0 ? (double) size : 5.0); switch (markerType) { @@ -731,7 +775,7 @@ public class GraphicsPlane { public synchronized void drawVectorText(double x, double y, String text, int colorArgb, double cellWidth, double cellHeight, int dir, double angle) { if (text == null || text.isEmpty()) return; - int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; + int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; double curX = x; double curY = y; @@ -822,7 +866,7 @@ public class GraphicsPlane { */ 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; + int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0]; int bytesPerRow = (width + 7) / 8; for (int row = 0; row < height; row++) { diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java index 5261b54..70a80c3 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java @@ -520,6 +520,8 @@ public class GocaDecoderTest { // Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20) // GEAR (0x60) ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(GocaConstants.G_GSPT); + out.write(GocaConstants.PT_SOLID); out.write(GocaConstants.G_GBAR); out.write(0x80); @@ -555,4 +557,240 @@ public class GocaDecoderTest { } assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels"); } + + @Test + public void testPatternSetAndPatternSymbolSeparation() { + GraphicsPlane plane = new GraphicsPlane(200, 200); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Color to Blue (Color 1) + out.write(GocaConstants.G_GSCOL); + out.write(0x01); // Blue + + // Set Pattern Set to 0 (default standard patterns) + out.write(GocaConstants.G_GSPS); + out.write(0x00); + + // Set Pattern Symbol to 5 (D5 50% checkerboard) + out.write(GocaConstants.G_GSPT); + out.write(0x05); + + // Begin Area with boundary (0x80) + out.write(GocaConstants.G_GBAR); + out.write(0x80); + + // Draw rectangle (0,0) to (50,0) to (50,50) to (0,50) to (0,0) + out.write(GocaConstants.G_GLINE); + out.write(0x14); // 5 points * 4 bytes = 20 bytes + out.write(0x00); out.write(0); out.write(0x00); out.write(0); + out.write(0x00); out.write(50); out.write(0x00); out.write(0); + out.write(0x00); out.write(50); out.write(0x00); out.write(50); + out.write(0x00); out.write(0); out.write(0x00); out.write(50); + out.write(0x00); out.write(0); out.write(0x00); out.write(0); + + // End Area + out.write(GocaConstants.G_GEAR); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + // Verify that D5 checkerboard pattern is NOT solid fill (it should have transparent gaps) + int[] buffer = plane.getRgbBuffer(); + int bluePixels = 0; + int zeroPixels = 0; + int blueArgb = GocaConstants.GOCA_COLORS[1]; + for (int p : buffer) { + if (p == blueArgb) bluePixels++; + else if (p == 0) zeroPixels++; + } + assertTrue(bluePixels > 0, "Expected blue pixels in patterned area"); + assertTrue(zeroPixels > 0, "Expected transparent zero pixels in patterned area due to D5 checkerboard gaps"); + } + + @Test + public void testEmptyPatternAreaDrawsOnlyBoundary() { + GraphicsPlane plane = new GraphicsPlane(200, 200); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Color to Red (Color 2) + out.write(GocaConstants.G_GSCOL); + out.write(0x02); + + // Set Pattern Symbol to PT_EMPTY (15) + out.write(GocaConstants.G_GSPT); + out.write(GocaConstants.PT_EMPTY); + + // Begin Area with boundary (0x80) + out.write(GocaConstants.G_GBAR); + out.write(0x80); + + // Draw rectangle + out.write(GocaConstants.G_GLINE); + out.write(0x14); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(0x00); out.write(40); out.write(0x00); out.write(10); + out.write(0x00); out.write(40); out.write(0x00); out.write(40); + out.write(0x00); out.write(10); out.write(0x00); out.write(40); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + + // End Area + out.write(GocaConstants.G_GEAR); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + // Verify that center of the box is transparent (not filled) + int centerX = plane.mapX(25); + int centerY = plane.mapY(25); + int centerPixel = plane.getRgbBuffer()[centerY * plane.getCanvasWidth() + centerX]; + assertEquals(0, centerPixel, "Expected interior of PT_EMPTY area to remain transparent"); + } + + @Test + public void testGearFollowedByNopOrder() { + GraphicsPlane plane = new GraphicsPlane(200, 200); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Area with PT_EMPTY + out.write(GocaConstants.G_GSPT); + out.write(GocaConstants.PT_EMPTY); + out.write(GocaConstants.G_GBAR); + out.write(0x80); + out.write(GocaConstants.G_GLINE); + out.write(0x10); + out.write(0x00); out.write(0); out.write(0x00); out.write(0); + out.write(0x00); out.write(10); out.write(0x00); out.write(0); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(0x00); out.write(0); out.write(0x00); out.write(0); + out.write(GocaConstants.G_GEAR); // 0x60 + out.write(GocaConstants.G_NOP1); // 0x00 NOP - must not be consumed as part of GEAR! + + // Followed by a line + out.write(GocaConstants.G_GSCOL); + out.write(0x03); // Pink + out.write(GocaConstants.G_GLINE); + out.write(0x08); + out.write(0x00); out.write(20); out.write(0x00); out.write(20); + out.write(0x00); out.write(40); out.write(0x00); out.write(40); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + int pinkArgb = GocaConstants.GOCA_COLORS[3]; + boolean foundPink = false; + for (int p : plane.getRgbBuffer()) { + if (p == pinkArgb) { + foundPink = true; + break; + } + } + assertTrue(foundPink, "Expected pink line after GEAR + NOP to be drawn"); + } + + @Test + public void testAdmopsSlidePreviewSequence() { + GraphicsPlane plane = new GraphicsPlane(200, 200); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // 1. Draw Blue stippled slide background (Pattern 6, Blue color 1) + out.write(GocaConstants.G_GSCOL); out.write(0x01); // Blue + out.write(GocaConstants.G_GSPT); out.write(0x06); // Pattern 6 + out.write(GocaConstants.G_GBAR); out.write(0x80); + out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(0x00); out.write(90); out.write(0x00); out.write(10); + out.write(0x00); out.write(90); out.write(0x00); out.write(90); + out.write(0x00); out.write(10); out.write(0x00); out.write(90); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(GocaConstants.G_GEAR); + + // 2. Draw Red banner at top (Solid Pattern 16, Red color 2) + out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red + out.write(GocaConstants.G_GSPT); out.write(0x10); // Solid + out.write(GocaConstants.G_GBAR); out.write(0x80); + out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(0x00); out.write(90); out.write(0x00); out.write(10); + out.write(0x00); out.write(90); out.write(0x00); out.write(30); + out.write(0x00); out.write(10); out.write(0x00); out.write(30); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(GocaConstants.G_GEAR); + + // 3. Draw Yellow bullet text (Color 6, default modal solid pattern) + out.write(GocaConstants.G_GSCOL); out.write(0x06); // Yellow + out.write(GocaConstants.G_GBAR); out.write(0x80); + out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points + out.write(0x00); out.write(20); out.write(0x00); out.write(40); + out.write(0x00); out.write(80); out.write(0x00); out.write(40); + out.write(0x00); out.write(80); out.write(0x00); out.write(50); + out.write(0x00); out.write(20); out.write(0x00); out.write(50); + out.write(0x00); out.write(20); out.write(0x00); out.write(40); + out.write(GocaConstants.G_GEAR); + + // 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, GSCOL White inside GBAR) + out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background) + out.write(GocaConstants.G_GBAR); out.write(0x80); + out.write(GocaConstants.G_GSCOL); out.write(0x07); // White line color + out.write(GocaConstants.G_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line + out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(0x00); out.write(90); out.write(0x00); out.write(10); + out.write(0x00); out.write(90); out.write(0x00); out.write(90); + out.write(0x00); out.write(10); out.write(0x00); out.write(90); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(GocaConstants.G_GEAR); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + + // Verify that the red banner (e.g. at (50, 20)) was NOT overwritten by black/white! + int bannerX = plane.mapX(50); + int bannerY = plane.mapY(20); + int bannerPixel = plane.getRgbBuffer()[bannerY * plane.getCanvasWidth() + bannerX]; + int redArgb = GocaConstants.GOCA_COLORS[2]; + assertEquals(redArgb, bannerPixel, "Red header banner must be solid red and preserved"); + + // Verify that the yellow text (e.g. at (50, 45)) was NOT overwritten! + int textX = plane.mapX(50); + int textY = plane.mapY(45); + int textPixel = plane.getRgbBuffer()[textY * plane.getCanvasWidth() + textX]; + int yellowArgb = GocaConstants.GOCA_COLORS[6]; + assertEquals(yellowArgb, textPixel, "Yellow bullet text must be solid yellow and preserved"); + + // Verify that the blue stipple (in body region (20..80, 60..80)) contains blue pixels and was NOT overwritten by white! + int blueArgb = GocaConstants.GOCA_COLORS[1]; + boolean foundBlue = false; + int yMin = Math.min(plane.mapY(60), plane.mapY(80)); + int yMax = Math.max(plane.mapY(60), plane.mapY(80)); + int xMin = Math.min(plane.mapX(20), plane.mapX(80)); + int xMax = Math.max(plane.mapX(20), plane.mapX(80)); + for (int y = yMin; y <= yMax; y++) { + for (int x = xMin; x <= xMax; x++) { + int pix = plane.getRgbBuffer()[y * plane.getCanvasWidth() + x]; + if (pix == blueArgb) { + foundBlue = true; + break; + } + } + if (foundBlue) break; + } + assertTrue(foundBlue, "Slide body must contain stippled blue pixels"); + + // Verify that the boundary (e.g. at (10, 50)) is drawn in White! + int whiteArgb = GocaConstants.GOCA_COLORS[7]; + int borderX = plane.mapX(10); + int borderY = plane.mapY(50); + int borderPixel = plane.getRgbBuffer()[borderY * plane.getCanvasWidth() + borderX]; + assertEquals(whiteArgb, borderPixel, "Slide border outline must be drawn in White"); + } }