Antialiasing and fill fixes
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s

This commit is contained in:
2026-08-25 15:48:36 +00:00
parent dbe0cdaa89
commit 36b93df986
7 changed files with 760 additions and 148 deletions
@@ -74,7 +74,7 @@ public class DataStreamProcessorTest {
input.setLastAid(AID_ENTER);
java.util.concurrent.atomic.AtomicReference<byte[]> sentData = new java.util.concurrent.atomic.AtomicReference<>();
processor.setOutputCallback(sentData::set);
processor.setOutputSender(sentData::set);
byte[] rbRecord = new byte[] { (byte) CMD_RB };
processor.processRecord(rbRecord, 0, rbRecord.length, true);
@@ -335,4 +335,224 @@ public class GocaDecoderTest {
assertEquals(0x40, decoder.getCharSet());
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
}
@Test
public void testAntialiasedLineRendering() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
int red = 0xFFFF0000;
// Draw a diagonal line with Xiaolin Wu anti-aliasing
plane.drawLine(10.0, 10.0, 50.0, 30.0, red, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
int pixel = buffer[y * 100 + x];
if (pixel != 0) {
nonZeroPixels++;
int alpha = (pixel >>> 24) & 0xFF;
int r = (pixel >>> 16) & 0xFF;
assertEquals(255, r, "Red channel must be preserved");
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
}
assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line");
assertTrue(hasIntermediateAlpha, "Expected Xiaolin Wu anti-aliasing to produce fractional alpha coverage");
}
@Test
public void testSubpixelArcAndAlphaBlending() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
int green = 0xFF00FF00;
plane.drawArc(50.0, 50.0, 30.0, 30.0, 0.0, 360.0, green, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int p : buffer) {
if (p != 0) {
nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
assertTrue(nonZeroPixels > 50, "Expected arc pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased arc edges with smooth alpha");
}
@Test
public void testVectorTextAntialiasedDrawing() {
GraphicsPlane plane = new GraphicsPlane(200, 100);
plane.clear();
int yellow = 0xFFFFFF00;
plane.drawVectorText(10.0, 10.0, "8% 1985 TAX", yellow, 12.0, 20.0, GocaConstants.CD_LR, 0.0);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int p : buffer) {
if (p != 0) {
nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased vector text strokes with fractional alpha");
}
@Test
public void testTextRendererPrecisionSwitching() {
GraphicsPlane plane = new GraphicsPlane(200, 100);
GocaDecoder decoder = new GocaDecoder(plane);
final boolean[] textRendererCalled = new boolean[1];
plane.setTextRenderer((p, x, y, text, color, cw, ch, dir, angle) -> {
textRendererCalled[0] = true;
});
// Test String Precision (Default): GCHST (0xC3) with "TAX"
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GCHST);
out.write(0x07); // length = 7 (4 bytes pos + 3 bytes text)
out.write(0x00); out.write(0x00); // x = 0
out.write(0x00); out.write(0x00); // y = 0
out.write(0xE3); // 'T' in EBCDIC
out.write(0xC1); // 'A' in EBCDIC
out.write(0xE7); // 'X' in EBCDIC
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(textRendererCalled[0], "Expected pluggable TextRenderer to be called for String precision");
// Now set Stroke precision (G_GSCC with 3):
textRendererCalled[0] = false;
ByteArrayOutputStream outStroke = new ByteArrayOutputStream();
outStroke.write(GocaConstants.G_GSCC);
outStroke.write(0x01);
outStroke.write(GocaConstants.CP_STROKE); // 3
outStroke.write(GocaConstants.G_GCHST);
outStroke.write(0x07);
outStroke.write(0x00); outStroke.write(0x00);
outStroke.write(0x00); outStroke.write(0x00);
outStroke.write(0xE3); outStroke.write(0xC1); outStroke.write(0xE7);
byte[] strokeStream = outStroke.toByteArray();
decoder.decodeStream(strokeStream, 0, strokeStream.length);
assertFalse(textRendererCalled[0], "Expected drawVectorText (not textRenderer) when precision is CP_STROKE");
assertTrue(plane.hasContent());
}
@Test
public void testThickLineRendering() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
plane.drawLine(10.0, 10.0, 50.0, 50.0, 0xFF00FF00, GocaConstants.LT_SOLID, GocaConstants.LW_THICK);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int nonZero = 0;
for (int p : buffer) {
if (p != 0) nonZero++;
}
assertTrue(nonZero > 60, "Expected thick line to occupy more pixels than standard 1px line");
}
@Test
public void testShortFormAttributeOrderParsing() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form with 0x80 flag (boundary=true, fill=false),
// followed immediately by GLINE (0xC1 len=12 for 3 points) and GEAR (0x60)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GBAR);
out.write(0x80); // Short 2-byte form with 0x80 flag! Must NOT be treated as length=128!
out.write(GocaConstants.G_GLINE);
out.write(0x0C); // 3 points = 12 bytes
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x32);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x32);
out.write(0x00); out.write(0x32);
out.write(GocaConstants.G_GEAR);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected GLINE and GEAR inside GBAR short-form 0x80 to be decoded properly");
}
@Test
public void testMultiPolygonAreaFilling() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
// Polygon 1 (e.g. Letter 'T' bar): (10,10) to (30,10) to (30,20) to (10,20) to (10,10)
// 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_GBAR);
out.write(0x80);
// Polygon 1
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points = 20 bytes
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(20);
out.write(0x00); out.write(10); out.write(0x00); out.write(20);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
// Polygon 2 (disconnected start -> triggers new subpath)
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points = 20 bytes
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
out.write(0x00); out.write(22); out.write(0x00); out.write(20);
out.write(0x00); out.write(22); out.write(0x00); out.write(40);
out.write(0x00); out.write(18); out.write(0x00); out.write(40);
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
out.write(GocaConstants.G_GEAR);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int nonZero = 0;
for (int p : buffer) {
if (p != 0) nonZero++;
}
assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels");
}
}
@@ -3,6 +3,7 @@ package org.lib3270j.input;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.telnet.TelnetFSM;
import static org.junit.jupiter.api.Assertions.*;
@@ -184,7 +185,7 @@ public class InputProcessorTest {
// Let's verify DataStreamProcessor ReadModified behavior with the same buffer
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
dsp.setInputProcessor(input);
dsp.setOutputCallback(sent::set);
dsp.setOutputSender(sent::set);
byte[] rmRecord = new byte[] { (byte) CMD_RM };
dsp.processRecord(rmRecord, 0, rmRecord.length, true);