core: Always check mergeAlpha in BitmapData.copyPixels

Previously, we would only use mergeAlpha if alphaBitmapData
and alphaPoint. However, mergeAlpha can be used even when
those parameters are null.

Some of the AVM1 argument handling was also incorrect - I've fixed
it, and extended the existing test with the output-based test
added for AVM2.
This commit is contained in:
Aaron Hill 2022-08-26 13:11:43 -05:00
parent 1d6b3b6f57
commit 81a5f3f10a
16 changed files with 382 additions and 72 deletions

View File

@ -789,6 +789,16 @@ pub fn copy_pixels<'gc>(
if let Some(src_bitmap) = source_bitmap.as_bitmap_data_object() {
if !src_bitmap.disposed() {
let merge_alpha = if args.len() >= 6 {
Some(
args.get(5)
.unwrap_or(&Value::Undefined)
.as_bool(activation.swf_version()),
)
} else {
None
};
// dealing with object aliasing...
let src_bitmap_clone: BitmapData; // only initialized if source is the same object as self
let src_bitmap_data_cell = src_bitmap.bitmap_data();
@ -802,62 +812,53 @@ pub fn copy_pixels<'gc>(
&src_bitmap_gc_ref
};
if args.len() >= 5 {
let alpha_point = args
.get(4)
.unwrap_or(&Value::Undefined)
.coerce_to_object(activation);
let alpha_bitmap = args
.get(3)
.unwrap_or(&Value::Undefined)
.coerce_to_object(activation);
let alpha_x = alpha_point
.get("x", activation)?
.coerce_to_f64(activation)?
as i32;
if let Some(alpha_bitmap) = alpha_bitmap.as_bitmap_data_object() {
if !alpha_bitmap.disposed() {
let alpha_point = args
.get(4)
.unwrap_or(&Value::Undefined)
.coerce_to_object(activation);
let alpha_y = alpha_point
.get("y", activation)?
.coerce_to_f64(activation)?
as i32;
let alpha_x = alpha_point
.get("x", activation)?
.coerce_to_f64(activation)?
as i32;
let alpha_bitmap = args
.get(3)
.unwrap_or(&Value::Undefined)
.coerce_to_object(activation);
let alpha_y = alpha_point
.get("y", activation)?
.coerce_to_f64(activation)?
as i32;
if let Some(alpha_bitmap) = alpha_bitmap.as_bitmap_data_object() {
if !alpha_bitmap.disposed() {
// dealing with aliasing the same way as for the source
let alpha_bitmap_clone: BitmapData;
let alpha_bitmap_data_cell = alpha_bitmap.bitmap_data();
let alpha_bitmap_gc_ref;
let alpha_bitmap_ref = if GcCell::ptr_eq(
alpha_bitmap.bitmap_data(),
bitmap_data.bitmap_data(),
) {
alpha_bitmap_clone = alpha_bitmap_data_cell.read().clone();
&alpha_bitmap_clone
} else {
alpha_bitmap_gc_ref = alpha_bitmap_data_cell.read();
&alpha_bitmap_gc_ref
};
// dealing with aliasing the same way as for the source
let alpha_bitmap_clone: BitmapData;
let alpha_bitmap_data_cell = alpha_bitmap.bitmap_data();
let alpha_bitmap_gc_ref;
let alpha_bitmap_ref = if GcCell::ptr_eq(
alpha_bitmap.bitmap_data(),
bitmap_data.bitmap_data(),
) {
alpha_bitmap_clone = alpha_bitmap_data_cell.read().clone();
&alpha_bitmap_clone
} else {
alpha_bitmap_gc_ref = alpha_bitmap_data_cell.read();
&alpha_bitmap_gc_ref
};
let merge_alpha = if args.len() >= 6 {
args.get(5)
.unwrap_or(&Value::Undefined)
.as_bool(activation.swf_version())
} else {
true
};
bitmap_data
.bitmap_data()
.write(activation.context.gc_context)
.copy_pixels(
source_bitmap_ref,
(src_min_x, src_min_y, src_width, src_height),
(dest_x, dest_y),
Some((alpha_bitmap_ref, (alpha_x, alpha_y), merge_alpha)),
);
}
bitmap_data
.bitmap_data()
.write(activation.context.gc_context)
.copy_pixels(
source_bitmap_ref,
(src_min_x, src_min_y, src_width, src_height),
(dest_x, dest_y),
Some((alpha_bitmap_ref, (alpha_x, alpha_y))),
merge_alpha.unwrap_or(true),
);
}
} else {
bitmap_data
@ -868,6 +869,9 @@ pub fn copy_pixels<'gc>(
(src_min_x, src_min_y, src_width, src_height),
(dest_x, dest_y),
None,
// Despite what the docs claim, mergeAlpa appears to be treated as 'false'
// when no 'alphaBitmap' is specified (e.g. only 3 args are passed)
merge_alpha.unwrap_or(false),
);
}
}

View File

@ -254,18 +254,62 @@ pub fn copy_pixels<'gc>(
&src_bitmap_gc_ref
};
let mut alpha_source = None;
if args.len() >= 4 {
log::warn!("BitmapData.copyPixels - alpha not implemented");
if let Some(alpha_bitmap) = args
.get(3)
.and_then(|o| o.as_object())
.and_then(|o| o.as_bitmap_data())
{
// Testing shows that a null/undefined 'alphaPoint' parameter is treated
// as 'new Point(0, 0)'
let mut x = 0;
let mut y = 0;
if let Ok(alpha_point) = args
.get(4)
.unwrap_or(&Value::Undefined)
.coerce_to_object(activation)
{
x = alpha_point
.get_property(&Multiname::public("x"), activation)?
.coerce_to_i32(activation)?;
y = alpha_point
.get_property(&Multiname::public("y"), activation)?
.coerce_to_i32(activation)?;
}
alpha_source = Some((alpha_bitmap, (x, y)));
}
}
bitmap_data
.write(activation.context.gc_context)
.copy_pixels(
source_bitmap_ref,
(src_min_x, src_min_y, src_width, src_height),
(dest_x, dest_y),
None,
);
let merge_alpha = args
.get(5)
.unwrap_or(&Value::Bool(false))
.coerce_to_boolean();
if let Some((alpha_bitmap, alpha_point)) = alpha_source {
bitmap_data
.write(activation.context.gc_context)
.copy_pixels(
source_bitmap_ref,
(src_min_x, src_min_y, src_width, src_height),
(dest_x, dest_y),
Some((&*alpha_bitmap.read(), alpha_point)),
merge_alpha,
);
} else {
bitmap_data
.write(activation.context.gc_context)
.copy_pixels(
source_bitmap_ref,
(src_min_x, src_min_y, src_width, src_height),
(dest_x, dest_y),
None,
merge_alpha,
);
}
}
}

View File

@ -559,7 +559,8 @@ impl<'gc> BitmapData<'gc> {
source_bitmap: &Self,
src_rect: (i32, i32, i32, i32),
dest_point: (i32, i32),
alpha_source: Option<(&Self, (i32, i32), bool)>,
alpha_source: Option<(&Self, (i32, i32))>,
merge_alpha: bool,
) {
let (src_min_x, src_min_y, src_width, src_height) = src_rect;
let (dest_min_x, dest_min_y) = dest_point;
@ -581,8 +582,7 @@ impl<'gc> BitmapData<'gc> {
let mut dest_color = self.get_pixel_raw(dest_x as u32, dest_y as u32).unwrap();
if let Some((alpha_bitmap, (alpha_min_x, alpha_min_y), merge_alpha)) = alpha_source
{
if let Some((alpha_bitmap, (alpha_min_x, alpha_min_y))) = alpha_source {
let alpha_x = src_x - src_min_x + alpha_min_x;
let alpha_y = src_y - src_min_y + alpha_min_y;
@ -627,11 +627,16 @@ impl<'gc> BitmapData<'gc> {
intermediate_color
};
} else {
dest_color = if source_bitmap.transparency && !self.transparency {
dest_color.blend_over(&source_color)
} else {
source_color
};
dest_color =
if (source_bitmap.transparency && !self.transparency) || merge_alpha {
dest_color.blend_over(&source_color)
} else {
source_color
};
if !self.transparency {
dest_color = dest_color.with_alpha(0xFF)
}
}
self.set_pixel32_raw(dest_x as u32, dest_y as u32, dest_color);

View File

@ -189,6 +189,7 @@ swf_tests! {
(as3_astypelate, "avm2/astypelate", 1),
(as3_bitand, "avm2/bitand", 1),
(as3_bitmap_constr, "avm2/bitmap_constr", 1),
(as3_bitmapdata_copypixels, "avm2/bitmapdata_copypixels", 2, img = true),
#[ignore] (as3_bitmap_properties, "avm2/bitmap_properties", 1),
(as3_bitmap_timeline, "avm2/bitmap_timeline", 1),
(as3_bitmapdata_constr, "avm2/bitmapdata_constr", 1),
@ -1328,6 +1329,7 @@ fn test_swf_approx(
expected_captures.len(),
"Differing numbers of regex captures"
);
// Each capture group (other than group 0, which is always the entire regex
// match) represents a floating-point value
for (actual_val, expected_val) in actual_captures

View File

@ -1,7 +1,8 @@
import flash.display.BitmapData;
import flash.display.BitmapData;
import flash.geom.Rectangle;
import flash.geom.Point;
class BitmapCopyPixels {
static function plop(mc : MovieClip, sx : Number, sy : Number,
@ -27,7 +28,7 @@ class BitmapCopyPixels {
src_img._x = sx + 110;
src_img._y = sy + 20;
dest.copyPixels(src, new Rectangle(0,0,80,20), new Point(10, 10));
dest.copyPixels(src, new Rectangle(0,0,80,20), new Point(10, 10));
var alpha:BitmapData = new BitmapData(40, 20, transp_alpha, 0x66884422);
@ -41,7 +42,71 @@ class BitmapCopyPixels {
dest.copyPixels(src, new Rectangle(0,0,80,20), new Point(10, 50), alpha, new Point(0,0), merge);
}
static function main(mc) {
public static function test(mc) {
// These values are straight alpha.
// BitmapData internally converts to premultiplied alpha,
// and converts back to straight alpha in 'getPixel'. This results
// in rounding, which Ruffle currently doesn't match. These values
// were chosen to produce the same rounded result in Flash and Ruffle.
// FIXME - determine the correct roudning behavior to use for Ruffle,
// and test that a 'getPixel/setPixel' round-trip always agrees between
// Flash and Ruffle.
var target = new BitmapData(5, 5, true, 0x45112233);
var source = new BitmapData(5, 5, true, 0x22445566);
var otherSource = new BitmapData(5, 5, true, 0x80aabbcc);
target.copyPixels(source, new Rectangle(0, 0, 2, 2), new Point(0, 0), null, null, true);
target.copyPixels(otherSource, new Rectangle(0, 0, 1, 1), new Point(4, 4), null, null, false);
for (var py = 0; py < target.height; py++) {
var line = "";
for (var px = 0; px < target.height; px++) {
line += target.getPixel32(px, py).toString(16) + " ";
}
trace(line);
}
var transparent = new BitmapData(1, 1, false, 0x80FFFFFF);
var nonTransparent = new BitmapData(1, 1, false, 0x0);
var transparentSource = new BitmapData(1, 1, true, 0xF00E0E0E);
trace("transparentSource: " + transparentSource.getPixel32(0, 0).toString(16));
trace("Non-transparent testing");
var nonTransparentSource = new BitmapData(1, 1, false, 0x80020406);
trace("Original pixel: " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
trace("transparent source mergeAlpha=false " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
trace("transparent source mergeAlpha=true " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
trace("nontransparent source mergeAlpha=false " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
trace("nontransparent source mergeAlpha=true " + nonTransparent.getPixel32(0, 0).toString(16));
trace("");
trace("Transparent testing");
trace("Original pixel: " + transparent.getPixel32(0, 0).toString(16));
// FIXME - enable these when Ruffle rounding is correct
//transparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
//trace("transparent source mergeAlpha=false " + transparent.getPixel32(0, 0).toString(16));
//transparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
//trace("transparent source mergeAlpha=true " + transparent.getPixel32(0, 0).toString(16));
transparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
trace("nontransparent source mergeAlpha=false " + transparent.getPixel32(0, 0).toString(16));
transparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
trace("nontransparent source mergeAlpha=true " + transparent.getPixel32(0, 0).toString(16));
BitmapCopyPixels.plop(mc, 10, 20, true, true, true , false);
BitmapCopyPixels.plop(mc, 210, 20, true, true, false , false);
BitmapCopyPixels.plop(mc, 410, 20, true, false, true , false);
@ -63,4 +128,4 @@ class BitmapCopyPixels {
BitmapCopyPixels.plop(mc, 410, 720, false, false, true , true);
BitmapCopyPixels.plop(mc, 610, 720, false, false, false , true);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,17 @@
5d243147 5d243147 45122134 45122134 45122134
5d243147 5d243147 45122134 45122134 45122134
45122134 45122134 45122134 45122134 45122134
45122134 45122134 45122134 45122134 45122134
45122134 45122134 45122134 45122134 -7f564435
transparentSource: -ff1f1f2
Non-transparent testing
Original pixel: -1000000
transparent source mergeAlpha=false -f2f2f3
transparent source mergeAlpha=true -f2f2f3
nontransparent source mergeAlpha=false -fdfbfa
nontransparent source mergeAlpha=true -fdfbfa
Transparent testing
Original pixel: -1
nontransparent source mergeAlpha=false -fdfbfa
nontransparent source mergeAlpha=true -fdfbfa

Binary file not shown.

View File

@ -0,0 +1,150 @@
package {
import flash.display.BitmapData;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.display.Bitmap;
import flash.display.Stage;
public class Test {
public function Test(stage: Stage) {
// These values are straight alpha.
// BitmapData internally converts to premultiplied alpha,
// and converts back to straight alpha in 'getPixel'. This results
// in rounding, which Ruffle currently doesn't match. These values
// were chosen to produce the same rounded result in Flash and Ruffle.
// FIXME - determine the correct roudning behavior to use for Ruffle,
// and test that a 'getPixel/setPixel' round-trip always agrees between
// Flash and Ruffle.
var target = new BitmapData(5, 5, true, 0x45112233);
var source = new BitmapData(5, 5, true, 0x22445566);
var otherSource = new BitmapData(5, 5, true, 0x80aabbcc);
target.copyPixels(source, new Rectangle(0, 0, 2, 2), new Point(0, 0), null, null, true);
target.copyPixels(otherSource, new Rectangle(0, 0, 1, 1), new Point(4, 4), null, null, false);
printImage(target);
var transparent = new BitmapData(1, 1, false, 0x80FFFFFF);
var nonTransparent = new BitmapData(1, 1, false, 0x0);
var transparentSource = new BitmapData(1, 1, true, 0xF00E0E0E);
trace("transparentSource: " + transparentSource.getPixel32(0, 0).toString(16));
trace("Non-transparent testing");
var nonTransparentSource = new BitmapData(1, 1, false, 0x80020406);
trace("Original pixel: " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
trace("transparent source mergeAlpha=false " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
trace("transparent source mergeAlpha=true " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
trace("nontransparent source mergeAlpha=false " + nonTransparent.getPixel32(0, 0).toString(16));
nonTransparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
trace("nontransparent source mergeAlpha=true " + nonTransparent.getPixel32(0, 0).toString(16));
trace();
trace("Transparent testing");
trace("Original pixel: " + transparent.getPixel32(0, 0).toString(16));
// FIXME - enable these when Ruffle rounding is correct
//transparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
//trace("transparent source mergeAlpha=false " + transparent.getPixel32(0, 0).toString(16));
//transparent.copyPixels(transparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
//trace("transparent source mergeAlpha=true " + transparent.getPixel32(0, 0).toString(16));
transparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, false);
trace("nontransparent source mergeAlpha=false " + transparent.getPixel32(0, 0).toString(16));
transparent.copyPixels(nonTransparentSource, new Rectangle(0, 0, 1, 1), new Point(0, 0), null, null, true);
trace("nontransparent source mergeAlpha=true " + transparent.getPixel32(0, 0).toString(16));
plop(stage, 10, 20, true, true, true , false);
plop(stage, 210, 20, true, true, false , false);
plop(stage, 410, 20, true, false, true , false);
plop(stage, 610, 20, true, false, false , false);
plop(stage, 10, 220, false, true, true , false);
plop(stage, 210, 220, false, true, false , false);
plop(stage, 410, 220, false, false, true , false);
plop(stage, 610, 220, false, false, false , false);
plop(stage, 10, 520, true, true, true , true);
plop(stage, 210, 520, true, true, false , true);
plop(stage, 410, 520, true, false, true , true);
plop(stage, 610, 520, true, false, false , true);
plop(stage, 10, 720, false, true, true , true);
plop(stage, 210, 720, false, true, false , true);
plop(stage, 410, 720, false, false, true , true);
plop(stage, 610, 720, false, false, false , true);
var weirdDest = new BitmapData(5, 5, true, 0);
var weirdSource = new BitmapData(5, 5, false, 0x800000);
var weirdAlphaSource = new BitmapData(5, 5, true, 0xff112233);
weirdAlphaSource.fillRect(new Rectangle(3, 3, 2, 2), 0xaabbccdd);
trace("Copy with alphaPoint=undefined:");
// Specifying an alphaPoint of 'undefined' should use 'new Point(0, 0)''
weirdDest.copyPixels(weirdSource, new Rectangle(0, 0, 2, 2), new Point(0, 0), weirdAlphaSource, undefined, true);
printImage(weirdDest);
}
static function printImage(target:BitmapData) {
for (var py = 0; py < target.height; py++) {
var line = "";
for (var px = 0; px < target.height; px++) {
line += target.getPixel32(px, py).toString(16) + " ";
}
trace(line);
}
}
static function plop(stage: Stage, sx : Number, sy : Number,
transp_src : Boolean, transp_dest : Boolean, transp_alpha : Boolean,
merge: Boolean) {
var dest:BitmapData = new BitmapData(100, 100, transp_dest, 0xBBFF0000);
dest.fillRect(new Rectangle(20,10, 20, 80), 0x8888FF00);
dest.fillRect(new Rectangle(50,10, 20, 80), 0x220088FF);
var dest_img = new Bitmap(dest);
dest_img.x = sx;
dest_img.y = sy;
var src:BitmapData = new BitmapData(80, 20, transp_src, 0x44888888);
var src_img = new Bitmap(src);
src.fillRect(new Rectangle(5, 5, 10, 10), 0xAA2288DD);
src_img.x = sx + 110;
src_img.y = sy + 20;
dest.copyPixels(src, new Rectangle(0,0,80,20), new Point(10, 10));
var alpha:BitmapData = new BitmapData(40, 20, transp_alpha, 0x66884422);
var alpha_img = new Bitmap(alpha);
alpha.fillRect(new Rectangle(4, 8, 12, 30), 0xEE8844DD);
alpha_img.x = sx + 110;
alpha_img.y = sy + 60;
dest.copyPixels(src, new Rectangle(0,0,80,20), new Point(10, 50), alpha, new Point(0,0), merge);
stage.addChild(dest_img);
stage.addChild(src_img);
stage.addChild(alpha_img);
}
}
}

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,23 @@
5d243147 5d243147 45122134 45122134 45122134
5d243147 5d243147 45122134 45122134 45122134
45122134 45122134 45122134 45122134 45122134
45122134 45122134 45122134 45122134 45122134
45122134 45122134 45122134 45122134 80a9bbcb
transparentSource: f00e0e0e
Non-transparent testing
Original pixel: ff000000
transparent source mergeAlpha=false ff0d0d0d
transparent source mergeAlpha=true ff0d0d0d
nontransparent source mergeAlpha=false ff020406
nontransparent source mergeAlpha=true ff020406
Transparent testing
Original pixel: ffffffff
nontransparent source mergeAlpha=false ff020406
nontransparent source mergeAlpha=true ff020406
Copy with alphaPoint=undefined:
ff800000 ff800000 0 0 0
ff800000 ff800000 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0

Binary file not shown.

Binary file not shown.