Cleaner rendering
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m6s

This commit is contained in:
2026-08-21 11:51:37 -04:00
parent d7d2f8bad5
commit c32521fd4b
13 changed files with 516 additions and 334 deletions
@@ -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();
@@ -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];
}
}
@@ -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);
}
}
@@ -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<Integer> 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;
}
}
@@ -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.
*/
@@ -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;
}
}
}
@@ -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 };
@@ -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