Add: setting window for tauri - bugs not fixed yet
This commit is contained in:
@@ -1,24 +1,30 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use sprimo_platform::{PlatformAdapter, create_adapter};
|
||||
use sprimo_protocol::v1::FrontendCommand;
|
||||
use sprimo_platform::{create_adapter, PlatformAdapter};
|
||||
use sprimo_protocol::v1::{FrontendState, FrontendStateSnapshot};
|
||||
use sprimo_runtime_core::{RuntimeCore, RuntimeCoreError};
|
||||
use sprimo_sprite::{load_manifest, resolve_pack_path, AnimationDefinition};
|
||||
use sprimo_sprite::{AnimationDefinition, SpritePackManifest, load_manifest, resolve_pack_path};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use tauri::menu::{CheckMenuItem, Menu, MenuItem};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{AppHandle, Emitter, Manager, Wry};
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder, Wry};
|
||||
use thiserror::Error;
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::warn;
|
||||
|
||||
const APP_NAME: &str = "sprimo";
|
||||
const DEFAULT_PACK: &str = "default";
|
||||
const SPRITE_PNG_FILE: &str = "sprite.png";
|
||||
const SPRITE_GRID_COLUMNS: u32 = 8;
|
||||
const SPRITE_GRID_ROWS: u32 = 7;
|
||||
const MAIN_WINDOW_LABEL: &str = "main";
|
||||
const SETTINGS_WINDOW_LABEL: &str = "settings";
|
||||
const TRAY_ID: &str = "main";
|
||||
const MENU_ID_SETTINGS: &str = "settings";
|
||||
const MENU_ID_TOGGLE_VISIBILITY: &str = "toggle_visibility";
|
||||
const MENU_ID_TOGGLE_ALWAYS_ON_TOP: &str = "toggle_always_on_top";
|
||||
const MENU_ID_TOGGLE_DEBUG_OVERLAY: &str = "toggle_debug_overlay";
|
||||
@@ -50,6 +56,12 @@ struct UiSpritePack {
|
||||
anchor: UiAnchor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AtlasGeometry {
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct UiSnapshot {
|
||||
state: String,
|
||||
@@ -58,6 +70,22 @@ struct UiSnapshot {
|
||||
y: f32,
|
||||
scale: f32,
|
||||
active_sprite_pack: String,
|
||||
visible: bool,
|
||||
always_on_top: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct UiSettingsSnapshot {
|
||||
active_sprite_pack: String,
|
||||
scale: f32,
|
||||
visible: bool,
|
||||
always_on_top: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct UiSpritePackOption {
|
||||
id: String,
|
||||
pack_id_or_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -74,6 +102,7 @@ enum AppError {
|
||||
struct AppState {
|
||||
runtime_core: Arc<RuntimeCore>,
|
||||
runtime: Arc<Runtime>,
|
||||
tray_state: Arc<std::sync::Mutex<Option<TrayMenuState>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -96,6 +125,7 @@ fn current_state(state: tauri::State<'_, AppState>) -> Result<UiSnapshot, String
|
||||
|
||||
#[tauri::command]
|
||||
fn load_active_sprite_pack(state: tauri::State<'_, AppState>) -> Result<UiSpritePack, String> {
|
||||
let root = sprite_pack_root(state.runtime_core.as_ref())?;
|
||||
let config = state
|
||||
.runtime_core
|
||||
.config()
|
||||
@@ -103,11 +133,6 @@ fn load_active_sprite_pack(state: tauri::State<'_, AppState>) -> Result<UiSprite
|
||||
.map_err(|_| "config lock poisoned".to_string())?
|
||||
.clone();
|
||||
|
||||
let root = std::env::current_dir()
|
||||
.map_err(|err| err.to_string())?
|
||||
.join("assets")
|
||||
.join(config.sprite.sprite_packs_dir);
|
||||
|
||||
let selected = config.sprite.selected_pack;
|
||||
let pack_path = match resolve_pack_path(&root, &selected) {
|
||||
Ok(path) => path,
|
||||
@@ -117,6 +142,8 @@ fn load_active_sprite_pack(state: tauri::State<'_, AppState>) -> Result<UiSprite
|
||||
let manifest = load_manifest(&pack_path).map_err(|err| err.to_string())?;
|
||||
let image_path = pack_path.join(&manifest.image);
|
||||
let image_bytes = std::fs::read(&image_path).map_err(|err| err.to_string())?;
|
||||
let geometry = atlas_geometry_for_manifest(&manifest, &image_bytes)?;
|
||||
validate_animation_frames(&manifest)?;
|
||||
let atlas_data_url = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(image_bytes)
|
||||
@@ -124,14 +151,10 @@ fn load_active_sprite_pack(state: tauri::State<'_, AppState>) -> Result<UiSprite
|
||||
|
||||
Ok(UiSpritePack {
|
||||
id: manifest.id,
|
||||
frame_width: manifest.frame_width,
|
||||
frame_height: manifest.frame_height,
|
||||
frame_width: geometry.frame_width,
|
||||
frame_height: geometry.frame_height,
|
||||
atlas_data_url,
|
||||
animations: manifest
|
||||
.animations
|
||||
.into_iter()
|
||||
.map(to_ui_clip)
|
||||
.collect(),
|
||||
animations: manifest.animations.into_iter().map(to_ui_clip).collect(),
|
||||
anchor: UiAnchor {
|
||||
x: manifest.anchor.x,
|
||||
y: manifest.anchor.y,
|
||||
@@ -147,6 +170,139 @@ fn debug_overlay_visible(state: tauri::State<'_, AppState>) -> Result<bool, Stri
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn settings_snapshot(state: tauri::State<'_, AppState>) -> Result<UiSettingsSnapshot, String> {
|
||||
let snapshot = state
|
||||
.runtime_core
|
||||
.snapshot()
|
||||
.read()
|
||||
.map_err(|_| "snapshot lock poisoned".to_string())?
|
||||
.clone();
|
||||
Ok(UiSettingsSnapshot {
|
||||
active_sprite_pack: snapshot.active_sprite_pack,
|
||||
scale: snapshot.scale,
|
||||
visible: snapshot.flags.visible,
|
||||
always_on_top: snapshot.flags.always_on_top,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_sprite_packs(state: tauri::State<'_, AppState>) -> Result<Vec<UiSpritePackOption>, String> {
|
||||
let root = sprite_pack_root(state.runtime_core.as_ref())?;
|
||||
let mut packs = Vec::new();
|
||||
|
||||
let entries = fs::read_dir(root).map_err(|err| err.to_string())?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|err| err.to_string())?;
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if load_manifest(&path).is_err() {
|
||||
continue;
|
||||
}
|
||||
let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
packs.push(UiSpritePackOption {
|
||||
id: dir_name.to_string(),
|
||||
pack_id_or_path: dir_name.to_string(),
|
||||
});
|
||||
}
|
||||
packs.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
Ok(packs)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_sprite_pack(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
pack_id_or_path: String,
|
||||
) -> Result<UiSnapshot, String> {
|
||||
let root = sprite_pack_root(state.runtime_core.as_ref())?;
|
||||
let pack_path = resolve_pack_path(&root, &pack_id_or_path).map_err(|err| err.to_string())?;
|
||||
let manifest = load_manifest(&pack_path).map_err(|err| err.to_string())?;
|
||||
let image_path = pack_path.join(&manifest.image);
|
||||
let image_bytes = fs::read(&image_path).map_err(|err| err.to_string())?;
|
||||
let _geometry = atlas_geometry_for_manifest(&manifest, &image_bytes)?;
|
||||
validate_animation_frames(&manifest)?;
|
||||
|
||||
state
|
||||
.runtime_core
|
||||
.apply_command(&FrontendCommand::SetSpritePack { pack_id_or_path })
|
||||
.map_err(|err| err.to_string())?;
|
||||
emit_ui_refresh(state.inner(), &app_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_scale(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
scale: f32,
|
||||
) -> Result<UiSnapshot, String> {
|
||||
if !scale.is_finite() || scale <= 0.0 {
|
||||
return Err("scale must be a positive finite number".to_string());
|
||||
}
|
||||
state
|
||||
.runtime_core
|
||||
.apply_command(&FrontendCommand::SetTransform {
|
||||
x: None,
|
||||
y: None,
|
||||
anchor: None,
|
||||
scale: Some(scale),
|
||||
opacity: None,
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
emit_ui_refresh(state.inner(), &app_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_visibility(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
visible: bool,
|
||||
) -> Result<UiSnapshot, String> {
|
||||
state
|
||||
.runtime_core
|
||||
.apply_command(&FrontendCommand::SetFlags {
|
||||
click_through: None,
|
||||
always_on_top: None,
|
||||
visible: Some(visible),
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||
if visible {
|
||||
window.show().map_err(|err| err.to_string())?;
|
||||
window.set_focus().map_err(|err| err.to_string())?;
|
||||
} else {
|
||||
window.hide().map_err(|err| err.to_string())?;
|
||||
}
|
||||
}
|
||||
emit_ui_refresh(state.inner(), &app_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_always_on_top(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
always_on_top: bool,
|
||||
) -> Result<UiSnapshot, String> {
|
||||
state
|
||||
.runtime_core
|
||||
.apply_command(&FrontendCommand::SetFlags {
|
||||
click_through: None,
|
||||
always_on_top: Some(always_on_top),
|
||||
visible: None,
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||
window
|
||||
.set_always_on_top(always_on_top)
|
||||
.map_err(|err| err.to_string())?;
|
||||
}
|
||||
emit_ui_refresh(state.inner(), &app_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_debug_overlay_visible(
|
||||
app_handle: tauri::AppHandle,
|
||||
@@ -158,6 +314,11 @@ fn set_debug_overlay_visible(
|
||||
.set_frontend_debug_overlay_visible(visible)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let _ = emit_debug_overlay_visibility(state.runtime_core.as_ref(), &app_handle);
|
||||
if let Ok(guard) = state.tray_state.lock() {
|
||||
if let Some(tray_state) = guard.as_ref() {
|
||||
let _ = refresh_tray_menu_state(state.runtime_core.as_ref(), tray_state);
|
||||
}
|
||||
}
|
||||
Ok(visible)
|
||||
}
|
||||
|
||||
@@ -176,6 +337,7 @@ fn main() -> Result<(), AppError> {
|
||||
let state = AppState {
|
||||
runtime_core: Arc::clone(&runtime_core),
|
||||
runtime: Arc::clone(&runtime),
|
||||
tray_state: Arc::new(std::sync::Mutex::new(None)),
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
@@ -185,15 +347,25 @@ fn main() -> Result<(), AppError> {
|
||||
current_state,
|
||||
load_active_sprite_pack,
|
||||
debug_overlay_visible,
|
||||
set_debug_overlay_visible
|
||||
set_debug_overlay_visible,
|
||||
settings_snapshot,
|
||||
list_sprite_packs,
|
||||
set_sprite_pack,
|
||||
set_scale,
|
||||
set_visibility,
|
||||
set_always_on_top
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_state: tauri::State<'_, AppState> = app.state();
|
||||
let runtime_core = Arc::clone(&app_state.runtime_core);
|
||||
let runtime = Arc::clone(&app_state.runtime);
|
||||
let tray_state_holder = Arc::clone(&app_state.tray_state);
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
let tray_state = setup_tray(&app_handle, &runtime_core)?;
|
||||
if let Ok(mut guard) = tray_state_holder.lock() {
|
||||
*guard = Some(tray_state.clone());
|
||||
}
|
||||
if let Ok(snapshot) = runtime_core.snapshot().read() {
|
||||
let _ = app_handle.emit(EVENT_RUNTIME_SNAPSHOT, to_ui_snapshot(&snapshot));
|
||||
}
|
||||
@@ -251,10 +423,8 @@ fn main() -> Result<(), AppError> {
|
||||
};
|
||||
if runtime_core.apply_command(&command).is_ok() {
|
||||
if let Ok(snapshot) = runtime_core.snapshot().read() {
|
||||
let _ = app_handle.emit(
|
||||
EVENT_RUNTIME_SNAPSHOT,
|
||||
to_ui_snapshot(&snapshot),
|
||||
);
|
||||
let _ = app_handle
|
||||
.emit(EVENT_RUNTIME_SNAPSHOT, to_ui_snapshot(&snapshot));
|
||||
}
|
||||
let _ = refresh_tray_menu_state(
|
||||
runtime_core.as_ref(),
|
||||
@@ -295,9 +465,46 @@ fn to_ui_snapshot(snapshot: &FrontendStateSnapshot) -> UiSnapshot {
|
||||
y: snapshot.y,
|
||||
scale: snapshot.scale,
|
||||
active_sprite_pack: snapshot.active_sprite_pack.clone(),
|
||||
visible: snapshot.flags.visible,
|
||||
always_on_top: snapshot.flags.always_on_top,
|
||||
}
|
||||
}
|
||||
|
||||
fn sprite_pack_root(runtime_core: &RuntimeCore) -> Result<std::path::PathBuf, String> {
|
||||
let sprite_packs_dir = runtime_core
|
||||
.config()
|
||||
.read()
|
||||
.map_err(|_| "config lock poisoned".to_string())?
|
||||
.sprite
|
||||
.sprite_packs_dir
|
||||
.clone();
|
||||
let root = std::env::current_dir()
|
||||
.map_err(|err| err.to_string())?
|
||||
.join("assets")
|
||||
.join(sprite_packs_dir);
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
fn emit_ui_refresh(state: &AppState, app_handle: &AppHandle<Wry>) -> Result<UiSnapshot, String> {
|
||||
let snapshot = state
|
||||
.runtime_core
|
||||
.snapshot()
|
||||
.read()
|
||||
.map_err(|_| "snapshot lock poisoned".to_string())?
|
||||
.clone();
|
||||
let ui_snapshot = to_ui_snapshot(&snapshot);
|
||||
app_handle
|
||||
.emit(EVENT_RUNTIME_SNAPSHOT, ui_snapshot.clone())
|
||||
.map_err(|err| err.to_string())?;
|
||||
let _ = emit_debug_overlay_visibility(state.runtime_core.as_ref(), app_handle);
|
||||
if let Ok(guard) = state.tray_state.lock() {
|
||||
if let Some(tray_state) = guard.as_ref() {
|
||||
let _ = refresh_tray_menu_state(state.runtime_core.as_ref(), tray_state);
|
||||
}
|
||||
}
|
||||
Ok(ui_snapshot)
|
||||
}
|
||||
|
||||
fn setup_tray(
|
||||
app_handle: &AppHandle<Wry>,
|
||||
runtime_core: &RuntimeCore,
|
||||
@@ -311,6 +518,7 @@ fn setup_tray(
|
||||
.frontend_debug_overlay_visible()
|
||||
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
|
||||
|
||||
let settings = MenuItem::with_id(app_handle, MENU_ID_SETTINGS, "Settings", true, None::<&str>)?;
|
||||
let toggle_visibility = MenuItem::with_id(
|
||||
app_handle,
|
||||
MENU_ID_TOGGLE_VISIBILITY,
|
||||
@@ -338,6 +546,7 @@ fn setup_tray(
|
||||
let menu = Menu::with_items(
|
||||
app_handle,
|
||||
&[
|
||||
&settings,
|
||||
&toggle_visibility,
|
||||
&toggle_always_on_top,
|
||||
&toggle_debug_overlay,
|
||||
@@ -368,6 +577,10 @@ fn handle_menu_event(
|
||||
menu_id: &str,
|
||||
) -> Result<(), String> {
|
||||
match menu_id {
|
||||
MENU_ID_SETTINGS => {
|
||||
open_settings_window(app_handle).map_err(|err| err.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
MENU_ID_TOGGLE_VISIBILITY => {
|
||||
let current = runtime_core
|
||||
.snapshot()
|
||||
@@ -408,7 +621,9 @@ fn handle_menu_event(
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||
window.set_always_on_top(next).map_err(|err| err.to_string())?;
|
||||
window
|
||||
.set_always_on_top(next)
|
||||
.map_err(|err| err.to_string())?;
|
||||
}
|
||||
}
|
||||
MENU_ID_TOGGLE_DEBUG_OVERLAY => {
|
||||
@@ -419,7 +634,8 @@ fn handle_menu_event(
|
||||
runtime_core
|
||||
.set_frontend_debug_overlay_visible(next)
|
||||
.map_err(|err| err.to_string())?;
|
||||
emit_debug_overlay_visibility(runtime_core, app_handle).map_err(|err| err.to_string())?;
|
||||
emit_debug_overlay_visibility(runtime_core, app_handle)
|
||||
.map_err(|err| err.to_string())?;
|
||||
}
|
||||
MENU_ID_QUIT => {
|
||||
persist_current_ui_flags(runtime_core)?;
|
||||
@@ -436,6 +652,30 @@ fn handle_menu_event(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_settings_window(app_handle: &AppHandle<Wry>) -> Result<(), tauri::Error> {
|
||||
if let Some(window) = app_handle.get_webview_window(SETTINGS_WINDOW_LABEL) {
|
||||
window.show()?;
|
||||
window.set_focus()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let window = WebviewWindowBuilder::new(
|
||||
app_handle,
|
||||
SETTINGS_WINDOW_LABEL,
|
||||
WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("sprimo settings")
|
||||
.inner_size(420.0, 520.0)
|
||||
.resizable(true)
|
||||
.decorations(true)
|
||||
.transparent(false)
|
||||
.always_on_top(false)
|
||||
.visible(true)
|
||||
.build()?;
|
||||
window.set_focus()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_current_ui_flags(runtime_core: &RuntimeCore) -> Result<(), String> {
|
||||
let snapshot = runtime_core
|
||||
.snapshot()
|
||||
@@ -496,11 +736,7 @@ fn refresh_tray_menu_state(
|
||||
}
|
||||
|
||||
fn visibility_menu_title(visible: bool) -> &'static str {
|
||||
if visible {
|
||||
"Hide"
|
||||
} else {
|
||||
"Show"
|
||||
}
|
||||
if visible { "Hide" } else { "Show" }
|
||||
}
|
||||
|
||||
fn state_name(value: FrontendState) -> &'static str {
|
||||
@@ -522,3 +758,95 @@ fn to_ui_clip(value: AnimationDefinition) -> UiAnimationClip {
|
||||
one_shot: value.one_shot.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn atlas_geometry_for_manifest(
|
||||
manifest: &SpritePackManifest,
|
||||
image_bytes: &[u8],
|
||||
) -> Result<AtlasGeometry, String> {
|
||||
let (image_width, image_height) = decode_png_dimensions(image_bytes)?;
|
||||
if image_width == 0 || image_height == 0 {
|
||||
return Err("atlas image dimensions must be non-zero".to_string());
|
||||
}
|
||||
|
||||
if manifest.image.eq_ignore_ascii_case(SPRITE_PNG_FILE) {
|
||||
let frame_width = image_width / SPRITE_GRID_COLUMNS;
|
||||
let frame_height = image_height / SPRITE_GRID_ROWS;
|
||||
if frame_width == 0 || frame_height == 0 {
|
||||
return Err(format!(
|
||||
"sprite atlas too small for {}x{} grid: {}x{}",
|
||||
SPRITE_GRID_COLUMNS, SPRITE_GRID_ROWS, image_width, image_height
|
||||
));
|
||||
}
|
||||
return Ok(AtlasGeometry {
|
||||
frame_width,
|
||||
frame_height,
|
||||
});
|
||||
}
|
||||
|
||||
if manifest.frame_width == 0 || manifest.frame_height == 0 {
|
||||
return Err("manifest frame dimensions must be non-zero".to_string());
|
||||
}
|
||||
Ok(AtlasGeometry {
|
||||
frame_width: manifest.frame_width,
|
||||
frame_height: manifest.frame_height,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_animation_frames(manifest: &SpritePackManifest) -> Result<(), String> {
|
||||
let total_frames = if manifest.image.eq_ignore_ascii_case(SPRITE_PNG_FILE) {
|
||||
SPRITE_GRID_COLUMNS
|
||||
.checked_mul(SPRITE_GRID_ROWS)
|
||||
.ok_or_else(|| "sprite grid frame count overflow".to_string())?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if total_frames == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for clip in &manifest.animations {
|
||||
for &index in &clip.frames {
|
||||
if index >= total_frames {
|
||||
return Err(format!(
|
||||
"animation '{}' references frame {} but max frame index is {}",
|
||||
clip.name,
|
||||
index,
|
||||
total_frames.saturating_sub(1)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_png_dimensions(image_bytes: &[u8]) -> Result<(u32, u32), String> {
|
||||
const PNG_SIGNATURE_LEN: usize = 8;
|
||||
const PNG_IHDR_TOTAL_LEN: usize = 33;
|
||||
const IHDR_TYPE_OFFSET: usize = 12;
|
||||
const IHDR_DATA_OFFSET: usize = 16;
|
||||
const IHDR_WIDTH_OFFSET: usize = 16;
|
||||
const IHDR_HEIGHT_OFFSET: usize = 20;
|
||||
|
||||
if image_bytes.len() < PNG_IHDR_TOTAL_LEN {
|
||||
return Err("atlas image is too small to be a valid PNG".to_string());
|
||||
}
|
||||
let expected_signature: [u8; PNG_SIGNATURE_LEN] = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||
if image_bytes[..PNG_SIGNATURE_LEN] != expected_signature {
|
||||
return Err("atlas image must be PNG format".to_string());
|
||||
}
|
||||
if &image_bytes[IHDR_TYPE_OFFSET..IHDR_DATA_OFFSET] != b"IHDR" {
|
||||
return Err("atlas PNG missing IHDR chunk".to_string());
|
||||
}
|
||||
let width = u32::from_be_bytes(
|
||||
image_bytes[IHDR_WIDTH_OFFSET..IHDR_WIDTH_OFFSET + 4]
|
||||
.try_into()
|
||||
.map_err(|_| "failed to decode PNG width".to_string())?,
|
||||
);
|
||||
let height = u32::from_be_bytes(
|
||||
image_bytes[IHDR_HEIGHT_OFFSET..IHDR_HEIGHT_OFFSET + 4]
|
||||
.try_into()
|
||||
.map_err(|_| "failed to decode PNG height".to_string())?,
|
||||
);
|
||||
Ok((width, height))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user