diff --git a/Cargo.lock b/Cargo.lock index 023c20bb6fa6..bb3ef616f493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2539,6 +2539,8 @@ dependencies = [ "num-traits", "png", "tiff", + "zune-core 0.5.0", + "zune-jpeg 0.5.5", ] [[package]] @@ -5541,7 +5543,7 @@ dependencies = [ "half", "quick-error", "weezl", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -7293,13 +7295,28 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" + [[package]] name = "zune-jpeg" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "zune-core", + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" +dependencies = [ + "zune-core 0.5.0", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index 8fac545cec25..b338d0232d80 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -60,7 +60,7 @@ png = { version = "0.18.0", optional = true } flv-rs = { path = "../flv" } async-channel = { workspace = true } jpegxr = { git = "https://github.com/ruffle-rs/jpegxr", rev = "2a429b0d71ab416e10b73d4dbdcf34cfe2900395", optional = true } -image = { workspace = true, features = ["tiff"] } +image = { workspace = true, features = ["tiff", "png", "jpeg"] } enum-map = { workspace = true } ttf-parser = "0.25" num-bigint = "0.4" diff --git a/core/common/src/avm_string/common.rs b/core/common/src/avm_string/common.rs index 781b91c14b29..4f54e121bfb9 100644 --- a/core/common/src/avm_string/common.rs +++ b/core/common/src/avm_string/common.rs @@ -115,6 +115,7 @@ define_common_strings! { str_error: b"error", str_extension: b"extension", str_false: b"false", + str_fastCompression: b"fastCompression", str_flushed: b"flushed", str_focusEnabled: b"focusEnabled", str_fontStyle: b"fontStyle", diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index ecadcded8a00..d0d829380cd3 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -179,6 +179,8 @@ pub struct SystemClasses<'gc> { pub workerdomain: ClassObject<'gc>, pub messagechannel: ClassObject<'gc>, pub securitydomain: ClassObject<'gc>, + pub jpegencoder_options: ClassObject<'gc>, + pub pngencoder_options: ClassObject<'gc>, } #[derive(Clone, Collect)] @@ -357,6 +359,8 @@ impl<'gc> SystemClasses<'gc> { workerdomain: object, messagechannel: object, securitydomain: object, + jpegencoder_options: object, + pngencoder_options: object, } } } @@ -696,6 +700,8 @@ pub fn init_native_system_classes(activation: &mut Activation<'_, '_>) { ("flash.display", "Sprite", sprite), ("flash.display", "Stage", stage), ("flash.display", "Stage3D", stage3d), + ("flash.display", "JPEGEncoderOptions", jpegencoder_options), + ("flash.display", "PNGEncoderOptions", pngencoder_options), ("flash.display3D", "Context3D", context3d), ("flash.display3D", "IndexBuffer3D", indexbuffer3d), ("flash.display3D", "Program3D", program3d), diff --git a/core/src/avm2/globals/flash/display/BitmapData.as b/core/src/avm2/globals/flash/display/BitmapData.as index 952cc8f9f65b..224819632a65 100644 --- a/core/src/avm2/globals/flash/display/BitmapData.as +++ b/core/src/avm2/globals/flash/display/BitmapData.as @@ -208,9 +208,6 @@ package flash.display { } [API("680")] - public function encode(rect:Rectangle, compressor:Object, byteArray:ByteArray = null):ByteArray { - stub_method("flash.display.BitmapData", "encode"); - return null; - } + public native function encode(rect:Rectangle, compressor:Object, byteArray:ByteArray = null):ByteArray; } } diff --git a/core/src/avm2/globals/flash/display/bitmap_data.rs b/core/src/avm2/globals/flash/display/bitmap_data.rs index fa1a6f59d731..95d58f84bf7c 100644 --- a/core/src/avm2/globals/flash/display/bitmap_data.rs +++ b/core/src/avm2/globals/flash/display/bitmap_data.rs @@ -23,6 +23,10 @@ use crate::bitmap::{is_size_valid, operations}; use crate::character::{Character, CompressedBitmap}; use crate::ecma_conversions::round_to_even; use crate::swf::BlendMode; +use image::ImageEncoder; +use ruffle_macros::istr; +use ruffle_render::backend::RenderBackend; +use ruffle_render::bitmap::PixelRegion; use ruffle_render::filters::Filter; use ruffle_render::transform::Transform; use std::str::FromStr; @@ -1623,3 +1627,156 @@ pub fn merge<'gc>( Ok(Value::Undefined) } + +/// Implements `BitmapData.encode` +pub fn encode<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.as_object().unwrap(); + + if let Some(bitmap_data) = this.as_bitmap_data() { + bitmap_data.check_valid(activation)?; + + let rectangle = args.get_object(activation, 0, "rect")?; + let (x, y, width, height) = get_rectangle_x_y_width_height(activation, rectangle)?; + + let compressor = args.get_object(activation, 1, "compressor")?; + + // Get or create the output ByteArray + let output_bytearray = if let Some(ba) = args.try_get_object(2) { + ba.as_bytearray_object().unwrap() + } else { + let storage = ByteArrayStorage::new(activation.context); + ByteArrayObject::from_storage(activation.context, storage) + }; + + let jpeg_encoder_class = activation.avm2().classes().jpegencoder_options; + let png_encoder_class = activation.avm2().classes().pngencoder_options; + + let options = if compressor.is_of_type(jpeg_encoder_class.inner_class_definition()) { + let quality = Value::from(compressor) + .get_public_property(istr!("quality"), activation)? + .coerce_to_u32(activation)? + .clamp(1, 100) as u8; + + EncodeOptions::Jpeg { quality } + } else if compressor.is_of_type(png_encoder_class.inner_class_definition()) { + let fast_compression = Value::from(compressor) + .get_public_property(istr!("fastCompression"), activation)? + .coerce_to_boolean(); + + EncodeOptions::Png { fast_compression } + } else { + avm2_stub_method!( + activation, + "flash.display.BitmapData", + "encode", + "with JPEGXREncoderOptions" + ); + return Ok(Value::Null); + }; + + encode_internal( + bitmap_data, + activation.context.renderer, + x, + y, + width, + height, + options, + &mut output_bytearray.storage_mut(), + ) + .unwrap(); + + return Ok(output_bytearray.into()); + } + + Ok(Value::Null) +} + +enum EncodeOptions { + Jpeg { quality: u8 }, + Png { fast_compression: bool }, +} + +#[expect(clippy::too_many_arguments)] +fn encode_internal( + bitmap: BitmapData, + renderer: &mut dyn RenderBackend, + x: i32, + y: i32, + width: i32, + height: i32, + options: EncodeOptions, + bytearray: &mut ByteArrayStorage, +) -> Result<(), image::ImageError> { + let mut region = PixelRegion::for_region_i32(x, y, width, height); + + region.clamp(bitmap.width(), bitmap.height()); + + if region.width() == 0 || region.height() == 0 { + return Ok(()); + } + + let read = bitmap.read_area(region, renderer); + + match options { + EncodeOptions::Jpeg { quality } => { + let mut rgb_data = Vec::with_capacity((region.width() * region.height() * 3) as usize); + + for y in region.y_min..region.y_max { + for x in region.x_min..region.x_max { + let color = read.get_pixel32_raw(x, y).to_un_multiplied_alpha(); + rgb_data.push(color.red()); + rgb_data.push(color.green()); + rgb_data.push(color.blue()); + } + } + + let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(bytearray, quality); + + encoder.write_image( + &rgb_data, + region.width(), + region.height(), + image::ExtendedColorType::Rgb8, + )?; + } + EncodeOptions::Png { fast_compression } => { + let mut rgba_data = Vec::with_capacity((region.width() * region.height() * 4) as usize); + + for y in region.y_min..region.y_max { + for x in region.x_min..region.x_max { + let color = read.get_pixel32_raw(x, y).to_un_multiplied_alpha(); + rgba_data.push(color.red()); + rgba_data.push(color.green()); + rgba_data.push(color.blue()); + rgba_data.push(color.alpha()); + } + } + + let compression = if fast_compression { + image::codecs::png::CompressionType::Fast + } else { + image::codecs::png::CompressionType::Default + }; + + let encoder = image::codecs::png::PngEncoder::new_with_quality( + bytearray, + compression, + image::codecs::png::FilterType::Adaptive, + ); + + encoder.write_image( + &rgba_data, + region.width(), + region.height(), + image::ExtendedColorType::Rgba8, + )?; + } + }; + + Ok(()) +}