refactor(mobile): move the native core decoders into a codecs crate (#32765)
* refactor(mobile): move the native core decoders into a codecs crate * make Format private * rename the decode export and options for what they hold * drop the unused ios and server groups and rename the target params (cherry picked from commit 11bf875313782b8a9e10269bb6f85226b7bdcd72)
This commit is contained in:
Generated
+13
-6
@@ -56,6 +56,18 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "codecs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"jxl",
|
||||
"libc",
|
||||
"moxcms",
|
||||
"pic-scale",
|
||||
"quick_cache",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.8"
|
||||
@@ -287,12 +299,7 @@ dependencies = [
|
||||
name = "native_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"jxl",
|
||||
"libc",
|
||||
"moxcms",
|
||||
"pic-scale",
|
||||
"quick_cache",
|
||||
"rayon",
|
||||
"codecs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/native_core", "crates/native_core_ffi"]
|
||||
members = ["crates/codecs", "crates/native_core", "crates/native_core_ffi"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
|
||||
+3
-2
@@ -5,8 +5,9 @@ Rust code that ships inside the mobile app. Flutter compiles it from source thro
|
||||
in `mobile/` sets one up, otherwise install [rustup](https://rustup.rs) once and the build
|
||||
fetches the pinned toolchain itself.
|
||||
|
||||
- `crates/native_core`: the shared logic
|
||||
- `crates/native_core_ffi`: the C ABI, cbindgen writes `include/native_core.h`
|
||||
- `crates/codecs`: the image decoders, a feature each, behind one `decode`
|
||||
- `crates/native_core`: the shared logic, a feature per part and a group per target
|
||||
- `crates/native_core_ffi`: the C ABI on the `flutter` group, cbindgen writes `include/native_core.h`
|
||||
- `native_core`: Flutter package with the build hook and the ffigen bindings
|
||||
(`NOTICES` carries the licenses of the crates compiled into the core, update it with the dependencies)
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "codecs"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["jxl"]
|
||||
jxl = ["dep:jxl", "dep:rayon"]
|
||||
|
||||
[dependencies]
|
||||
jxl = { workspace = true, optional = true }
|
||||
libc = { workspace = true }
|
||||
moxcms = { workspace = true }
|
||||
pic-scale = { workspace = true }
|
||||
quick_cache = { workspace = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -70,7 +70,7 @@ impl JxlParallelRunner for Cancellable<'_> {
|
||||
}
|
||||
|
||||
impl<'a> Jxl<'a> {
|
||||
pub fn open(mut input: &'a [u8], target: Target) -> Option<Self> {
|
||||
pub fn open(mut input: &'a [u8], color_space: Target) -> Option<Self> {
|
||||
let mut options = JxlDecoderOptions::default();
|
||||
// Colour transforms need straight alpha; premultiplying happens last.
|
||||
options.alpha_mode = AlphaMode::Unpremultiplied;
|
||||
@@ -85,7 +85,7 @@ impl<'a> Jxl<'a> {
|
||||
.ok()?;
|
||||
let profile = decoder.output_color_profile()?;
|
||||
if let Some(cicp) = cicp(profile)
|
||||
&& (target == Target::Source || color::is_srgb(cicp))
|
||||
&& (color_space == Target::Source || color::is_srgb(cicp))
|
||||
{
|
||||
if color::is_hdr(cicp) {
|
||||
decoder
|
||||
@@ -121,11 +121,11 @@ impl<'a> Jxl<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes the first frame unless cancelled, downscaled to cover `target` pixels and to fit
|
||||
/// within `max_pixel_size`.
|
||||
/// Decodes the first frame unless cancelled, downscaled to cover `target_size` pixels and to
|
||||
/// fit within `max_pixel_size`.
|
||||
pub fn decode(
|
||||
mut self,
|
||||
target: Option<(usize, usize)>,
|
||||
target_size: Option<(usize, usize)>,
|
||||
max_pixel_size: usize,
|
||||
cancelled: &AtomicBool,
|
||||
) -> Option<Image> {
|
||||
@@ -139,7 +139,7 @@ impl<'a> Jxl<'a> {
|
||||
let pixel_bytes =
|
||||
format.color_type.samples_per_pixel() * format.color_data_format?.bytes_per_sample();
|
||||
let stride = w.checked_mul(pixel_bytes)?;
|
||||
let size = resize::cover((w, h), target, max_pixel_size);
|
||||
let size = resize::cover((w, h), target_size, max_pixel_size);
|
||||
let samples = if color::is_hdr(self.cicp) {
|
||||
let mut rgba = Pixels::zeroed(w, h, pixel_bytes)?;
|
||||
self.decode_frame(&mut rgba, stride, cancelled)?;
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use color::Target;
|
||||
use image::Image;
|
||||
|
||||
pub mod color;
|
||||
pub mod image;
|
||||
#[cfg(feature = "jxl")]
|
||||
pub mod jxl;
|
||||
mod resize;
|
||||
|
||||
enum Format {
|
||||
#[cfg(feature = "jxl")]
|
||||
Jxl,
|
||||
}
|
||||
|
||||
pub struct Options {
|
||||
pub color_space: Target,
|
||||
|
||||
/// Downscales to the smallest size that still fills this.
|
||||
pub target_size: Option<(usize, usize)>,
|
||||
|
||||
/// Downscales to fit within this many pixels per side.
|
||||
pub max_pixel_size: usize,
|
||||
}
|
||||
|
||||
fn probe(data: &[u8]) -> Option<Format> {
|
||||
#[cfg(feature = "jxl")]
|
||||
if matches!(
|
||||
::jxl::api::check_signature(data),
|
||||
::jxl::api::JxlSignature::Codestream | ::jxl::api::JxlSignature::Container
|
||||
) {
|
||||
return Some(Format::Jxl);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Decodes the first frame of `data` unless cancelled.
|
||||
pub fn decode(data: &[u8], options: &Options, cancelled: &AtomicBool) -> Option<Image> {
|
||||
match probe(data)? {
|
||||
#[cfg(feature = "jxl")]
|
||||
Format::Jxl => jxl::Jxl::open(data, options.color_space)?.decode(
|
||||
options.target_size,
|
||||
options.max_pixel_size,
|
||||
cancelled,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -47,16 +47,16 @@ impl Lanczos for u16 {
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of `size` scaled down to cover `target` and to fit within `max_pixel_size`, if that
|
||||
/// is smaller.
|
||||
/// The size of `size` scaled down to cover `target_size` and to fit within `max_pixel_size`, if
|
||||
/// that is smaller.
|
||||
pub(crate) fn cover(
|
||||
(w, h): (usize, usize),
|
||||
target: Option<(usize, usize)>,
|
||||
target_size: Option<(usize, usize)>,
|
||||
max_pixel_size: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
// Scales as side ratios, so the side that sets the scale lands exactly on its limit.
|
||||
let fit = (max_pixel_size, w.max(h));
|
||||
let (num, den) = match target {
|
||||
let (num, den) = match target_size {
|
||||
Some((tw, th)) => {
|
||||
let cover = if tw * h >= th * w { (tw, w) } else { (th, h) };
|
||||
if cover.0 * fit.1 < fit.0 * cover.1 {
|
||||
Vendored
Vendored
Vendored
@@ -1,11 +1,19 @@
|
||||
#![cfg(feature = "jxl")]
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use native_core::color::{CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics};
|
||||
use native_core::image::{Image, Pixels, Samples};
|
||||
use native_core::jxl::Jxl;
|
||||
use codecs::Options;
|
||||
use codecs::color::{CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics};
|
||||
use codecs::image::{Image, Pixels, Samples};
|
||||
use codecs::jxl::Jxl;
|
||||
|
||||
fn decode(data: &[u8], target: Target) -> Option<Image> {
|
||||
Jxl::open(data, target)?.decode(None, 16384, &AtomicBool::new(false))
|
||||
fn decode(data: &[u8], color_space: Target) -> Option<Image> {
|
||||
let options = Options {
|
||||
color_space,
|
||||
target_size: None,
|
||||
max_pixel_size: 16384,
|
||||
};
|
||||
codecs::decode(data, &options, &AtomicBool::new(false))
|
||||
}
|
||||
|
||||
fn decode_srgb(data: &[u8]) -> Option<Pixels<u8>> {
|
||||
@@ -4,13 +4,16 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
convert = []
|
||||
rotate = []
|
||||
thumbhash = []
|
||||
jxl = ["codecs/jxl"]
|
||||
# One group per target.
|
||||
flutter = ["convert", "rotate", "thumbhash", "jxl"]
|
||||
|
||||
[dependencies]
|
||||
jxl = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
moxcms = { workspace = true }
|
||||
pic-scale = { workspace = true }
|
||||
quick_cache = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
codecs = { path = "../codecs", default-features = false, optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
pub mod color;
|
||||
#[cfg(feature = "codecs")]
|
||||
pub use codecs;
|
||||
#[cfg(feature = "convert")]
|
||||
pub mod convert;
|
||||
pub mod image;
|
||||
pub mod jxl;
|
||||
mod resize;
|
||||
#[cfg(feature = "rotate")]
|
||||
pub mod rotate;
|
||||
#[cfg(feature = "thumbhash")]
|
||||
pub mod thumbhash;
|
||||
|
||||
pub fn version() -> &'static str {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "convert")]
|
||||
|
||||
use native_core::convert::convert_1010102;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "rotate")]
|
||||
|
||||
use native_core::rotate::{Orientation, rotate};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "thumbhash")]
|
||||
|
||||
use native_core::thumbhash::decode;
|
||||
|
||||
// Expected pixels come from the old Swift decoder patched to round like the Java did (it truncated),
|
||||
|
||||
@@ -10,7 +10,7 @@ name = "native_core_ffi"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
native_core = { path = "../native_core" }
|
||||
native_core = { path = "../native_core", features = ["flutter"] }
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
fn main() {
|
||||
// Files that define exported items. A directory here would make Flutter rerun the hook every build.
|
||||
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||
println!("cargo:rerun-if-changed=src/jxl.rs");
|
||||
println!("cargo:rerun-if-changed=src/decode.rs");
|
||||
println!("cargo:rerun-if-changed=src/thumbhash.rs");
|
||||
println!("cargo:rerun-if-changed=cbindgen.toml");
|
||||
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
|
||||
+17
-14
@@ -10,9 +10,9 @@ use jni::{
|
||||
objects::{JByteBuffer, JClass, JIntArray},
|
||||
sys::{jint, jlong},
|
||||
};
|
||||
use native_core::color::Target;
|
||||
use native_core::image::{Image, Samples};
|
||||
use native_core::jxl::Jxl;
|
||||
use native_core::codecs::color::Target;
|
||||
use native_core::codecs::image::{Image, Samples};
|
||||
use native_core::codecs::{self, Options};
|
||||
|
||||
use super::guard;
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct NativeCoreImageInfo {
|
||||
pub transfer: u8,
|
||||
}
|
||||
|
||||
/// Decodes a JPEG XL image to premultiplied RGBA in sRGB, or with `keep_space`, in the image's own
|
||||
/// Decodes an image to premultiplied RGBA in sRGB, or with `keep_space`, in the image's own
|
||||
/// space when it has BT.709, BT.2020 or Display P3 primaries with the sRGB, PQ or HLG transfer.
|
||||
/// Unless either is 0, it is downscaled to cover `target_width` x `target_height`, and it never
|
||||
/// exceeds `max_pixel_size` pixels per side. `info` gets the size and space. The caller frees the
|
||||
@@ -42,7 +42,7 @@ pub struct NativeCoreImageInfo {
|
||||
/// `cancelled` must be null or valid for the call and only changed through `native_core_cancel`.
|
||||
/// `info` must be null or a valid writable pointer, outside `data`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn native_core_jxl_decode(
|
||||
pub unsafe extern "C" fn native_core_decode_image(
|
||||
data: *const u8,
|
||||
len: size_t,
|
||||
keep_space: bool,
|
||||
@@ -60,16 +60,19 @@ pub unsafe extern "C" fn native_core_jxl_decode(
|
||||
let cancelled = unsafe { AtomicBool::from_ptr(cancelled) };
|
||||
// SAFETY: the caller provides a buffer readable for len bytes.
|
||||
let input = unsafe { slice::from_raw_parts(data, len) };
|
||||
let target = if keep_space {
|
||||
let color_space = if keep_space {
|
||||
Target::Source
|
||||
} else {
|
||||
Target::Srgb
|
||||
};
|
||||
let cover = (target_width > 0 && target_height > 0)
|
||||
let target_size = (target_width > 0 && target_height > 0)
|
||||
.then_some((target_width as usize, target_height as usize));
|
||||
let Some(Image { cicp, samples }) = Jxl::open(input, target)
|
||||
.and_then(|image| image.decode(cover, max_pixel_size as usize, cancelled))
|
||||
else {
|
||||
let options = Options {
|
||||
color_space,
|
||||
target_size,
|
||||
max_pixel_size: max_pixel_size as usize,
|
||||
};
|
||||
let Some(Image { cicp, samples }) = codecs::decode(input, &options, cancelled) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let (width, height, bit_depth, pixels) = match samples {
|
||||
@@ -93,7 +96,7 @@ pub unsafe extern "C" fn native_core_jxl_decode(
|
||||
/// Asks the decode given `flag` to stop early.
|
||||
///
|
||||
/// # Safety
|
||||
/// `flag` must be null or valid and otherwise only read by `native_core_jxl_decode`.
|
||||
/// `flag` must be null or valid and otherwise only read by `native_core_decode_image`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn native_core_cancel(flag: *mut bool) {
|
||||
if !flag.is_null() {
|
||||
@@ -104,7 +107,7 @@ pub unsafe extern "C" fn native_core_cancel(flag: *mut bool) {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_app_alextran_immich_NativeImage_decodeJxl<'caller>(
|
||||
pub extern "system" fn Java_app_alextran_immich_NativeImage_decode<'caller>(
|
||||
mut env: EnvUnowned<'caller>,
|
||||
_class: JClass<'caller>,
|
||||
data: jlong,
|
||||
@@ -121,7 +124,7 @@ pub extern "system" fn Java_app_alextran_immich_NativeImage_decodeJxl<'caller>(
|
||||
// SAFETY: Kotlin passes a live native buffer of `length` bytes and a direct buffer it only
|
||||
// changes through `cancel`, and the output is valid.
|
||||
let rgba = unsafe {
|
||||
native_core_jxl_decode(
|
||||
native_core_decode_image(
|
||||
data as *const u8,
|
||||
length as size_t,
|
||||
false,
|
||||
@@ -154,7 +157,7 @@ pub extern "system" fn Java_app_alextran_immich_NativeImage_cancel<'caller>(
|
||||
) {
|
||||
env.with_env(|env| -> jni::errors::Result<()> {
|
||||
let flag = env.get_direct_buffer_address(&flag)?.cast::<bool>();
|
||||
// SAFETY: the buffer is the live one-byte flag Kotlin passed to `decodeJxl`.
|
||||
// SAFETY: the buffer is the live one-byte flag Kotlin passed to `decode`.
|
||||
unsafe { native_core_cancel(flag) };
|
||||
Ok(())
|
||||
})
|
||||
@@ -9,7 +9,7 @@ mod android;
|
||||
mod bitmap;
|
||||
#[cfg(target_os = "android")]
|
||||
mod convert;
|
||||
mod jxl;
|
||||
mod decode;
|
||||
#[cfg(target_os = "android")]
|
||||
mod rotate;
|
||||
mod thumbhash;
|
||||
|
||||
@@ -11,6 +11,7 @@ const _manifests = [
|
||||
'../.cargo/config.toml',
|
||||
'../Cargo.toml',
|
||||
'../Cargo.lock',
|
||||
'../crates/codecs/Cargo.toml',
|
||||
'../crates/native_core/Cargo.toml',
|
||||
'$_crate/Cargo.toml',
|
||||
'$_crate/rust-toolchain.toml',
|
||||
|
||||
Reference in New Issue
Block a user