initial commit
This commit is contained in:
144
fluxer_app/crates/libfluxcore/Cargo.lock
generated
Normal file
144
fluxer_app/crates/libfluxcore/Cargo.lock
generated
Normal file
@@ -0,0 +1,144 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libfluxcore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gif",
|
||||
"ruzstd",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ruzstd"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
12
fluxer_app/crates/libfluxcore/Cargo.toml
Normal file
12
fluxer_app/crates/libfluxcore/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "libfluxcore"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
gif = "0.13"
|
||||
ruzstd = { version = "0.7", default-features = false, features = ["std"] }
|
||||
wasm-bindgen = { version = "0.2", features = ["std"] }
|
||||
35
fluxer_app/crates/libfluxcore/src/gateway.rs
Normal file
35
fluxer_app/crates/libfluxcore/src/gateway.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 ruzstd::StreamingDecoder;
|
||||
use std::io::{Cursor, Read};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn decompress_zstd_frame(input: &[u8]) -> Result<Box<[u8]>, JsValue> {
|
||||
let mut decoder = StreamingDecoder::new(Cursor::new(input))
|
||||
.map_err(|e| JsValue::from_str(&format!("zstd init error: {e}")))?;
|
||||
|
||||
let mut output = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut output)
|
||||
.map_err(|e| JsValue::from_str(&format!("zstd read error: {e}")))?;
|
||||
|
||||
Ok(output.into_boxed_slice())
|
||||
}
|
||||
566
fluxer_app/crates/libfluxcore/src/gif.rs
Normal file
566
fluxer_app/crates/libfluxcore/src/gif.rs
Normal file
@@ -0,0 +1,566 @@
|
||||
/*
|
||||
* 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, DisposalMethod, Encoder as GifEncoder, Frame, Repeat};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
enum EncodeError {
|
||||
TooManyColors,
|
||||
Js(JsValue),
|
||||
}
|
||||
|
||||
impl From<JsValue> for EncodeError {
|
||||
fn from(value: JsValue) -> Self {
|
||||
Self::Js(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn crop_and_rotate_gif(
|
||||
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> {
|
||||
match process_gif(
|
||||
input,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotation_deg,
|
||||
resize_width,
|
||||
resize_height,
|
||||
EncoderMode::Palette,
|
||||
) {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(EncodeError::TooManyColors) => process_gif(
|
||||
input,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotation_deg,
|
||||
resize_width,
|
||||
resize_height,
|
||||
EncoderMode::Quantized,
|
||||
)
|
||||
.map_err(|err| match err {
|
||||
EncodeError::Js(js) => js,
|
||||
EncodeError::TooManyColors => {
|
||||
JsValue::from_str("GIF contains more than 256 unique colors")
|
||||
}
|
||||
}),
|
||||
Err(EncodeError::Js(js)) => Err(js),
|
||||
}
|
||||
}
|
||||
|
||||
enum EncoderMode {
|
||||
Palette,
|
||||
Quantized,
|
||||
}
|
||||
|
||||
fn process_gif(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation_deg: u32,
|
||||
resize_width: Option<u32>,
|
||||
resize_height: Option<u32>,
|
||||
mode: EncoderMode,
|
||||
) -> Result<Box<[u8]>, EncodeError> {
|
||||
let mut decoder = create_decoder(input)?;
|
||||
|
||||
let screen_width = decoder.width() as u32;
|
||||
let screen_height = decoder.height() as u32;
|
||||
|
||||
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(EncodeError::Js(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(EncodeError::Js(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 frame_encoder = FrameEncoder::new(mode, target_w as u16, target_h as u16)?;
|
||||
|
||||
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;
|
||||
|
||||
while let Some(frame) = decoder
|
||||
.read_next_frame()
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("gif read_next_frame: {e}"))))?
|
||||
{
|
||||
processed_any = true;
|
||||
|
||||
if frame.dispose == DisposalMethod::Previous {
|
||||
previous_canvas = Some(canvas.clone());
|
||||
}
|
||||
|
||||
draw_frame_on_canvas(
|
||||
&mut canvas,
|
||||
screen_width,
|
||||
frame.left,
|
||||
frame.top,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.buffer.as_ref(),
|
||||
);
|
||||
|
||||
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(EncodeError::Js(JsValue::from_str(
|
||||
"Animated GIF is too large to crop. Try reducing its dimensions or number of frames.",
|
||||
)));
|
||||
}
|
||||
|
||||
frame_encoder.write_frame(final_rgba, frame.delay)?;
|
||||
|
||||
match frame.dispose {
|
||||
DisposalMethod::Background => {
|
||||
clear_rect(
|
||||
&mut canvas,
|
||||
screen_width,
|
||||
frame.left,
|
||||
frame.top,
|
||||
frame.width,
|
||||
frame.height,
|
||||
);
|
||||
}
|
||||
DisposalMethod::Previous => {
|
||||
if let Some(prev) = previous_canvas.take() {
|
||||
canvas = prev;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !processed_any {
|
||||
return Err(EncodeError::Js(JsValue::from_str("GIF has no frames")));
|
||||
}
|
||||
|
||||
frame_encoder.finish()
|
||||
}
|
||||
|
||||
fn draw_frame_on_canvas(
|
||||
canvas: &mut [u8],
|
||||
canvas_width: u32,
|
||||
left: u16,
|
||||
top: u16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
buffer: &[u8],
|
||||
) {
|
||||
let fw = width as usize;
|
||||
let fh = height as usize;
|
||||
let fx = left as usize;
|
||||
let fy = top as usize;
|
||||
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;
|
||||
|
||||
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: u16, y: u16, w: u16, h: u16) {
|
||||
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;
|
||||
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
|
||||
}
|
||||
|
||||
fn create_decoder(input: &[u8]) -> Result<gif::Decoder<Cursor<&[u8]>>, EncodeError> {
|
||||
let cursor = Cursor::new(input);
|
||||
let mut options = DecodeOptions::new();
|
||||
options.set_color_output(ColorOutput::RGBA);
|
||||
options
|
||||
.read_info(cursor)
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("gif read_info: {e}"))))
|
||||
}
|
||||
|
||||
enum FrameEncoder {
|
||||
Palette(PaletteFrameEncoder),
|
||||
Quantized(QuantizedFrameEncoder),
|
||||
}
|
||||
|
||||
impl FrameEncoder {
|
||||
fn new(mode: EncoderMode, width: u16, height: u16) -> Result<Self, EncodeError> {
|
||||
match mode {
|
||||
EncoderMode::Palette => PaletteFrameEncoder::new(width, height).map(Self::Palette),
|
||||
EncoderMode::Quantized => {
|
||||
QuantizedFrameEncoder::new(width, height).map(Self::Quantized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_frame(&mut self, rgba: Vec<u8>, delay: u16) -> Result<(), EncodeError> {
|
||||
match self {
|
||||
Self::Palette(enc) => enc.write_frame(rgba, delay),
|
||||
Self::Quantized(enc) => enc.write_frame(rgba, delay),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Box<[u8]>, EncodeError> {
|
||||
match self {
|
||||
Self::Palette(enc) => enc.finish(),
|
||||
Self::Quantized(enc) => enc.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteFrameEncoder {
|
||||
encoder: GifEncoder<Cursor<Vec<u8>>>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl PaletteFrameEncoder {
|
||||
fn new(width: u16, height: u16) -> Result<Self, EncodeError> {
|
||||
let cursor = Cursor::new(Vec::new());
|
||||
let mut encoder = GifEncoder::new(cursor, width, height, &[])
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("GifEncoder::new: {e}"))))?;
|
||||
encoder
|
||||
.set_repeat(Repeat::Infinite)
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("set_repeat: {e}"))))?;
|
||||
Ok(Self {
|
||||
encoder,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_frame(&mut self, rgba: Vec<u8>, delay: u16) -> Result<(), EncodeError> {
|
||||
let PaletteFrameData {
|
||||
indices,
|
||||
palette,
|
||||
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;
|
||||
self.encoder.write_frame(&frame).map_err(map_encoding_error)
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Box<[u8]>, EncodeError> {
|
||||
let cursor = self.encoder.into_inner().map_err(map_io_error)?;
|
||||
Ok(cursor.into_inner().into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteFrameData {
|
||||
indices: Vec<u8>,
|
||||
palette: Vec<u8>,
|
||||
transparent_index: Option<u8>,
|
||||
}
|
||||
|
||||
impl PaletteFrameData {
|
||||
fn from_rgba(rgba: &[u8]) -> Result<Self, EncodeError> {
|
||||
let mut palette = Vec::with_capacity(256 * 3);
|
||||
let mut color_to_index = HashMap::with_capacity(256);
|
||||
let mut transparent_index = None;
|
||||
let mut indices = Vec::with_capacity(rgba.len() / 4);
|
||||
|
||||
for pixel in rgba.chunks_exact(4) {
|
||||
let idx = if pixel[3] == 0 {
|
||||
if let Some(idx) = transparent_index {
|
||||
idx
|
||||
} else {
|
||||
let next_index = palette.len() / 3;
|
||||
if next_index >= 256 {
|
||||
return Err(EncodeError::TooManyColors);
|
||||
}
|
||||
palette.extend_from_slice(&[0, 0, 0]);
|
||||
let idx = next_index as u8;
|
||||
transparent_index = Some(idx);
|
||||
idx
|
||||
}
|
||||
} else {
|
||||
let key = [pixel[0], pixel[1], pixel[2]];
|
||||
if let Some(&idx) = color_to_index.get(&key) {
|
||||
idx
|
||||
} else {
|
||||
let next_index = palette.len() / 3;
|
||||
if next_index >= 256 {
|
||||
return Err(EncodeError::TooManyColors);
|
||||
}
|
||||
palette.extend_from_slice(&key);
|
||||
let idx = next_index as u8;
|
||||
color_to_index.insert(key, idx);
|
||||
idx
|
||||
}
|
||||
};
|
||||
indices.push(idx);
|
||||
}
|
||||
|
||||
if palette.is_empty() {
|
||||
palette.extend_from_slice(&[0, 0, 0]);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
indices,
|
||||
palette,
|
||||
transparent_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct QuantizedFrameEncoder {
|
||||
encoder: GifEncoder<Cursor<Vec<u8>>>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl QuantizedFrameEncoder {
|
||||
fn new(width: u16, height: u16) -> Result<Self, EncodeError> {
|
||||
let cursor = Cursor::new(Vec::new());
|
||||
let mut encoder = GifEncoder::new(cursor, width, height, &[])
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("GifEncoder::new: {e}"))))?;
|
||||
encoder
|
||||
.set_repeat(Repeat::Infinite)
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("set_repeat: {e}"))))?;
|
||||
Ok(Self {
|
||||
encoder,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_frame(&mut self, mut rgba: Vec<u8>, delay: u16) -> Result<(), EncodeError> {
|
||||
let mut frame = Frame::from_rgba_speed(self.width, self.height, &mut rgba, 10);
|
||||
frame.delay = delay;
|
||||
self.encoder.write_frame(&frame).map_err(map_encoding_error)
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Box<[u8]>, EncodeError> {
|
||||
let cursor = self.encoder.into_inner().map_err(map_io_error)?;
|
||||
Ok(cursor.into_inner().into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_encoding_error(err: gif::EncodingError) -> EncodeError {
|
||||
EncodeError::Js(JsValue::from_str(&format!("gif encode: {err}")))
|
||||
}
|
||||
|
||||
fn map_io_error(err: std::io::Error) -> EncodeError {
|
||||
EncodeError::Js(JsValue::from_str(&format!("gif io: {err}")))
|
||||
}
|
||||
24
fluxer_app/crates/libfluxcore/src/lib.rs
Normal file
24
fluxer_app/crates/libfluxcore/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
pub mod gif;
|
||||
pub mod gateway;
|
||||
|
||||
pub use gif::crop_and_rotate_gif;
|
||||
pub use gateway::decompress_zstd_frame;
|
||||
Reference in New Issue
Block a user