refactor progress
This commit is contained in:
159
fluxer_app/crates/libfluxcore/src/animation.rs
Normal file
159
fluxer_app/crates/libfluxcore/src/animation.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use gif::{ColorOutput, DecodeOptions};
|
||||
use std::io::Cursor;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||
|
||||
enum ImageFormat {
|
||||
Gif,
|
||||
Png,
|
||||
Webp,
|
||||
Avif,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
fn detect_format(input: &[u8]) -> ImageFormat {
|
||||
if input.len() >= 6 && &input[..6] == b"GIF89a" {
|
||||
return ImageFormat::Gif;
|
||||
}
|
||||
if input.len() >= 6 && &input[..6] == b"GIF87a" {
|
||||
return ImageFormat::Gif;
|
||||
}
|
||||
if input.len() >= PNG_SIGNATURE.len() && input[..PNG_SIGNATURE.len()] == PNG_SIGNATURE {
|
||||
return ImageFormat::Png;
|
||||
}
|
||||
if input.len() >= 12 && &input[..4] == b"RIFF" && &input[8..12] == b"WEBP" {
|
||||
return ImageFormat::Webp;
|
||||
}
|
||||
if is_avif_file(input) {
|
||||
return ImageFormat::Avif;
|
||||
}
|
||||
ImageFormat::Unknown
|
||||
}
|
||||
|
||||
fn is_animated_gif(input: &[u8]) -> bool {
|
||||
let mut options = DecodeOptions::new();
|
||||
options.set_color_output(ColorOutput::RGBA);
|
||||
let cursor = Cursor::new(input);
|
||||
let mut reader = match options.read_info(cursor) {
|
||||
Ok(reader) => reader,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let mut frame_count = 0;
|
||||
loop {
|
||||
match reader.read_next_frame() {
|
||||
Ok(Some(_frame)) => {
|
||||
frame_count += 1;
|
||||
if frame_count > 1 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn has_apng_actl(input: &[u8]) -> bool {
|
||||
if input.len() < PNG_SIGNATURE.len() || input[..PNG_SIGNATURE.len()] != PNG_SIGNATURE {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut offset = PNG_SIGNATURE.len();
|
||||
while offset + 12 <= input.len() {
|
||||
let length_bytes = &input[offset..offset + 4];
|
||||
let length = u32::from_be_bytes(length_bytes.try_into().unwrap()) as usize;
|
||||
let chunk_type = &input[offset + 4..offset + 8];
|
||||
if chunk_type == b"acTL" {
|
||||
return true;
|
||||
}
|
||||
offset = offset
|
||||
.saturating_add(8)
|
||||
.saturating_add(length)
|
||||
.saturating_add(4);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn has_webp_anim(input: &[u8]) -> bool {
|
||||
if input.len() < 12 || &input[..4] != b"RIFF" || &input[8..12] != b"WEBP" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut offset = 12;
|
||||
while offset + 8 <= input.len() {
|
||||
let chunk_id = &input[offset..offset + 4];
|
||||
let size_bytes = &input[offset + 4..offset + 8];
|
||||
let size = u32::from_le_bytes(size_bytes.try_into().unwrap()) as usize;
|
||||
|
||||
if chunk_id == b"ANIM" {
|
||||
return true;
|
||||
}
|
||||
|
||||
let advance = 8 + size + (size % 2);
|
||||
offset = offset.saturating_add(advance);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_avif_file(input: &[u8]) -> bool {
|
||||
if input.len() < 12 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let box_type = &input[4..8];
|
||||
|
||||
if box_type != b"ftyp" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let brand = &input[8..12];
|
||||
brand == b"avif" || brand == b"avis"
|
||||
}
|
||||
|
||||
fn has_avif_anim(input: &[u8]) -> bool {
|
||||
if !is_avif_file(input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if input.len() < 12 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let brand = &input[8..12];
|
||||
brand == b"avis"
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_animated_image(input: &[u8]) -> bool {
|
||||
match detect_format(input) {
|
||||
ImageFormat::Gif => is_animated_gif(input),
|
||||
ImageFormat::Png => has_apng_actl(input),
|
||||
ImageFormat::Webp => has_webp_anim(input),
|
||||
ImageFormat::Avif => has_avif_anim(input),
|
||||
ImageFormat::Unknown => false,
|
||||
}
|
||||
}
|
||||
411
fluxer_app/crates/libfluxcore/src/apng.rs
Normal file
411
fluxer_app/crates/libfluxcore/src/apng.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use png::{BlendOp, DisposeOp};
|
||||
use std::io::Cursor;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn crop_and_rotate_apng(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation_deg: u32,
|
||||
resize_width: Option<u32>,
|
||||
resize_height: Option<u32>,
|
||||
) -> Result<Box<[u8]>, JsValue> {
|
||||
process_apng(
|
||||
input,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotation_deg,
|
||||
resize_width,
|
||||
resize_height,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn process_apng(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation_deg: u32,
|
||||
resize_width: Option<u32>,
|
||||
resize_height: Option<u32>,
|
||||
) -> Result<Box<[u8]>, JsValue> {
|
||||
let cursor = Cursor::new(input);
|
||||
let mut decoder = png::Decoder::new(cursor);
|
||||
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
|
||||
|
||||
let mut reader = decoder
|
||||
.read_info()
|
||||
.map_err(|e| JsValue::from_str(&format!("png read_info: {e}")))?;
|
||||
|
||||
let info = reader.info();
|
||||
let animation_control = info
|
||||
.animation_control()
|
||||
.ok_or_else(|| JsValue::from_str("Not an animated PNG"))?;
|
||||
|
||||
let screen_width = info.width;
|
||||
let screen_height = info.height;
|
||||
|
||||
let crop_x = x.min(screen_width);
|
||||
let crop_y = y.min(screen_height);
|
||||
let crop_w = width.min(screen_width - crop_x);
|
||||
let crop_h = height.min(screen_height - crop_y);
|
||||
|
||||
if crop_w == 0 || crop_h == 0 {
|
||||
return Err(JsValue::from_str("Crop area is empty"));
|
||||
}
|
||||
|
||||
let rotation = rotation_deg.rem_euclid(360);
|
||||
|
||||
let (base_w, base_h) = match rotation {
|
||||
90 | 270 => (crop_h, crop_w),
|
||||
_ => (crop_w, crop_h),
|
||||
};
|
||||
|
||||
let (target_w, target_h) = match (
|
||||
resize_width.filter(|w| *w > 0),
|
||||
resize_height.filter(|h| *h > 0),
|
||||
) {
|
||||
(Some(w), Some(h)) => (w, h),
|
||||
_ => (base_w, base_h),
|
||||
};
|
||||
|
||||
if target_w == 0 || target_h == 0 {
|
||||
return Err(JsValue::from_str("Target dimensions are empty"));
|
||||
}
|
||||
|
||||
if crop_x == 0
|
||||
&& crop_y == 0
|
||||
&& crop_w == screen_width
|
||||
&& crop_h == screen_height
|
||||
&& rotation == 0
|
||||
&& target_w == screen_width
|
||||
&& target_h == screen_height
|
||||
{
|
||||
return Ok(input.to_vec().into_boxed_slice());
|
||||
}
|
||||
|
||||
let mut output = Cursor::new(Vec::new());
|
||||
let mut encoder = png::Encoder::new(&mut output, target_w, target_h);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder
|
||||
.set_animated(animation_control.num_frames, animation_control.num_plays)
|
||||
.map_err(|e| JsValue::from_str(&format!("png set_animated: {e}")))?;
|
||||
encoder.validate_sequence(true);
|
||||
let mut writer = encoder
|
||||
.write_header()
|
||||
.map_err(|e| JsValue::from_str(&format!("png write_header: {e}")))?;
|
||||
|
||||
let mut canvas = vec![0u8; (screen_width * screen_height * 4) as usize];
|
||||
let mut previous_canvas: Option<Vec<u8>> = None;
|
||||
|
||||
let mut processed_any = false;
|
||||
const MAX_TOTAL_PIXELS: u64 = 200_000_000;
|
||||
let mut processed_pixels: u64 = 0;
|
||||
|
||||
let mut frame_buffer = vec![0u8; (screen_width * screen_height * 4) as usize];
|
||||
|
||||
while let Ok(frame_info) = reader.next_frame_info() {
|
||||
processed_any = true;
|
||||
|
||||
let dispose_op = frame_info.dispose_op;
|
||||
let blend_op = frame_info.blend_op;
|
||||
let delay_num = frame_info.delay_num;
|
||||
let delay_den = frame_info.delay_den;
|
||||
let fx = frame_info.x_offset as usize;
|
||||
let fy = frame_info.y_offset as usize;
|
||||
let fw = frame_info.width as usize;
|
||||
let fh = frame_info.height as usize;
|
||||
let rect_x = frame_info.x_offset;
|
||||
let rect_y = frame_info.y_offset;
|
||||
let rect_w = frame_info.width;
|
||||
let rect_h = frame_info.height;
|
||||
|
||||
if dispose_op == DisposeOp::Previous {
|
||||
previous_canvas = Some(canvas.clone());
|
||||
}
|
||||
|
||||
reader
|
||||
.next_frame(&mut frame_buffer)
|
||||
.map_err(|e| JsValue::from_str(&format!("png next_frame: {e}")))?;
|
||||
|
||||
if blend_op == BlendOp::Source {
|
||||
draw_frame_on_canvas_source(&mut canvas, screen_width, fx, fy, fw, fh, &frame_buffer);
|
||||
} else {
|
||||
draw_frame_on_canvas_over(&mut canvas, screen_width, fx, fy, fw, fh, &frame_buffer);
|
||||
}
|
||||
|
||||
let (cw, ch) = (crop_w as usize, crop_h as usize);
|
||||
let cropped = crop_rgba(
|
||||
&canvas,
|
||||
screen_width as usize,
|
||||
screen_height as usize,
|
||||
crop_x as usize,
|
||||
crop_y as usize,
|
||||
cw,
|
||||
ch,
|
||||
)?;
|
||||
|
||||
let (rotated, rw, rh) = match rotation {
|
||||
90 => rotate_rgba_90(&cropped, cw, ch),
|
||||
180 => rotate_rgba_180(&cropped, cw, ch),
|
||||
270 => rotate_rgba_270(&cropped, cw, ch),
|
||||
_ => (cropped, cw, ch),
|
||||
};
|
||||
|
||||
let (final_rgba, _fw, _fh) = if target_w as usize != rw || target_h as usize != rh {
|
||||
let resized =
|
||||
resize_rgba_nearest(&rotated, rw, rh, target_w as usize, target_h as usize);
|
||||
(resized, target_w as usize, target_h as usize)
|
||||
} else {
|
||||
(rotated, rw, rh)
|
||||
};
|
||||
|
||||
processed_pixels += (final_rgba.len() / 4) as u64;
|
||||
if processed_pixels > MAX_TOTAL_PIXELS {
|
||||
return Err(JsValue::from_str(
|
||||
"Animated PNG is too large to crop. Try reducing its dimensions or number of frames.",
|
||||
));
|
||||
}
|
||||
|
||||
writer
|
||||
.set_frame_delay(delay_num, delay_den)
|
||||
.map_err(|e| JsValue::from_str(&format!("png set_frame_delay: {e}")))?;
|
||||
|
||||
writer
|
||||
.set_dispose_op(dispose_op)
|
||||
.map_err(|e| JsValue::from_str(&format!("png set_dispose_op: {e}")))?;
|
||||
writer
|
||||
.set_blend_op(blend_op)
|
||||
.map_err(|e| JsValue::from_str(&format!("png set_blend_op: {e}")))?;
|
||||
|
||||
writer
|
||||
.write_image_data(&final_rgba)
|
||||
.map_err(|e| JsValue::from_str(&format!("png write_image_data: {e}")))?;
|
||||
|
||||
match dispose_op {
|
||||
DisposeOp::Background => {
|
||||
clear_rect(&mut canvas, screen_width, rect_x, rect_y, rect_w, rect_h);
|
||||
}
|
||||
DisposeOp::Previous => {
|
||||
if let Some(prev) = previous_canvas.take() {
|
||||
canvas = prev;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !processed_any {
|
||||
return Err(JsValue::from_str("APNG has no frames"));
|
||||
}
|
||||
|
||||
writer
|
||||
.finish()
|
||||
.map_err(|e| JsValue::from_str(&format!("png finish: {e}")))?;
|
||||
|
||||
Ok(output.into_inner().into_boxed_slice())
|
||||
}
|
||||
|
||||
fn draw_frame_on_canvas_source(
|
||||
canvas: &mut [u8],
|
||||
canvas_width: u32,
|
||||
fx: usize,
|
||||
fy: usize,
|
||||
fw: usize,
|
||||
fh: usize,
|
||||
buffer: &[u8],
|
||||
) {
|
||||
let cw = canvas_width as usize;
|
||||
|
||||
for row in 0..fh {
|
||||
let canvas_y = fy + row;
|
||||
let canvas_offset = (canvas_y * cw + fx) * 4;
|
||||
let frame_offset = row * fw * 4;
|
||||
|
||||
if canvas_offset + fw * 4 <= canvas.len() && frame_offset + fw * 4 <= buffer.len() {
|
||||
canvas[canvas_offset..canvas_offset + fw * 4]
|
||||
.copy_from_slice(&buffer[frame_offset..frame_offset + fw * 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_frame_on_canvas_over(
|
||||
canvas: &mut [u8],
|
||||
canvas_width: u32,
|
||||
fx: usize,
|
||||
fy: usize,
|
||||
fw: usize,
|
||||
fh: usize,
|
||||
buffer: &[u8],
|
||||
) {
|
||||
let cw = canvas_width as usize;
|
||||
|
||||
for row in 0..fh {
|
||||
let canvas_y = fy + row;
|
||||
let canvas_offset = (canvas_y * cw + fx) * 4;
|
||||
let frame_offset = row * fw * 4;
|
||||
|
||||
if canvas_offset + fw * 4 <= canvas.len() && frame_offset + fw * 4 <= buffer.len() {
|
||||
let frame_row = &buffer[frame_offset..frame_offset + fw * 4];
|
||||
let canvas_row = &mut canvas[canvas_offset..canvas_offset + fw * 4];
|
||||
|
||||
for i in 0..fw {
|
||||
let pixel_idx = i * 4;
|
||||
let alpha = frame_row[pixel_idx + 3];
|
||||
if alpha > 0 {
|
||||
canvas_row[pixel_idx] = frame_row[pixel_idx];
|
||||
canvas_row[pixel_idx + 1] = frame_row[pixel_idx + 1];
|
||||
canvas_row[pixel_idx + 2] = frame_row[pixel_idx + 2];
|
||||
canvas_row[pixel_idx + 3] = frame_row[pixel_idx + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_rect(canvas: &mut [u8], canvas_width: u32, x: u32, y: u32, w: u32, h: u32) {
|
||||
let cw = canvas_width as usize;
|
||||
let x = x as usize;
|
||||
let y = y as usize;
|
||||
let w = w as usize;
|
||||
let h = h as usize;
|
||||
|
||||
for row in 0..h {
|
||||
let canvas_y = y + row;
|
||||
let offset = (canvas_y * cw + x) * 4;
|
||||
if offset + w * 4 <= canvas.len() {
|
||||
for i in 0..w {
|
||||
let idx = offset + i * 4;
|
||||
canvas[idx] = 0;
|
||||
canvas[idx + 1] = 0;
|
||||
canvas[idx + 2] = 0;
|
||||
canvas[idx + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn crop_rgba(
|
||||
src: &[u8],
|
||||
src_w: usize,
|
||||
src_h: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
if x + w > src_w || y + h > src_h {
|
||||
return Err(JsValue::from_str("Crop rect out of bounds"));
|
||||
}
|
||||
|
||||
let mut dst = vec![0u8; w * h * 4];
|
||||
|
||||
for row in 0..h {
|
||||
let src_y = y + row;
|
||||
let src_offset = (src_y * src_w + x) * 4;
|
||||
let dst_offset = row * w * 4;
|
||||
dst[dst_offset..dst_offset + w * 4].copy_from_slice(&src[src_offset..src_offset + w * 4]);
|
||||
}
|
||||
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
fn rotate_rgba_90(src: &[u8], src_w: usize, src_h: usize) -> (Vec<u8>, usize, usize) {
|
||||
let dst_w = src_h;
|
||||
let dst_h = src_w;
|
||||
let mut dst = vec![0u8; dst_w * dst_h * 4];
|
||||
|
||||
for y in 0..src_h {
|
||||
for x in 0..src_w {
|
||||
let src_idx = (y * src_w + x) * 4;
|
||||
let dst_x = src_h - 1 - y;
|
||||
let dst_y = x;
|
||||
let dst_idx = (dst_y * dst_w + dst_x) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
(dst, dst_w, dst_h)
|
||||
}
|
||||
|
||||
fn rotate_rgba_180(src: &[u8], src_w: usize, src_h: usize) -> (Vec<u8>, usize, usize) {
|
||||
let mut dst = vec![0u8; src.len()];
|
||||
for y in 0..src_h {
|
||||
for x in 0..src_w {
|
||||
let src_idx = (y * src_w + x) * 4;
|
||||
let dst_x = src_w - 1 - x;
|
||||
let dst_y = src_h - 1 - y;
|
||||
let dst_idx = (dst_y * src_w + dst_x) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
(dst, src_w, src_h)
|
||||
}
|
||||
|
||||
fn rotate_rgba_270(src: &[u8], src_w: usize, src_h: usize) -> (Vec<u8>, usize, usize) {
|
||||
let dst_w = src_h;
|
||||
let dst_h = src_w;
|
||||
let mut dst = vec![0u8; dst_w * dst_h * 4];
|
||||
|
||||
for y in 0..src_h {
|
||||
for x in 0..src_w {
|
||||
let src_idx = (y * src_w + x) * 4;
|
||||
let dst_x = y;
|
||||
let dst_y = dst_h - 1 - x;
|
||||
let dst_idx = (dst_y * dst_w + dst_x) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
(dst, dst_w, dst_h)
|
||||
}
|
||||
|
||||
fn resize_rgba_nearest(
|
||||
src: &[u8],
|
||||
src_w: usize,
|
||||
src_h: usize,
|
||||
dst_w: usize,
|
||||
dst_h: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut dst = vec![0u8; dst_w * dst_h * 4];
|
||||
|
||||
for dy in 0..dst_h {
|
||||
let sy = dy * src_h / dst_h;
|
||||
for dx in 0..dst_w {
|
||||
let sx = dx * src_w / dst_w;
|
||||
let src_idx = (sy * src_w + sx) * 4;
|
||||
let dst_idx = (dy * dst_w + dx) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
dst
|
||||
}
|
||||
@@ -35,6 +35,7 @@ impl From<JsValue> for EncodeError {
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn crop_and_rotate_gif(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
@@ -83,6 +84,7 @@ enum EncoderMode {
|
||||
Quantized,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn process_gif(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
@@ -451,13 +453,15 @@ impl PaletteFrameEncoder {
|
||||
transparent_index,
|
||||
} = PaletteFrameData::from_rgba(&rgba)?;
|
||||
|
||||
let mut frame = Frame::default();
|
||||
frame.width = self.width;
|
||||
frame.height = self.height;
|
||||
frame.delay = delay;
|
||||
frame.buffer = Cow::Owned(indices);
|
||||
frame.palette = Some(palette);
|
||||
frame.transparent = transparent_index;
|
||||
let frame = Frame {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
delay,
|
||||
buffer: Cow::Owned(indices),
|
||||
palette: Some(palette),
|
||||
transparent: transparent_index,
|
||||
..Frame::default()
|
||||
};
|
||||
self.encoder.write_frame(&frame).map_err(map_encoding_error)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod gif;
|
||||
pub mod animation;
|
||||
pub mod apng;
|
||||
pub mod gateway;
|
||||
pub mod gif;
|
||||
pub mod static_image;
|
||||
|
||||
pub use gif::crop_and_rotate_gif;
|
||||
pub use animation::is_animated_image;
|
||||
pub use apng::crop_and_rotate_apng;
|
||||
pub use gateway::decompress_zstd_frame;
|
||||
pub use gif::crop_and_rotate_gif;
|
||||
pub use static_image::crop_and_rotate_image;
|
||||
|
||||
98
fluxer_app/crates/libfluxcore/src/static_image.rs
Normal file
98
fluxer_app/crates/libfluxcore/src/static_image.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, RgbaImage, imageops};
|
||||
use std::io::Cursor;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
fn normalize_rotation(rotation_deg: u32) -> u32 {
|
||||
rotation_deg % 360
|
||||
}
|
||||
|
||||
fn map_format_hint(hint: &str) -> Result<ImageFormat, JsValue> {
|
||||
match hint.trim().to_lowercase().as_str() {
|
||||
"png" | "apng" => Ok(ImageFormat::Png),
|
||||
"jpeg" | "jpg" => Ok(ImageFormat::Jpeg),
|
||||
"webp" => Ok(ImageFormat::WebP),
|
||||
"avif" => Ok(ImageFormat::Avif),
|
||||
"gif" => Ok(ImageFormat::Gif),
|
||||
_ => Err(JsValue::from_str("Unsupported static format")),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn crop_and_rotate_image(
|
||||
input: &[u8],
|
||||
format_hint: &str,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation_deg: u32,
|
||||
resize_width: Option<u32>,
|
||||
resize_height: Option<u32>,
|
||||
) -> Result<Box<[u8]>, JsValue> {
|
||||
let format = map_format_hint(format_hint)?;
|
||||
let dynamic_image = image::load_from_memory_with_format(input, format)
|
||||
.map_err(|err| JsValue::from_str(&format!("Failed to decode {format_hint}: {err}")))?;
|
||||
|
||||
let (img_w, img_h) = dynamic_image.dimensions();
|
||||
let crop_x = x.min(img_w);
|
||||
let crop_y = y.min(img_h);
|
||||
let crop_w = width.min(img_w.saturating_sub(crop_x));
|
||||
let crop_h = height.min(img_h.saturating_sub(crop_y));
|
||||
|
||||
if crop_w == 0 || crop_h == 0 {
|
||||
return Err(JsValue::from_str("Crop area is empty"));
|
||||
}
|
||||
|
||||
let cropped: RgbaImage = dynamic_image
|
||||
.crop_imm(crop_x, crop_y, crop_w, crop_h)
|
||||
.to_rgba8();
|
||||
|
||||
let rotated = match normalize_rotation(rotation_deg) {
|
||||
90 => imageops::rotate90(&cropped),
|
||||
180 => imageops::rotate180(&cropped),
|
||||
270 => imageops::rotate270(&cropped),
|
||||
_ => cropped.clone(),
|
||||
};
|
||||
|
||||
let target_w = resize_width.filter(|w| *w > 0).unwrap_or(rotated.width());
|
||||
let target_h = resize_height.filter(|h| *h > 0).unwrap_or(rotated.height());
|
||||
|
||||
if target_w == 0 || target_h == 0 {
|
||||
return Err(JsValue::from_str("Target dimensions are empty"));
|
||||
}
|
||||
|
||||
let final_buffer = if target_w == rotated.width() && target_h == rotated.height() {
|
||||
rotated
|
||||
} else {
|
||||
imageops::resize(&rotated, target_w, target_h, FilterType::Lanczos3)
|
||||
};
|
||||
|
||||
let final_frame = DynamicImage::ImageRgba8(final_buffer);
|
||||
let mut output = Cursor::new(Vec::new());
|
||||
final_frame
|
||||
.write_to(&mut output, format)
|
||||
.map_err(|err| JsValue::from_str(&format!("Failed to encode {format_hint}: {err}")))?;
|
||||
|
||||
Ok(output.into_inner().into_boxed_slice())
|
||||
}
|
||||
Reference in New Issue
Block a user