diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 67d605280..dc715cb4f 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -107,5 +107,5 @@ corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml) corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384") # Build and link our app's native lib -add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c hash.c natsort/strnatcmp.c) +add_library(${PROJECT_NAME} SHARED archive.c gifutils.c hash.c natsort/strnatcmp.c) target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics -Wl,--exclude-libs,ALL) diff --git a/app/src/main/cpp/border.c b/app/src/main/cpp/border.c deleted file mode 100644 index 2178c3fb2..000000000 --- a/app/src/main/cpp/border.c +++ /dev/null @@ -1,241 +0,0 @@ -#include -#include -#include -#include -#include -#include - -/** A line will be considered as having content if 0.25% of it is filled. */ -const float filledRatioLimit = 0.0025; - -/** When the threshold is closer to 1, less content will be cropped. **/ -#define THRESHOLD 0.75 - -const uint8_t thresholdForBlack = (uint8_t) (255.0 * THRESHOLD); - -const uint8_t thresholdForWhite = (uint8_t) (255.0 - 255.0 * THRESHOLD); - -static uint8_t inline grey(uint32_t pixel) { - const uint8_t r = ((uint8_t *) &pixel)[0]; - const uint8_t g = ((uint8_t *) &pixel)[1]; - const uint8_t b = ((uint8_t *) &pixel)[2]; - return (r * 77 + g * 151 + b * 28) >> 8; -} - -static bool inline isBlackPixel(const uint32_t *pixels, uint32_t width, uint32_t x, uint32_t y) { - const uint32_t pixel = *((uint32_t *) pixels + (y * width + x)); - return grey(pixel) < thresholdForBlack; -} - -static bool inline isWhitePixel(const uint32_t *pixels, uint32_t width, uint32_t x, uint32_t y) { - const uint32_t pixel = *((uint32_t *) pixels + (y * width + x)); - return grey(pixel) > thresholdForWhite; -} - -/** Return the first x position where there is a substantial amount of fill, - * starting the search from the left. */ -uint32_t findBorderLeft(uint32_t *pixels, uint32_t width, uint32_t height, - uint32_t top, uint32_t bottom) { - int x, y; - const uint32_t filledLimit = round(height * filledRatioLimit / 2); - - // Scan first line to detect dominant color - uint32_t whitePixels = 0; - uint32_t blackPixels = 0; - - for (y = top; y < bottom; y += 2) { - if (isBlackPixel(pixels, width, 0, y)) { - blackPixels++; - } else if (isWhitePixel(pixels, width, 0, y)) { - whitePixels++; - } - } - - bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel; - if (whitePixels > filledLimit && blackPixels > filledLimit) { - // Mixed fill found... don't crop anything - return 0; - } else if (blackPixels > filledLimit) { - detectFunc = isWhitePixel; - } - - // Scan vertical lines in search of filled lines - for (x = 1; x < width; x++) { - uint32_t filledCount = 0; - - for (y = top; y < bottom; y += 2) { - if (detectFunc(pixels, width, x, y)) { - filledCount++; - } - } - - if (filledCount > filledLimit) { - // This line contains enough fill - return x; - } - } - - // No fill found... don't crop anything - return 0; -} - -/** Return the first x position where there is a substantial amount of fill, - * starting the search from the right. */ -uint32_t findBorderRight(uint32_t *pixels, uint32_t width, uint32_t height, - uint32_t top, uint32_t bottom) { - int x, y; - const uint32_t filledLimit = round(height * filledRatioLimit / 2); - - // Scan first line to detect dominant color - uint32_t whitePixels = 0; - uint32_t blackPixels = 0; - - uint32_t lastX = width - 1; - for (y = top; y < bottom; y += 2) { - if (isBlackPixel(pixels, width, lastX, y)) { - blackPixels++; - } else if (isWhitePixel(pixels, width, lastX, y)) { - whitePixels++; - } - } - - bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel; - if (whitePixels > filledLimit && blackPixels > filledLimit) { - // Mixed fill found... don't crop anything - return width; - } else if (blackPixels > filledLimit) { - detectFunc = isWhitePixel; - } - - // Scan vertical lines in search of filled lines - for (x = width - 2; x > 0; x--) { - uint32_t filledCount = 0; - - for (y = top; y < bottom; y += 2) { - if (detectFunc(pixels, width, x, y)) { - filledCount++; - } - } - - if (filledCount > filledLimit) { - // This line contains enough fill - return x + 1; - } - } - - // No fill found... don't crop anything - return width; -} - -/** Return the first y position where there is a substantial amount of fill, - * starting the search from the top. */ -uint32_t findBorderTop(uint32_t *pixels, uint32_t width, uint32_t height) { - int x, y; - const uint32_t filledLimit = round(width * filledRatioLimit / 2); - - // Scan first line to detect dominant color - uint32_t whitePixels = 0; - uint32_t blackPixels = 0; - - for (x = 0; x < width; x += 2) { - if (isBlackPixel(pixels, width, x, 0)) { - blackPixels++; - } else if (isWhitePixel(pixels, width, x, 0)) { - whitePixels++; - } - } - - bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel; - if (whitePixels > filledLimit && blackPixels > filledLimit) { - // Mixed fill found... don't crop anything - return 0; - } else if (blackPixels > filledLimit) { - detectFunc = isWhitePixel; - } - - // Scan horizontal lines in search of filled lines - for (y = 1; y < height; y++) { - uint32_t filledCount = 0; - - for (x = 0; x < width; x += 2) { - if (detectFunc(pixels, width, x, y)) { - filledCount++; - } - } - - if (filledCount > filledLimit) { - // This line contains enough fill - return y; - } - } - - // No fill found... don't crop anything - return 0; -} - -/** Return the first y position where there is a substantial amount of fill, - * starting the search from the bottom. */ -uint32_t findBorderBottom(uint32_t *pixels, uint32_t width, uint32_t height) { - int x, y; - const uint32_t filledLimit = round(width * filledRatioLimit / 2); - - // Scan first line to detect dominant color - uint32_t whitePixels = 0; - uint32_t blackPixels = 0; - uint32_t lastY = height - 1; - - for (x = 0; x < width; x += 2) { - if (isBlackPixel(pixels, width, x, lastY)) { - blackPixels++; - } else if (isWhitePixel(pixels, width, x, lastY)) { - whitePixels++; - } - } - - bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel; - if (whitePixels > filledLimit && blackPixels > filledLimit) { - // Mixed fill found... don't crop anything - return height; - } else if (blackPixels > filledLimit) { - detectFunc = isWhitePixel; - } - - // Scan horizontal lines in search of filled lines - for (y = height - 2; y > 0; y--) { - uint32_t filledCount = 0; - - for (x = 0; x < width; x += 2) { - if (detectFunc(pixels, width, x, y)) { - filledCount++; - } - } - - if (filledCount > filledLimit) { - // This line contains enough fill - return y + 1; - } - } - - // No fill found... don't crop anything - return height; -} - -JNIEXPORT jintArray JNICALL -Java_com_hippo_ehviewer_image_ImageKt_detectBorder(JNIEnv *env, jclass clazz, jobject bitmap) { - void *pixels = NULL; - AndroidBitmapInfo info; - memset(&info, 0, sizeof(info)); - AndroidBitmap_getInfo(env, bitmap, &info); - AndroidBitmap_lockPixels(env, bitmap, &pixels); - uint32_t width = info.width; - uint32_t height = info.height; - uint32_t top = findBorderTop(pixels, width, height); - uint32_t bottom = findBorderBottom(pixels, width, height); - uint32_t left = findBorderLeft(pixels, width, height, top, bottom); - uint32_t right = findBorderRight(pixels, width, height, top, bottom); - int array[4] = {left, top, right, bottom}; - AndroidBitmap_unlockPixels(env, bitmap); - jintArray ret = (*env)->NewIntArray(env, 4); - (*env)->SetIntArrayRegion(env, ret, 0, 4, array); - return ret; -} diff --git a/app/src/main/rust/src/img.rs b/app/src/main/rust/src/img.rs index 9ad8744ee..c7702487d 100644 --- a/app/src/main/rust/src/img.rs +++ b/app/src/main/rust/src/img.rs @@ -1 +1,2 @@ +mod border; mod qr_code; diff --git a/app/src/main/rust/src/img/border.rs b/app/src/main/rust/src/img/border.rs new file mode 100644 index 000000000..9715a8899 --- /dev/null +++ b/app/src/main/rust/src/img/border.rs @@ -0,0 +1,95 @@ +use crate::{jni_throwing, with_bitmap_content}; +use anyhow::anyhow; +use image::{ImageBuffer, Luma, Pixel, Rgba}; +use jni::objects::JClass; +use jni::sys::{jintArray, jobject}; +use jni::JNIEnv; +use jni_fn::jni_fn; + +/** A line will be considered as having content if 0.25% of it is filled. */ +static FILLED_RATIO_LIMIT: f32 = 0.0025; + +/** When the threshold is closer to 1, less content will be cropped. **/ +static THRESHOLD: f32 = 0.75; + +static THRESHOLD_FOR_BLACK: u8 = (255.0 * THRESHOLD) as u8; +static THRESHOLD_FOR_WHITE: u8 = (255.0 - 255.0 * THRESHOLD) as u8; + +fn is_white(pixel: &Luma) -> bool { + pixel.0[0] > THRESHOLD_FOR_WHITE +} + +fn is_black(pixel: &Luma) -> bool { + pixel.0[0] < THRESHOLD_FOR_BLACK +} + +fn line_not_filled_by<'pixel>( + line: impl Iterator>, + white: bool, + limit: i32, +) -> bool { + let f = if white { is_black } else { is_white }; + line.step_by(2).filter(|p| f(&p.to_luma())).count() as i32 > limit +} + +fn try_count_lines<'pixel>( + iter: impl Iterator>>, + white: bool, + limit: i32, +) -> i32 { + let mut count = 1; + for line in iter { + if line_not_filled_by(line, white, limit) { + break; + } else { + count += 1; + } + } + count +} + +fn detect_border_lines<'pixel>( + mut iter: impl Iterator>>, + line_len: u32, +) -> i32 { + let first_row = iter.next().expect("Image is empty!"); + let filled_limit = (line_len as f32 * FILLED_RATIO_LIMIT / 2.0).round() as i32; + let (mut black, mut white) = (0, 0); + for pixel in first_row.step_by(2) { + let luma = &pixel.to_luma(); + if is_black(luma) { + black += 1; + } else if is_white(luma) { + white += 1; + } + } + match (black > filled_limit, white > filled_limit) { + // Black is dominant color. + (true, false) => try_count_lines(iter, false, filled_limit), + // White is dominant color. + (false, true) => try_count_lines(iter, true, filled_limit), + _ => 0, + } +} + +fn detect_border(image: &ImageBuffer, &[u8]>) -> Option<[i32; 4]> { + let (w, h) = image.dimensions(); + let top = detect_border_lines(image.rows(), w); + let bottom = detect_border_lines(image.rows().rev(), w); + let iter = (0..w).map(|row| (0..h).map(move |col| image.get_pixel(row, col))); + let left = detect_border_lines(iter.clone(), h); + let right = detect_border_lines(iter.rev(), h); + Some([left, top, w as i32 - right, h as i32 - bottom]) +} + +#[no_mangle] +#[allow(non_snake_case)] +#[jni_fn("com.hippo.ehviewer.image.ImageKt")] +pub fn detectBorder(mut env: JNIEnv, _class: JClass, object: jobject) -> jintArray { + jni_throwing(&mut env, |env| { + let slice = with_bitmap_content(env, object, |img| Ok(detect_border(img)))?; + let array = env.new_int_array(4)?; + env.set_int_array_region(&array, 0, &slice.ok_or(anyhow!("Image too small!"))?)?; + Ok(array.into_raw()) + }) +} diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs index 7452a20fa..71e6a3d18 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -58,8 +58,8 @@ where } fn deref_mut_direct_bytebuffer<'local>( - env: &'local JNIEnv, - buffer: JByteBuffer, + env: &JNIEnv, + buffer: JByteBuffer<'local>, ) -> Result<&'local mut [u8]> { let ptr = env.get_direct_buffer_address(&buffer)?; let cap = env.get_direct_buffer_capacity(&buffer)?;