Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CodenameOne/src/com/codename1/ui/RGBImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,47 @@ protected void drawImage(Graphics g, Object nativeGraphics, int x, int y) {
g.drawRGB(rgb, 0, x, y, width, height, !opaque);
}

/// {@inheritDoc}
///
/// `RGBImage` has no native peer, so the inherited scaled-draw path
/// (`g.drawImageWH(image, ...)`) renders nothing. Instead, build a
/// translate + scale affine transform on top of the graphics context's
/// current transform and emit `drawRGB` at the image's native size --
/// the platform pipeline (iOS Metal, Android Skia, Graphics2D, ...)
/// performs the actual scaling in hardware / native code.
///
/// `Graphics.setTransform` is used (rather than `translateMatrix` +
/// `scale`) because on ports where `impl.isTranslationSupported()` is
/// false (iOS), prior `g.translate(int, int)` calls accumulate into a
/// per-Graphics integer translate that is baked into draw coordinates
/// **before** the impl matrix is applied. A naked `translateMatrix` /
/// `scale` composition would therefore multiply that accumulator by
/// the scale factor, shifting the on-screen position. `setTransform`
/// conjugates the matrix with `T(xTranslate, yTranslate)`, cancelling
/// the accumulator so the result lands at the requested coordinates
/// on every port.
@Override
protected void drawImage(Graphics g, Object nativeGraphics, int x, int y, int w, int h) {
if (w <= 0 || h <= 0) {
return;
}
if (w == width && h == height) {
g.drawRGB(rgb, 0, x, y, width, height, !opaque);
return;
}
Transform saved = Transform.makeIdentity();
g.getTransform(saved);
try {
Transform scaled = saved.copy();
scaled.translate(x, y);
scaled.scale(((float) w) / width, ((float) h) / height);
g.setTransform(scaled);
g.drawRGB(rgb, 0, 0, 0, width, height, !opaque);
} finally {
g.setTransform(saved);
}
}

/// Indicates if an image should be treated as opaque, this can improve support
/// for fast drawing of RGB images without alpha support.
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
import com.codename1.ui.TextSelection;
import com.codename1.ui.Transform;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.events.MessageEvent;
Expand Down Expand Up @@ -1293,10 +1294,34 @@ public void resetClipTracking() {

@Override
public void resetAffine(Object nativeGraphics) {
if (nativeGraphics instanceof TestGraphics) {
((TestGraphics) nativeGraphics).transform.setIdentity();
}
}

@Override
public void scale(Object nativeGraphics, float x, float y) {
if (nativeGraphics instanceof TestGraphics) {
TestTransform t = ((TestGraphics) nativeGraphics).transform;
TestTransform s = new TestTransform();
s.setScale(x, y, 1f);
t.concatenate(s);
}
}

@Override
public boolean isTranslateMatrixSupported() {
return true;
}

@Override
public void translateMatrix(Object nativeGraphics, float x, float y) {
if (nativeGraphics instanceof TestGraphics) {
TestTransform t = ((TestGraphics) nativeGraphics).transform;
TestTransform translation = new TestTransform();
translation.setTranslation(x, y, 0f);
t.concatenate(translation);
}
}


Expand Down Expand Up @@ -1371,6 +1396,11 @@ public Object makeTransformAffine(double m00, double m10, double m01, double m11
return transform;
}

@Override
public void setTransformAffine(Object nativeTransform, double m00, double m10, double m01, double m11, double m02, double m12) {
((TestTransform) nativeTransform).setAffine((float) m00, (float) m01, (float) m02, (float) m10, (float) m11, (float) m12);
}

@Override
public Object makeTransformInverse(Object nativeTransform) {
return ((TestTransform) nativeTransform).createInverse();
Expand Down Expand Up @@ -2174,8 +2204,135 @@ public void fillLinearGradient(Object graphics, int startColor, int endColor, in
public void drawImage(Object graphics, Object img, int x, int y) {
}

/**
* Rasterizes the RGB array into the destination {@link TestImage} backing the
* supplied graphics, applying the current affine transform stored on the
* {@link TestGraphics}. Used by tests that exercise scaled image draws --
* the production no-op was insufficient because pixel-level assertions need
* actual output. The mapping is inverse, nearest-neighbour, so an integer
* scale yields exact source pixel replication.
*/
@Override
public void drawRGB(Object graphics, int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) {
if (!(graphics instanceof TestGraphics) || w <= 0 || h <= 0) {
return;
}
TestGraphics g = (TestGraphics) graphics;
if (g.image == null) {
return;
}

float[] corner = new float[2];
float[] mapped = new float[2];
float minDestX = Float.POSITIVE_INFINITY, minDestY = Float.POSITIVE_INFINITY;
float maxDestX = Float.NEGATIVE_INFINITY, maxDestY = Float.NEGATIVE_INFINITY;
for (int c = 0; c < 4; c++) {
corner[0] = (c & 1) == 0 ? x : x + w;
corner[1] = (c & 2) == 0 ? y : y + h;
g.transform.transformPoint(corner, mapped);
if (mapped[0] < minDestX) minDestX = mapped[0];
if (mapped[1] < minDestY) minDestY = mapped[1];
if (mapped[0] > maxDestX) maxDestX = mapped[0];
if (mapped[1] > maxDestY) maxDestY = mapped[1];
}

int clipRight = g.clipX + Math.max(0, g.clipWidth);
int clipBottom = g.clipY + Math.max(0, g.clipHeight);
int destStartX = Math.max((int) Math.floor(minDestX), g.clipX);
int destStartY = Math.max((int) Math.floor(minDestY), g.clipY);
int destEndX = Math.min((int) Math.ceil(maxDestX), clipRight);
int destEndY = Math.min((int) Math.ceil(maxDestY), clipBottom);
if (destStartX >= destEndX || destStartY >= destEndY) {
return;
}

TestTransform inv = g.transform.createInverse();
int imgW = g.image.width;
int imgH = g.image.height;
float[] destSample = new float[2];
float[] srcSample = new float[2];
for (int dy = destStartY; dy < destEndY; dy++) {
if (dy < 0 || dy >= imgH) {
continue;
}
int rowOffset = dy * imgW;
for (int dx = destStartX; dx < destEndX; dx++) {
if (dx < 0 || dx >= imgW) {
continue;
}
destSample[0] = dx + 0.5f;
destSample[1] = dy + 0.5f;
inv.transformPoint(destSample, srcSample);
int sx = (int) Math.floor(srcSample[0]) - x;
int sy = (int) Math.floor(srcSample[1]) - y;
if (sx < 0 || sx >= w || sy < 0 || sy >= h) {
continue;
}
int srcIdx = offset + sy * w + sx;
if (srcIdx < 0 || srcIdx >= rgbData.length) {
continue;
}
int srcArgb = rgbData[srcIdx];
int dstIdx = rowOffset + dx;
if (!processAlpha || (srcArgb & 0xff000000) == 0xff000000) {
g.image.argb[dstIdx] = srcArgb;
} else {
int srcAlpha = (srcArgb >>> 24) & 0xff;
if (srcAlpha == 0) {
continue;
}
int srcR = (srcArgb >>> 16) & 0xff;
int srcG = (srcArgb >>> 8) & 0xff;
int srcB = srcArgb & 0xff;
int dstArgb = g.image.argb[dstIdx];
int dstR = (dstArgb >>> 16) & 0xff;
int dstG = (dstArgb >>> 8) & 0xff;
int dstB = dstArgb & 0xff;
int outR = (srcR * srcAlpha + dstR * (255 - srcAlpha)) / 255;
int outG = (srcG * srcAlpha + dstG * (255 - srcAlpha)) / 255;
int outB = (srcB * srcAlpha + dstB * (255 - srcAlpha)) / 255;
g.image.argb[dstIdx] = 0xff000000 | (outR << 16) | (outG << 8) | outB;
}
}
}
}

@Override
public void setTransform(Object graphics, Transform transform) {
if (!(graphics instanceof TestGraphics)) {
super.setTransform(graphics, transform);
return;
}
TestGraphics g = (TestGraphics) graphics;
if (transform == null || transform.isIdentity()) {
g.transform.setIdentity();
return;
}
Object nativeT = transform.getNativeTransform();
if (nativeT instanceof TestTransform) {
g.transform.copyFrom((TestTransform) nativeT);
} else {
g.transform.setIdentity();
}
}

@Override
public Transform getTransform(Object graphics) {
if (!(graphics instanceof TestGraphics)) {
return super.getTransform(graphics);
}
TestTransform t = ((TestGraphics) graphics).transform;
return Transform.makeAffine(t.m00, t.m10, t.m01, t.m11, t.m02, t.m12);
}

@Override
public void getTransform(Object graphics, Transform t) {
if (!(graphics instanceof TestGraphics)) {
super.getTransform(graphics, t);
return;
}
TestTransform src = ((TestGraphics) graphics).transform;
t.setAffine(src.m00, src.m10, src.m01, src.m11, src.m02, src.m12);
}

@Override
Expand Down Expand Up @@ -3785,6 +3942,7 @@ public static final class TestGraphics {
int clipHeight;
int translateX;
int translateY;
final TestTransform transform = new TestTransform();
TestFont font;
TestImage image;

Expand Down
140 changes: 140 additions & 0 deletions maven/core-unittests/src/test/java/com/codename1/ui/RGBImageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.codename1.junit.UITestBase;
import com.codename1.ui.Graphics;
import com.codename1.ui.Image;
import com.codename1.ui.Transform;

import static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -73,4 +74,143 @@ void testDrawImageAndGetRGB() {
Graphics g = canvas.getGraphics();
g.drawImage(image, 0, 0);
}

// Regression test for https://github.com/codenameone/CodenameOne/issues/4188:
// the (w, h) overload of drawImage used to fall through Image#drawImage, which
// dispatches through the (null) native peer and renders nothing. The new
// override pushes a translate + scale affine transform onto the graphics
// context and emits drawRGB at native size, so the platform pipeline applies
// the scaling.
@FormTest
void testScaledDrawImageIntegerScale() {
int red = 0xffff0000;
int green = 0xff00ff00;
int blue = 0xff0000ff;
int white = 0xffffffff;
RGBImage source = new RGBImage(new int[]{red, green, blue, white}, 2, 2);

Image canvas = Image.createImage(4, 4, 0xff000000);
Graphics g = canvas.getGraphics();
g.drawImage(source, 0, 0, 4, 4);

int[] actual = canvas.getRGB();
int[] expected = new int[]{
red, red, green, green,
red, red, green, green,
blue, blue, white, white,
blue, blue, white, white
};
assertArrayEquals(expected, actual,
"2x integer upscale of RGBImage should replicate each source pixel into a 2x2 block");
}

@FormTest
void testScaledDrawImageAtOffset() {
int red = 0xffff0000;
int green = 0xff00ff00;
int blue = 0xff0000ff;
int white = 0xffffffff;
int bg = 0xff000000;
RGBImage source = new RGBImage(new int[]{red, green, blue, white}, 2, 2);

Image canvas = Image.createImage(6, 6, bg);
Graphics g = canvas.getGraphics();
g.drawImage(source, 1, 1, 4, 4);

int[] actual = canvas.getRGB();
int[] expected = new int[]{
bg, bg, bg, bg, bg, bg,
bg, red, red, green, green, bg,
bg, red, red, green, green, bg,
bg, blue, blue, white, white, bg,
bg, blue, blue, white, white, bg,
bg, bg, bg, bg, bg, bg
};
assertArrayEquals(expected, actual,
"Scaled draw at (1,1) of a 4x4 region should land within the inner 4x4 of a 6x6 canvas");
}

@FormTest
void testScaledDrawImageDownscale() {
int red = 0xffff0000;
int green = 0xff00ff00;
int blue = 0xff0000ff;
int white = 0xffffffff;
int[] src = new int[16];
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
int color;
if (row < 2 && col < 2) color = red;
else if (row < 2) color = green;
else if (col < 2) color = blue;
else color = white;
src[row * 4 + col] = color;
}
}
RGBImage source = new RGBImage(src, 4, 4);

Image canvas = Image.createImage(2, 2, 0xff000000);
Graphics g = canvas.getGraphics();
g.drawImage(source, 0, 0, 2, 2);

int[] actual = canvas.getRGB();
int[] expected = new int[]{red, green, blue, white};
assertArrayEquals(expected, actual,
"2x integer downscale should pick one representative pixel per source quadrant");
}

// Regression test for the screen-rendering case: on ports where
// impl.isTranslationSupported() is false (iOS), g.translate(...) calls
// accumulate xTranslate/yTranslate on the Graphics object and are baked
// into drawRGB coordinates BEFORE the impl matrix is applied. A naked
// translateMatrix + scale composition would multiply that accumulator
// by the scale factor, shifting the image off-target. The fix uses
// setTransform, which conjugates the matrix with T(xT, yT) so the
// image lands at (xT + x, yT + y) regardless of the scale factor.
@FormTest
void testScaledDrawImageRespectsPriorGraphicsTranslate() {
int red = 0xffff0000;
int green = 0xff00ff00;
int blue = 0xff0000ff;
int white = 0xffffffff;
int bg = 0xff000000;
RGBImage source = new RGBImage(new int[]{red, green, blue, white}, 2, 2);

Image canvas = Image.createImage(8, 8, bg);
Graphics g = canvas.getGraphics();
g.translate(2, 2);
g.drawImage(source, 0, 0, 4, 4);

int[] actual = canvas.getRGB();
int[] expected = new int[]{
bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, red, red, green, green, bg, bg,
bg, bg, red, red, green, green, bg, bg,
bg, bg, blue, blue, white, white, bg, bg,
bg, bg, blue, blue, white, white, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg
};
assertArrayEquals(expected, actual,
"Scaled draw should land at (xTranslate + x, yTranslate + y) -- the prior g.translate " +
"must not be multiplied by the scale factor");
}

@FormTest
void testScaledDrawImagePreservesPriorTransform() {
RGBImage source = createSampleImage();

Image canvas = Image.createImage(4, 4, 0xff000000);
Graphics g = canvas.getGraphics();
Transform before = Transform.makeIdentity();
g.getTransform(before);

g.drawImage(source, 0, 0, 4, 4);

Transform after = Transform.makeIdentity();
g.getTransform(after);
assertTrue(before.equals(after),
"drawImage(w,h) must restore the graphics transform it modified");
}
}
Binary file modified scripts/android/screenshots/graphics-draw-image-rect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/ios/screenshots-metal/graphics-draw-image-rect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/ios/screenshots/graphics-draw-image-rect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading