310 lines
10 KiB
Rust
310 lines
10 KiB
Rust
|
|
#![allow(dead_code)]
|
||
|
|
use hcie_engine_api::brush::{BrushStyle, BrushTip};
|
||
|
|
use hcie_engine_api::BrushPreset;
|
||
|
|
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||
|
|
|
||
|
|
pub fn import_abr(data: &[u8]) -> Vec<BrushPreset> {
|
||
|
|
let mut presets = Vec::new();
|
||
|
|
let mut cursor = Cursor::new(data);
|
||
|
|
|
||
|
|
let mut buf = [0u8; 4];
|
||
|
|
if cursor.read_exact(&mut buf).is_err() {
|
||
|
|
return presets;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut pos = 0;
|
||
|
|
while pos + 8 < data.len() {
|
||
|
|
if &data[pos..pos + 4] == b"8BIM" {
|
||
|
|
let tag = String::from_utf8_lossy(&data[pos + 4..pos + 8]).to_string();
|
||
|
|
let _ = cursor.seek(SeekFrom::Start(pos as u64 + 8));
|
||
|
|
let size = read_u32(&mut cursor);
|
||
|
|
let block_end = (cursor.position() + size as u64).min(data.len() as u64);
|
||
|
|
|
||
|
|
if tag == "samp" {
|
||
|
|
let start = cursor.position() as usize;
|
||
|
|
let end = block_end as usize;
|
||
|
|
if end > start && end <= data.len() {
|
||
|
|
presets.extend(harvest_sampled_brushes(&data[start..end], start));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
pos = (block_end as usize + 1) & !1;
|
||
|
|
} else {
|
||
|
|
pos += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
presets.extend(harvest_computed_brushes(data));
|
||
|
|
presets
|
||
|
|
}
|
||
|
|
|
||
|
|
fn safe_read_i32(data: &[u8], pos: usize) -> Option<i32> {
|
||
|
|
if pos + 4 > data.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(i32::from_be_bytes([
|
||
|
|
data[pos],
|
||
|
|
data[pos + 1],
|
||
|
|
data[pos + 2],
|
||
|
|
data[pos + 3],
|
||
|
|
]))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn harvest_sampled_brushes(data: &[u8], base_pos: usize) -> Vec<BrushPreset> {
|
||
|
|
let mut presets = Vec::new();
|
||
|
|
let mut i = 0;
|
||
|
|
while i + 20 < data.len() {
|
||
|
|
if data[i] == 8 && data[i + 1] == 1 {
|
||
|
|
let sig_pos = if i > 0 && data[i - 1] == 0 { i - 1 } else { i };
|
||
|
|
let mut found = false;
|
||
|
|
if sig_pos >= 16 {
|
||
|
|
let t = safe_read_i32(data, sig_pos - 16);
|
||
|
|
let l = safe_read_i32(data, sig_pos - 12);
|
||
|
|
let b = safe_read_i32(data, sig_pos - 8);
|
||
|
|
let r = safe_read_i32(data, sig_pos - 4);
|
||
|
|
if let (Some(t), Some(l), Some(b), Some(r)) = (t, l, b, r) {
|
||
|
|
let h = (b as i64 - t as i64).abs() as u32;
|
||
|
|
let w = (r as i64 - l as i64).abs() as u32;
|
||
|
|
if w > 0 && h > 0 && w < 10000 && h < 10000 {
|
||
|
|
let (final_w, final_h) = if w > 1024 || h > 1024 {
|
||
|
|
let scale = 1024.0 / (w.max(h) as f32);
|
||
|
|
((w as f32 * scale) as u32, (h as f32 * scale) as u32)
|
||
|
|
} else {
|
||
|
|
(w, h)
|
||
|
|
};
|
||
|
|
if let Some(decoded) = try_decode_and_scale(
|
||
|
|
&data[i + 2..],
|
||
|
|
w as usize,
|
||
|
|
h as usize,
|
||
|
|
final_w,
|
||
|
|
final_h,
|
||
|
|
) {
|
||
|
|
presets.push(BrushPreset {
|
||
|
|
id: format!("abr_v6_{}_{}x{}", base_pos + i, final_w, final_h),
|
||
|
|
name: format!("Brush {}x{}", final_w, final_h),
|
||
|
|
category: "Imported (ABR)".to_string(),
|
||
|
|
tip: BrushTip {
|
||
|
|
style: BrushStyle::Bitmap,
|
||
|
|
size: final_w.max(final_h) as f32,
|
||
|
|
opacity: 1.0,
|
||
|
|
hardness: 1.0,
|
||
|
|
spacing: 0.1,
|
||
|
|
jitter_amount: 0.0,
|
||
|
|
scatter_amount: 0.0,
|
||
|
|
angle: 0.0,
|
||
|
|
roundness: 1.0,
|
||
|
|
spray_particle_size: 2.0,
|
||
|
|
spray_density: 100,
|
||
|
|
bitmap_pixels: decoded,
|
||
|
|
bitmap_w: final_w,
|
||
|
|
bitmap_h: final_h,
|
||
|
|
flow: 1.0,
|
||
|
|
color_variant: false,
|
||
|
|
variant_amount: 0.0,
|
||
|
|
density: 1.0,
|
||
|
|
drawing_angle: false,
|
||
|
|
rotation_random: 0.0,
|
||
|
|
},
|
||
|
|
style: BrushStyle::Bitmap,
|
||
|
|
pressure_size: true,
|
||
|
|
pressure_opacity: true,
|
||
|
|
pressure_flow: true,
|
||
|
|
jitter_position: 0.0,
|
||
|
|
jitter_angle: 0.0,
|
||
|
|
});
|
||
|
|
found = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if found {
|
||
|
|
i += 20;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
presets
|
||
|
|
}
|
||
|
|
|
||
|
|
fn try_decode_and_scale(
|
||
|
|
data: &[u8],
|
||
|
|
src_w: usize,
|
||
|
|
src_h: usize,
|
||
|
|
dst_w: u32,
|
||
|
|
dst_h: u32,
|
||
|
|
) -> Option<Vec<u8>> {
|
||
|
|
let decoded = decode_packbits_lenient(data, src_w * src_h);
|
||
|
|
if decoded.len() != src_w * src_h {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
if src_w as u32 == dst_w && src_h as u32 == dst_h {
|
||
|
|
return Some(decoded);
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut scaled = Vec::with_capacity((dst_w * dst_h) as usize);
|
||
|
|
for y in 0..dst_h {
|
||
|
|
for x in 0..dst_w {
|
||
|
|
let src_x = (x as f32 * (src_w as f32 / dst_w as f32)) as usize;
|
||
|
|
let src_y = (y as f32 * (src_h as f32 / dst_h as f32)) as usize;
|
||
|
|
scaled.push(decoded[src_y * src_w + src_x]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Some(scaled)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn decode_packbits_lenient(data: &[u8], expected_size: usize) -> Vec<u8> {
|
||
|
|
let mut out = Vec::with_capacity(expected_size);
|
||
|
|
let mut i = 0;
|
||
|
|
while i < data.len() && out.len() < expected_size {
|
||
|
|
let n = data[i] as i8;
|
||
|
|
i += 1;
|
||
|
|
if n >= 0 {
|
||
|
|
let count = n as usize + 1;
|
||
|
|
if i + count <= data.len() {
|
||
|
|
out.extend_from_slice(&data[i..i + count]);
|
||
|
|
i += count;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else if n != -128 {
|
||
|
|
let count = (-n) as usize + 1;
|
||
|
|
if i < data.len() {
|
||
|
|
let val = data[i];
|
||
|
|
i += 1;
|
||
|
|
for _ in 0..count {
|
||
|
|
out.push(val);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if out.is_empty() {
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
while out.len() < expected_size {
|
||
|
|
out.push(0);
|
||
|
|
}
|
||
|
|
if out.len() > expected_size {
|
||
|
|
out.truncate(expected_size);
|
||
|
|
}
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
fn harvest_computed_brushes(data: &[u8]) -> Vec<BrushPreset> {
|
||
|
|
let mut presets = Vec::new();
|
||
|
|
let marker = b"computedB";
|
||
|
|
let mut search_pos = 0;
|
||
|
|
while search_pos + marker.len() < data.len() {
|
||
|
|
if &data[search_pos..search_pos + marker.len()] == marker {
|
||
|
|
let window_end = (search_pos + 4096).min(data.len());
|
||
|
|
let window = &data[search_pos..window_end];
|
||
|
|
let name = parse_nm_text(window).unwrap_or_else(|| "Computed Brush".to_string());
|
||
|
|
let diameter = parse_f64_field(window, b"Dmtr").unwrap_or(10.0);
|
||
|
|
let hardness = parse_f64_field(window, b"Hrdn").unwrap_or(100.0);
|
||
|
|
let spacing = parse_f64_field(window, b"Spcn").unwrap_or(25.0);
|
||
|
|
if diameter > 0.0 && diameter < 5000.0 {
|
||
|
|
presets.push(BrushPreset {
|
||
|
|
id: format!("abr_comp_{:x}", search_pos),
|
||
|
|
name,
|
||
|
|
category: "Imported (ABR)".to_string(),
|
||
|
|
tip: BrushTip {
|
||
|
|
style: BrushStyle::Round,
|
||
|
|
size: diameter as f32,
|
||
|
|
opacity: 1.0,
|
||
|
|
hardness: (hardness / 100.0).clamp(0.0, 1.0) as f32,
|
||
|
|
spacing: (spacing / 100.0).clamp(0.01, 5.0) as f32,
|
||
|
|
jitter_amount: 0.0,
|
||
|
|
scatter_amount: 0.0,
|
||
|
|
angle: 0.0,
|
||
|
|
roundness: 1.0,
|
||
|
|
spray_particle_size: 2.0,
|
||
|
|
spray_density: 100,
|
||
|
|
bitmap_pixels: Vec::new(),
|
||
|
|
bitmap_w: 0,
|
||
|
|
bitmap_h: 0,
|
||
|
|
flow: 1.0,
|
||
|
|
color_variant: false,
|
||
|
|
variant_amount: 0.0,
|
||
|
|
density: 1.0,
|
||
|
|
drawing_angle: false,
|
||
|
|
rotation_random: 0.0,
|
||
|
|
},
|
||
|
|
style: BrushStyle::Round,
|
||
|
|
pressure_size: true,
|
||
|
|
pressure_opacity: true,
|
||
|
|
pressure_flow: true,
|
||
|
|
jitter_position: 0.0,
|
||
|
|
jitter_angle: 0.0,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
search_pos += marker.len();
|
||
|
|
} else {
|
||
|
|
search_pos += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
presets
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_nm_text(data: &[u8]) -> Option<String> {
|
||
|
|
let tag = b"Nm TEXT";
|
||
|
|
let pos = find_bytes(data, tag)?;
|
||
|
|
let p = pos + tag.len();
|
||
|
|
if p + 4 > data.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let nchars = u32::from_be_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]) as usize;
|
||
|
|
let p = p + 4;
|
||
|
|
if p + nchars * 2 > data.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let chars: Vec<u16> = data[p..p + nchars * 2]
|
||
|
|
.chunks_exact(2)
|
||
|
|
.map(|b| u16::from_be_bytes([b[0], b[1]]))
|
||
|
|
.collect();
|
||
|
|
let full = String::from_utf16_lossy(&chars);
|
||
|
|
let name = full
|
||
|
|
.split('=')
|
||
|
|
.last()
|
||
|
|
.unwrap_or(&full)
|
||
|
|
.trim_matches('\x00')
|
||
|
|
.to_string();
|
||
|
|
if name.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some(name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_f64_field(data: &[u8], tag: &[u8]) -> Option<f64> {
|
||
|
|
let pos = find_bytes(data, tag)?;
|
||
|
|
let p = pos + tag.len() + 8;
|
||
|
|
if p + 8 > data.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(f64::from_be_bytes([
|
||
|
|
data[p],
|
||
|
|
data[p + 1],
|
||
|
|
data[p + 2],
|
||
|
|
data[p + 3],
|
||
|
|
data[p + 4],
|
||
|
|
data[p + 5],
|
||
|
|
data[p + 6],
|
||
|
|
data[p + 7],
|
||
|
|
]))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||
|
|
haystack.windows(needle.len()).position(|w| w == needle)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_u32(cursor: &mut Cursor<&[u8]>) -> u32 {
|
||
|
|
let mut buf = [0u8; 4];
|
||
|
|
let _ = cursor.read_exact(&mut buf);
|
||
|
|
u32::from_be_bytes(buf)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn import_krita_preset(_data: &[u8]) -> Option<BrushPreset> {
|
||
|
|
None
|
||
|
|
}
|