Add image compressor
This commit is contained in:
@@ -11,36 +11,79 @@ set(FFMPEG_INCLUDE_DIR
|
||||
set(FFMPEG_LIB_DIR
|
||||
${FFMPEG_ROOT}/lib)
|
||||
|
||||
# Output dari setup_image_compressor_android_*.sh:
|
||||
# cpp/third_party/android-image-compres/<ABI>/include
|
||||
# cpp/third_party/android-image-compres/<ABI>/lib
|
||||
set(IMAGE_COMPRESS_ROOT
|
||||
${CMAKE_SOURCE_DIR}/third_party/android-image-compres/${ANDROID_ABI})
|
||||
|
||||
set(IMAGE_COMPRESS_INCLUDE_DIR
|
||||
${IMAGE_COMPRESS_ROOT}/include)
|
||||
|
||||
set(IMAGE_COMPRESS_LIB_DIR
|
||||
${IMAGE_COMPRESS_ROOT}/lib)
|
||||
|
||||
add_library(kcompressor SHARED
|
||||
native_compressor.cpp)
|
||||
native_compressor.cpp
|
||||
compressor_common.cpp
|
||||
video_compressor.cpp
|
||||
image_compressor.cpp)
|
||||
|
||||
target_link_options(kcompressor PRIVATE
|
||||
-Wl,--allow-multiple-definition
|
||||
-Wl,--exclude-libs,libimagequant.a
|
||||
-Wl,--exclude-libs,liboxipng_ffi.a)
|
||||
|
||||
target_include_directories(kcompressor PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${FFMPEG_INCLUDE_DIR}
|
||||
${FFMPEG_INCLUDE_DIR}/x264
|
||||
${FFMPEG_INCLUDE_DIR}/x265)
|
||||
${FFMPEG_INCLUDE_DIR}/x265
|
||||
${IMAGE_COMPRESS_INCLUDE_DIR})
|
||||
|
||||
function(import_static_required target_name lib_dir lib_name)
|
||||
set(full_path ${lib_dir}/${lib_name})
|
||||
if (NOT EXISTS ${full_path})
|
||||
message(FATAL_ERROR "Missing static library: ${full_path}")
|
||||
endif()
|
||||
|
||||
function(import_ffmpeg_static target_name lib_name)
|
||||
add_library(${target_name} STATIC IMPORTED)
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/${lib_name})
|
||||
IMPORTED_LOCATION ${full_path})
|
||||
endfunction()
|
||||
|
||||
import_ffmpeg_static(avcodec libavcodec.a)
|
||||
import_ffmpeg_static(avfilter libavfilter.a)
|
||||
import_ffmpeg_static(avformat libavformat.a)
|
||||
import_ffmpeg_static(avutil libavutil.a)
|
||||
import_ffmpeg_static(swresample libswresample.a)
|
||||
import_ffmpeg_static(swscale libswscale.a)
|
||||
import_ffmpeg_static(x264 libx264.a)
|
||||
import_ffmpeg_static(x265 libx265.a)
|
||||
import_static_required(avcodec ${FFMPEG_LIB_DIR} libavcodec.a)
|
||||
import_static_required(avfilter ${FFMPEG_LIB_DIR} libavfilter.a)
|
||||
import_static_required(avformat ${FFMPEG_LIB_DIR} libavformat.a)
|
||||
import_static_required(avutil ${FFMPEG_LIB_DIR} libavutil.a)
|
||||
import_static_required(swresample ${FFMPEG_LIB_DIR} libswresample.a)
|
||||
import_static_required(swscale ${FFMPEG_LIB_DIR} libswscale.a)
|
||||
import_static_required(x264 ${FFMPEG_LIB_DIR} libx264.a)
|
||||
import_static_required(x265 ${FFMPEG_LIB_DIR} libx265.a)
|
||||
|
||||
import_static_required(mozjpeg_jpeg ${IMAGE_COMPRESS_LIB_DIR} libjpeg.a)
|
||||
import_static_required(mozjpeg_turbojpeg ${IMAGE_COMPRESS_LIB_DIR} libturbojpeg.a)
|
||||
import_static_required(webp ${IMAGE_COMPRESS_LIB_DIR} libwebp.a)
|
||||
import_static_required(sharpyuv ${IMAGE_COMPRESS_LIB_DIR} libsharpyuv.a)
|
||||
import_static_required(avif ${IMAGE_COMPRESS_LIB_DIR} libavif.a)
|
||||
import_static_required(aom ${IMAGE_COMPRESS_LIB_DIR} libaom.a)
|
||||
import_static_required(yuv ${IMAGE_COMPRESS_LIB_DIR} libyuv.a)
|
||||
import_static_required(imagequant ${IMAGE_COMPRESS_LIB_DIR} libimagequant.a)
|
||||
import_static_required(oxipng_ffi ${IMAGE_COMPRESS_LIB_DIR} liboxipng_ffi.a)
|
||||
import_static_required(kimage_png ${IMAGE_COMPRESS_LIB_DIR} libkimage_png.a)
|
||||
|
||||
target_compile_features(kcompressor PRIVATE cxx_std_17)
|
||||
|
||||
target_compile_options(kcompressor PRIVATE
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wno-unused-parameter)
|
||||
-Wno-unused-parameter
|
||||
-Wno-deprecated-declarations)
|
||||
|
||||
# --start-group penting karena semua dependency berupa static archive.
|
||||
target_link_libraries(kcompressor
|
||||
-Wl,--start-group
|
||||
|
||||
avfilter
|
||||
avformat
|
||||
avcodec
|
||||
@@ -49,6 +92,18 @@ target_link_libraries(kcompressor
|
||||
avutil
|
||||
x264
|
||||
x265
|
||||
|
||||
mozjpeg_turbojpeg
|
||||
mozjpeg_jpeg
|
||||
webp
|
||||
sharpyuv
|
||||
avif
|
||||
aom
|
||||
yuv
|
||||
imagequant
|
||||
oxipng_ffi
|
||||
kimage_png
|
||||
|
||||
-Wl,--end-group
|
||||
|
||||
android
|
||||
@@ -56,4 +111,5 @@ target_link_libraries(kcompressor
|
||||
z
|
||||
m
|
||||
dl
|
||||
atomic
|
||||
c++_static)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "compressor_common.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
static std::atomic_bool g_cancel_requested(false);
|
||||
|
||||
void set_cancel_requested(bool value) {
|
||||
g_cancel_requested.store(value, std::memory_order_release);
|
||||
}
|
||||
|
||||
void reset_cancel_requested() {
|
||||
set_cancel_requested(false);
|
||||
}
|
||||
|
||||
bool is_cancel_requested() {
|
||||
return g_cancel_requested.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void callback_progress(JNIEnv *env, jobject callback, int percent, const std::string &message) {
|
||||
if (!env || !callback) return;
|
||||
|
||||
jclass cls = env->GetObjectClass(callback);
|
||||
if (!cls) return;
|
||||
|
||||
jmethodID mid = env->GetMethodID(cls, "onProgress", "(ILjava/lang/String;)V");
|
||||
if (!mid) {
|
||||
env->DeleteLocalRef(cls);
|
||||
return;
|
||||
}
|
||||
|
||||
jstring jmsg = env->NewStringUTF(message.c_str());
|
||||
if (jmsg) {
|
||||
env->CallVoidMethod(callback, mid, static_cast<jint>(percent), jmsg);
|
||||
env->DeleteLocalRef(jmsg);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(cls);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
|
||||
void set_cancel_requested(bool value);
|
||||
void reset_cancel_requested();
|
||||
bool is_cancel_requested();
|
||||
|
||||
void callback_progress(JNIEnv *env, jobject callback, int percent, const std::string &message);
|
||||
@@ -0,0 +1,735 @@
|
||||
#include "image_compressor.h"
|
||||
#include "compressor_common.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <turbojpeg.h>
|
||||
#include <webp/decode.h>
|
||||
#include <webp/encode.h>
|
||||
#include <avif/avif.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libimagequant.h>
|
||||
#include <oxipng_ffi.h>
|
||||
#include <kimage_png_codec.h>
|
||||
}
|
||||
|
||||
#include <lodepng.h>
|
||||
|
||||
#define LOG_TAG "KCompressorImage"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
struct RgbaImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> pixels;
|
||||
};
|
||||
|
||||
static bool read_all_fd(int fd, std::vector<uint8_t> &out) {
|
||||
if (fd < 0) return false;
|
||||
|
||||
int owned_fd = dup(fd);
|
||||
if (owned_fd < 0) return false;
|
||||
|
||||
if (lseek64(owned_fd, 0, SEEK_SET) < 0) {
|
||||
LOGD("input image fd is not seekable at start: %s", strerror(errno));
|
||||
}
|
||||
|
||||
struct stat st {};
|
||||
if (fstat(owned_fd, &st) == 0 && st.st_size > 0 && st.st_size < 1024LL * 1024LL * 1024LL) {
|
||||
out.reserve(static_cast<size_t>(st.st_size));
|
||||
}
|
||||
|
||||
uint8_t buffer[64 * 1024];
|
||||
while (true) {
|
||||
if (is_cancel_requested()) {
|
||||
close(owned_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t ret = read(owned_fd, buffer, sizeof(buffer));
|
||||
if (ret == 0) break;
|
||||
if (ret < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
close(owned_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.insert(out.end(), buffer, buffer + ret);
|
||||
}
|
||||
|
||||
close(owned_fd);
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
static bool write_all_fd(int fd, const std::vector<uint8_t> &data) {
|
||||
if (fd < 0 || data.empty()) return false;
|
||||
|
||||
int owned_fd = dup(fd);
|
||||
if (owned_fd < 0) return false;
|
||||
|
||||
if (lseek64(owned_fd, 0, SEEK_SET) < 0) {
|
||||
LOGD("output image fd is not seekable at start: %s", strerror(errno));
|
||||
}
|
||||
if (ftruncate(owned_fd, 0) < 0) {
|
||||
LOGD("ftruncate image output failed or unsupported: %s", strerror(errno));
|
||||
}
|
||||
|
||||
size_t total = 0;
|
||||
while (total < data.size()) {
|
||||
if (is_cancel_requested()) {
|
||||
close(owned_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t ret = write(owned_fd, data.data() + total, data.size() - total);
|
||||
if (ret < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
close(owned_fd);
|
||||
return false;
|
||||
}
|
||||
if (ret == 0) {
|
||||
close(owned_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
total += static_cast<size_t>(ret);
|
||||
}
|
||||
|
||||
fsync(owned_fd);
|
||||
close(owned_fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int clamp_int(int value, int min_value, int max_value) {
|
||||
return std::max(min_value, std::min(max_value, value));
|
||||
}
|
||||
|
||||
static bool has_prefix(const std::vector<uint8_t> &data, const uint8_t *magic, size_t magic_len) {
|
||||
return data.size() >= magic_len && memcmp(data.data(), magic, magic_len) == 0;
|
||||
}
|
||||
|
||||
static bool looks_like_jpeg(const std::vector<uint8_t> &data) {
|
||||
static const uint8_t magic[] = {0xFF, 0xD8};
|
||||
return has_prefix(data, magic, sizeof(magic));
|
||||
}
|
||||
|
||||
static bool looks_like_png(const std::vector<uint8_t> &data) {
|
||||
static const uint8_t magic[] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
|
||||
return has_prefix(data, magic, sizeof(magic));
|
||||
}
|
||||
|
||||
static bool looks_like_webp(const std::vector<uint8_t> &data) {
|
||||
return data.size() >= 12 &&
|
||||
memcmp(data.data(), "RIFF", 4) == 0 &&
|
||||
memcmp(data.data() + 8, "WEBP", 4) == 0;
|
||||
}
|
||||
|
||||
static bool looks_like_avif(const std::vector<uint8_t> &data) {
|
||||
if (data.size() < 16) return false;
|
||||
const size_t max_scan = std::min<size_t>(data.size(), 64);
|
||||
for (size_t i = 0; i + 8 <= max_scan; ++i) {
|
||||
if (memcmp(data.data() + i, "ftyp", 4) == 0) {
|
||||
const char *brand = reinterpret_cast<const char *>(data.data() + i + 4);
|
||||
return memcmp(brand, "avif", 4) == 0 ||
|
||||
memcmp(brand, "avis", 4) == 0 ||
|
||||
memcmp(brand, "mif1", 4) == 0 ||
|
||||
memcmp(brand, "msf1", 4) == 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool decode_jpeg_rgba(const std::vector<uint8_t> &input, RgbaImage &out) {
|
||||
tjhandle handle = tjInitDecompress();
|
||||
if (!handle) return false;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int subsamp = 0;
|
||||
int colorspace = 0;
|
||||
|
||||
int ret = tjDecompressHeader3(
|
||||
handle,
|
||||
const_cast<unsigned char *>(input.data()),
|
||||
static_cast<unsigned long>(input.size()),
|
||||
&width,
|
||||
&height,
|
||||
&subsamp,
|
||||
&colorspace
|
||||
);
|
||||
|
||||
if (ret != 0 || width <= 0 || height <= 0) {
|
||||
tjDestroy(handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = width;
|
||||
out.height = height;
|
||||
out.pixels.assign(static_cast<size_t>(width) * height * 4, 255);
|
||||
|
||||
ret = tjDecompress2(
|
||||
handle,
|
||||
const_cast<unsigned char *>(input.data()),
|
||||
static_cast<unsigned long>(input.size()),
|
||||
out.pixels.data(),
|
||||
width,
|
||||
width * 4,
|
||||
height,
|
||||
TJPF_RGBA,
|
||||
TJFLAG_FASTDCT
|
||||
);
|
||||
|
||||
tjDestroy(handle);
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
static bool decode_png_rgba(const std::vector<uint8_t> &input, RgbaImage &out) {
|
||||
uint8_t *rgba = nullptr;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
|
||||
int ret = kimg_png_decode_rgba(
|
||||
input.data(),
|
||||
input.size(),
|
||||
&rgba,
|
||||
&width,
|
||||
&height
|
||||
);
|
||||
|
||||
if (ret != 0 || !rgba || width == 0 || height == 0) {
|
||||
if (rgba) kimg_free(rgba);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = static_cast<int>(width);
|
||||
out.height = static_cast<int>(height);
|
||||
out.pixels.assign(rgba, rgba + static_cast<size_t>(width) * height * 4);
|
||||
kimg_free(rgba);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_webp_rgba(const std::vector<uint8_t> &input, RgbaImage &out) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (!WebPGetInfo(input.data(), input.size(), &width, &height) || width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t *rgba = WebPDecodeRGBA(input.data(), input.size(), &width, &height);
|
||||
if (!rgba) return false;
|
||||
|
||||
out.width = width;
|
||||
out.height = height;
|
||||
out.pixels.assign(rgba, rgba + static_cast<size_t>(width) * height * 4);
|
||||
WebPFree(rgba);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_avif_rgba(const std::vector<uint8_t> &input, RgbaImage &out) {
|
||||
avifDecoder *decoder = avifDecoderCreate();
|
||||
if (!decoder) return false;
|
||||
|
||||
avifImage *image = avifImageCreateEmpty();
|
||||
if (!image) {
|
||||
avifDecoderDestroy(decoder);
|
||||
return false;
|
||||
}
|
||||
|
||||
avifResult result = avifDecoderReadMemory(decoder, image, input.data(), input.size());
|
||||
if (result != AVIF_RESULT_OK || image->width == 0 || image->height == 0) {
|
||||
avifImageDestroy(image);
|
||||
avifDecoderDestroy(decoder);
|
||||
return false;
|
||||
}
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, image);
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
rgb.depth = 8;
|
||||
|
||||
if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) {
|
||||
avifImageDestroy(image);
|
||||
avifDecoderDestroy(decoder);
|
||||
return false;
|
||||
}
|
||||
|
||||
result = avifImageYUVToRGB(image, &rgb);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
avifRGBImageFreePixels(&rgb);
|
||||
avifImageDestroy(image);
|
||||
avifDecoderDestroy(decoder);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = static_cast<int>(image->width);
|
||||
out.height = static_cast<int>(image->height);
|
||||
out.pixels.assign(rgb.pixels, rgb.pixels + static_cast<size_t>(rgb.rowBytes) * image->height);
|
||||
|
||||
// Pastikan buffer contiguous width * height * 4, karena rowBytes bisa lebih besar.
|
||||
if (rgb.rowBytes != image->width * 4) {
|
||||
std::vector<uint8_t> compact(static_cast<size_t>(image->width) * image->height * 4);
|
||||
for (uint32_t y = 0; y < image->height; ++y) {
|
||||
memcpy(compact.data() + static_cast<size_t>(y) * image->width * 4,
|
||||
rgb.pixels + static_cast<size_t>(y) * rgb.rowBytes,
|
||||
static_cast<size_t>(image->width) * 4);
|
||||
}
|
||||
out.pixels.swap(compact);
|
||||
}
|
||||
|
||||
avifRGBImageFreePixels(&rgb);
|
||||
avifImageDestroy(image);
|
||||
avifDecoderDestroy(decoder);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_input_rgba(const std::vector<uint8_t> &input,
|
||||
const std::string &input_mime,
|
||||
RgbaImage &out) {
|
||||
std::string mime = input_mime;
|
||||
std::transform(mime.begin(), mime.end(), mime.begin(), ::tolower);
|
||||
|
||||
if (mime.find("jpeg") != std::string::npos || mime.find("jpg") != std::string::npos || looks_like_jpeg(input)) {
|
||||
return decode_jpeg_rgba(input, out);
|
||||
}
|
||||
|
||||
if (mime.find("png") != std::string::npos || looks_like_png(input)) {
|
||||
return decode_png_rgba(input, out);
|
||||
}
|
||||
|
||||
if (mime.find("webp") != std::string::npos || looks_like_webp(input)) {
|
||||
return decode_webp_rgba(input, out);
|
||||
}
|
||||
|
||||
if (mime.find("avif") != std::string::npos || looks_like_avif(input)) {
|
||||
return decode_avif_rgba(input, out);
|
||||
}
|
||||
|
||||
// Fallback by magic.
|
||||
if (looks_like_jpeg(input)) return decode_jpeg_rgba(input, out);
|
||||
if (looks_like_png(input)) return decode_png_rgba(input, out);
|
||||
if (looks_like_webp(input)) return decode_webp_rgba(input, out);
|
||||
if (looks_like_avif(input)) return decode_avif_rgba(input, out);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static RgbaImage resize_rgba_if_needed(const RgbaImage &src, int max_long_side) {
|
||||
if (src.width <= 0 || src.height <= 0 || src.pixels.empty()) {
|
||||
return src;
|
||||
}
|
||||
|
||||
int long_side = std::max(src.width, src.height);
|
||||
if (max_long_side <= 0 || max_long_side >= long_side) {
|
||||
return src;
|
||||
}
|
||||
|
||||
double scale = static_cast<double>(max_long_side) / static_cast<double>(long_side);
|
||||
int dst_w = std::max(1, static_cast<int>(std::llround(src.width * scale)));
|
||||
int dst_h = std::max(1, static_cast<int>(std::llround(src.height * scale)));
|
||||
|
||||
RgbaImage dst;
|
||||
dst.width = dst_w;
|
||||
dst.height = dst_h;
|
||||
dst.pixels.assign(static_cast<size_t>(dst_w) * dst_h * 4, 0);
|
||||
|
||||
const double inv_x = static_cast<double>(src.width) / static_cast<double>(dst_w);
|
||||
const double inv_y = static_cast<double>(src.height) / static_cast<double>(dst_h);
|
||||
|
||||
for (int y = 0; y < dst_h; ++y) {
|
||||
int sy = std::min(src.height - 1, static_cast<int>(y * inv_y));
|
||||
for (int x = 0; x < dst_w; ++x) {
|
||||
int sx = std::min(src.width - 1, static_cast<int>(x * inv_x));
|
||||
|
||||
const uint8_t *sp = src.pixels.data() + (static_cast<size_t>(sy) * src.width + sx) * 4;
|
||||
uint8_t *dp = dst.pixels.data() + (static_cast<size_t>(y) * dst_w + x) * 4;
|
||||
|
||||
dp[0] = sp[0];
|
||||
dp[1] = sp[1];
|
||||
dp[2] = sp[2];
|
||||
dp[3] = sp[3];
|
||||
}
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
static bool encode_jpeg_rgba(const RgbaImage &image, int quality, std::vector<uint8_t> &out) {
|
||||
tjhandle handle = tjInitCompress();
|
||||
if (!handle) return false;
|
||||
|
||||
unsigned char *jpeg_buf = nullptr;
|
||||
unsigned long jpeg_size = 0;
|
||||
|
||||
int ret = tjCompress2(
|
||||
handle,
|
||||
const_cast<uint8_t *>(image.pixels.data()),
|
||||
image.width,
|
||||
image.width * 4,
|
||||
image.height,
|
||||
TJPF_RGBA,
|
||||
&jpeg_buf,
|
||||
&jpeg_size,
|
||||
TJSAMP_420,
|
||||
clamp_int(quality, 1, 100),
|
||||
TJFLAG_FASTDCT
|
||||
);
|
||||
|
||||
if (ret != 0 || !jpeg_buf || jpeg_size == 0) {
|
||||
if (jpeg_buf) tjFree(jpeg_buf);
|
||||
tjDestroy(handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.assign(jpeg_buf, jpeg_buf + jpeg_size);
|
||||
tjFree(jpeg_buf);
|
||||
tjDestroy(handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool oxipng_optimize_vector(const std::vector<uint8_t> &png, std::vector<uint8_t> &out, uint8_t level) {
|
||||
uint8_t *optimized = nullptr;
|
||||
size_t optimized_len = 0;
|
||||
|
||||
int ret = oxipng_optimize_memory(
|
||||
png.data(),
|
||||
png.size(),
|
||||
level,
|
||||
1,
|
||||
0,
|
||||
&optimized,
|
||||
&optimized_len
|
||||
);
|
||||
|
||||
if (ret == 0 && optimized && optimized_len > 0) {
|
||||
out.assign(optimized, optimized + optimized_len);
|
||||
oxipng_free(optimized, optimized_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (optimized) {
|
||||
oxipng_free(optimized, optimized_len);
|
||||
}
|
||||
|
||||
out = png;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_png_lossless_rgba(const RgbaImage &image, std::vector<uint8_t> &out) {
|
||||
uint8_t *png = nullptr;
|
||||
size_t png_len = 0;
|
||||
|
||||
int ret = kimg_png_encode_rgba(
|
||||
image.pixels.data(),
|
||||
static_cast<unsigned>(image.width),
|
||||
static_cast<unsigned>(image.height),
|
||||
&png,
|
||||
&png_len
|
||||
);
|
||||
|
||||
if (ret != 0 || !png || png_len == 0) {
|
||||
if (png) kimg_free(png);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encoded(png, png + png_len);
|
||||
kimg_free(png);
|
||||
|
||||
return oxipng_optimize_vector(encoded, out, 3);
|
||||
}
|
||||
|
||||
static bool encode_png_lossy_quantized(const RgbaImage &image, int quality, std::vector<uint8_t> &out) {
|
||||
liq_attr *attr = liq_attr_create();
|
||||
if (!attr) return false;
|
||||
|
||||
const int max_quality = clamp_int(quality, 1, 100);
|
||||
const int min_quality = std::max(0, max_quality - 25);
|
||||
|
||||
liq_set_max_colors(attr, 256);
|
||||
liq_set_quality(attr, min_quality, max_quality);
|
||||
liq_set_speed(attr, 4);
|
||||
|
||||
liq_image *liq_img = liq_image_create_rgba(
|
||||
attr,
|
||||
const_cast<uint8_t *>(image.pixels.data()),
|
||||
image.width,
|
||||
image.height,
|
||||
0.0
|
||||
);
|
||||
|
||||
if (!liq_img) {
|
||||
liq_attr_destroy(attr);
|
||||
return false;
|
||||
}
|
||||
|
||||
liq_result *result = nullptr;
|
||||
liq_error err = liq_image_quantize(liq_img, attr, &result);
|
||||
if (err != LIQ_OK || !result) {
|
||||
liq_image_destroy(liq_img);
|
||||
liq_attr_destroy(attr);
|
||||
return false;
|
||||
}
|
||||
|
||||
liq_set_dithering_level(result, 0.7f);
|
||||
|
||||
std::vector<uint8_t> indexed(static_cast<size_t>(image.width) * image.height);
|
||||
err = liq_write_remapped_image(result, liq_img, indexed.data(), indexed.size());
|
||||
if (err != LIQ_OK) {
|
||||
liq_result_destroy(result);
|
||||
liq_image_destroy(liq_img);
|
||||
liq_attr_destroy(attr);
|
||||
return false;
|
||||
}
|
||||
|
||||
const liq_palette *palette = liq_get_palette(result);
|
||||
if (!palette || palette->count <= 0) {
|
||||
liq_result_destroy(result);
|
||||
liq_image_destroy(liq_img);
|
||||
liq_attr_destroy(attr);
|
||||
return false;
|
||||
}
|
||||
|
||||
lodepng::State state;
|
||||
state.encoder.auto_convert = 0;
|
||||
state.info_raw.colortype = LCT_PALETTE;
|
||||
state.info_raw.bitdepth = 8;
|
||||
state.info_png.color.colortype = LCT_PALETTE;
|
||||
state.info_png.color.bitdepth = 8;
|
||||
|
||||
for (unsigned int i = 0; i < palette->count; ++i) {
|
||||
const liq_color &c = palette->entries[i];
|
||||
lodepng_palette_add(&state.info_raw, c.r, c.g, c.b, c.a);
|
||||
lodepng_palette_add(&state.info_png.color, c.r, c.g, c.b, c.a);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> png;
|
||||
unsigned lode_err = lodepng::encode(
|
||||
png,
|
||||
indexed,
|
||||
static_cast<unsigned>(image.width),
|
||||
static_cast<unsigned>(image.height),
|
||||
state
|
||||
);
|
||||
|
||||
liq_result_destroy(result);
|
||||
liq_image_destroy(liq_img);
|
||||
liq_attr_destroy(attr);
|
||||
|
||||
if (lode_err != 0 || png.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encoded(png.begin(), png.end());
|
||||
return oxipng_optimize_vector(encoded, out, 4);
|
||||
}
|
||||
|
||||
static bool encode_webp_rgba(const RgbaImage &image, int quality, bool lossless, std::vector<uint8_t> &out) {
|
||||
uint8_t *webp = nullptr;
|
||||
size_t webp_size = 0;
|
||||
|
||||
if (lossless) {
|
||||
webp_size = WebPEncodeLosslessRGBA(
|
||||
image.pixels.data(),
|
||||
image.width,
|
||||
image.height,
|
||||
image.width * 4,
|
||||
&webp
|
||||
);
|
||||
} else {
|
||||
webp_size = WebPEncodeRGBA(
|
||||
image.pixels.data(),
|
||||
image.width,
|
||||
image.height,
|
||||
image.width * 4,
|
||||
static_cast<float>(clamp_int(quality, 1, 100)),
|
||||
&webp
|
||||
);
|
||||
}
|
||||
|
||||
if (!webp || webp_size == 0) {
|
||||
if (webp) WebPFree(webp);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.assign(webp, webp + webp_size);
|
||||
WebPFree(webp);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_avif_rgba(const RgbaImage &image, int quality, std::vector<uint8_t> &out) {
|
||||
avifImage *avif = avifImageCreate(
|
||||
static_cast<uint32_t>(image.width),
|
||||
static_cast<uint32_t>(image.height),
|
||||
8,
|
||||
AVIF_PIXEL_FORMAT_YUV444
|
||||
);
|
||||
|
||||
if (!avif) return false;
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, avif);
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
rgb.depth = 8;
|
||||
rgb.pixels = const_cast<uint8_t *>(image.pixels.data());
|
||||
rgb.rowBytes = static_cast<uint32_t>(image.width * 4);
|
||||
|
||||
avifResult result = avifImageRGBToYUV(avif, &rgb);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
avifImageDestroy(avif);
|
||||
return false;
|
||||
}
|
||||
|
||||
avifEncoder *encoder = avifEncoderCreate();
|
||||
if (!encoder) {
|
||||
avifImageDestroy(avif);
|
||||
return false;
|
||||
}
|
||||
|
||||
encoder->maxThreads = 0;
|
||||
encoder->speed = 6;
|
||||
encoder->quality = clamp_int(quality, 1, 100);
|
||||
encoder->qualityAlpha = clamp_int(quality, 1, 100);
|
||||
|
||||
avifRWData encoded = AVIF_DATA_EMPTY;
|
||||
result = avifEncoderWrite(encoder, avif, &encoded);
|
||||
|
||||
if (result == AVIF_RESULT_OK && encoded.data && encoded.size > 0) {
|
||||
out.assign(encoded.data, encoded.data + encoded.size);
|
||||
avifRWDataFree(&encoded);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(avif);
|
||||
return true;
|
||||
}
|
||||
|
||||
avifRWDataFree(&encoded);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(avif);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool encode_output(const RgbaImage &image,
|
||||
const std::string &output_format,
|
||||
int quality,
|
||||
std::vector<uint8_t> &out) {
|
||||
std::string fmt = output_format;
|
||||
std::transform(fmt.begin(), fmt.end(), fmt.begin(), ::tolower);
|
||||
|
||||
if (fmt == "jpeg" || fmt == "jpg") {
|
||||
return encode_jpeg_rgba(image, quality, out);
|
||||
}
|
||||
|
||||
if (fmt == "png_lossy") {
|
||||
if (encode_png_lossy_quantized(image, quality, out)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGD("PNG lossy quantize failed, fallback to PNG lossless");
|
||||
return encode_png_lossless_rgba(image, out);
|
||||
}
|
||||
|
||||
if (fmt == "png" || fmt == "png_lossless") {
|
||||
return encode_png_lossless_rgba(image, out);
|
||||
}
|
||||
|
||||
if (fmt == "webp_lossless") {
|
||||
return encode_webp_rgba(image, quality, true, out);
|
||||
}
|
||||
|
||||
if (fmt == "webp") {
|
||||
return encode_webp_rgba(image, quality, false, out);
|
||||
}
|
||||
|
||||
if (fmt == "avif") {
|
||||
return encode_avif_rgba(image, quality, out);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int compress_image_fd_impl(JNIEnv *env,
|
||||
int input_fd,
|
||||
int output_fd,
|
||||
const std::string &input_mime,
|
||||
const std::string &output_format,
|
||||
int quality,
|
||||
int max_long_side,
|
||||
jobject callback) {
|
||||
if (input_fd < 0 || output_fd < 0) {
|
||||
callback_progress(env, callback, 0, "FD gambar tidak valid");
|
||||
return -1;
|
||||
}
|
||||
|
||||
quality = clamp_int(quality, 1, 100);
|
||||
if (max_long_side < 0) max_long_side = 0;
|
||||
|
||||
callback_progress(env, callback, 0, "Membaca gambar...");
|
||||
|
||||
std::vector<uint8_t> input;
|
||||
if (!read_all_fd(input_fd, input)) {
|
||||
callback_progress(env, callback, 0, is_cancel_requested() ? "Canceled" : "Gagal membaca gambar");
|
||||
return is_cancel_requested() ? -125 : -2;
|
||||
}
|
||||
|
||||
if (is_cancel_requested()) {
|
||||
callback_progress(env, callback, 0, "Canceled");
|
||||
return -125;
|
||||
}
|
||||
|
||||
callback_progress(env, callback, 15, "Decode gambar...");
|
||||
|
||||
RgbaImage decoded;
|
||||
if (!decode_input_rgba(input, input_mime, decoded)) {
|
||||
callback_progress(env, callback, 0, "Format gambar tidak didukung");
|
||||
return -3;
|
||||
}
|
||||
|
||||
if (is_cancel_requested()) {
|
||||
callback_progress(env, callback, 0, "Canceled");
|
||||
return -125;
|
||||
}
|
||||
|
||||
callback_progress(env, callback, 35, "Resize gambar...");
|
||||
|
||||
RgbaImage work = resize_rgba_if_needed(decoded, max_long_side);
|
||||
|
||||
if (is_cancel_requested()) {
|
||||
callback_progress(env, callback, 0, "Canceled");
|
||||
return -125;
|
||||
}
|
||||
|
||||
callback_progress(env, callback, 60, "Encode gambar...");
|
||||
|
||||
std::vector<uint8_t> encoded;
|
||||
if (!encode_output(work, output_format, quality, encoded)) {
|
||||
callback_progress(env, callback, 0, "Gagal encode gambar");
|
||||
return -4;
|
||||
}
|
||||
|
||||
if (is_cancel_requested()) {
|
||||
callback_progress(env, callback, 0, "Canceled");
|
||||
return -125;
|
||||
}
|
||||
|
||||
callback_progress(env, callback, 92, "Menyimpan gambar...");
|
||||
|
||||
if (!write_all_fd(output_fd, encoded)) {
|
||||
callback_progress(env, callback, 0, "Gagal menulis output gambar");
|
||||
return -5;
|
||||
}
|
||||
|
||||
callback_progress(env, callback, 100, "Done");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
|
||||
int compress_image_fd_impl(JNIEnv *env,
|
||||
int input_fd,
|
||||
int output_fd,
|
||||
const std::string &input_mime,
|
||||
const std::string &output_format,
|
||||
int quality,
|
||||
int max_long_side,
|
||||
jobject callback);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
|
||||
int compress_video_fd_impl(JNIEnv *env,
|
||||
int input_fd,
|
||||
int output_fd,
|
||||
int target_short_side,
|
||||
int input_rotation_degrees,
|
||||
int crf,
|
||||
const std::string &encoder_name,
|
||||
const std::string &preset,
|
||||
const std::string &audio_mode,
|
||||
int audio_bitrate,
|
||||
jobject callback);
|
||||
Reference in New Issue
Block a user