add import support for emote psb files

fix create file bug in main
This commit is contained in:
2025-09-15 23:14:19 +08:00
parent 28288ecf1b
commit ec137bc5cd
5 changed files with 293 additions and 24 deletions

View File

@@ -130,7 +130,7 @@ msg-tool create -t <script-type> <input> <output>
### Emote
| Script Type | Feature Name | Name | Export | Import | Custom Export | Custom Import | Create | Remarks |
|---|---|---|---|---|---|---|---|---|
| `emote-psb`/`psb` | `emote-psb` | Emote PSB File | ❌ | ❌ | ✔️ | ❌| ❌ | |
| `emote-psb`/`psb` | `emote-psb` | Emote PSB File | ❌ | ❌ | ✔️ | ✔️ | ✔️ | |
| Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks |
|---|---|---|---|---|---|---|---|---|

View File

@@ -146,19 +146,27 @@ impl PsbValueFixed {
}
/// Find the resource's key in object
pub fn find_resource_key<'a>(&'a self, resource_id: u64) -> Option<&'a str> {
pub fn find_resource_key<'a>(
&'a self,
resource_id: u64,
now: Vec<&'a str>,
) -> Option<Vec<&'a str>> {
match self {
PsbValueFixed::List(l) => l.find_resource_key(resource_id),
PsbValueFixed::Object(o) => o.find_resource_key(resource_id),
PsbValueFixed::List(l) => l.find_resource_key(resource_id, now),
PsbValueFixed::Object(o) => o.find_resource_key(resource_id, now),
_ => None,
}
}
/// Find the extra resource's key in object
pub fn find_extra_resource_key<'a>(&'a self, extra_resource_id: u64) -> Option<&'a str> {
pub fn find_extra_resource_key<'a>(
&'a self,
extra_resource_id: u64,
now: Vec<&'a str>,
) -> Option<Vec<&'a str>> {
match self {
PsbValueFixed::List(l) => l.find_extra_resource_key(extra_resource_id),
PsbValueFixed::Object(o) => o.find_extra_resource_key(extra_resource_id),
PsbValueFixed::List(l) => l.find_extra_resource_key(extra_resource_id, now),
PsbValueFixed::Object(o) => o.find_extra_resource_key(extra_resource_id, now),
_ => None,
}
}
@@ -546,9 +554,13 @@ impl PsbListFixed {
}
/// Find the resource's key in object
pub fn find_resource_key<'a>(&'a self, resource_id: u64) -> Option<&'a str> {
pub fn find_resource_key<'a>(
&'a self,
resource_id: u64,
now: Vec<&'a str>,
) -> Option<Vec<&'a str>> {
for value in &self.values {
if let Some(key) = value.find_resource_key(resource_id) {
if let Some(key) = value.find_resource_key(resource_id, now.clone()) {
return Some(key);
}
}
@@ -556,9 +568,13 @@ impl PsbListFixed {
}
/// Find the extra resource's key in object
pub fn find_extra_resource_key<'a>(&'a self, extra_resource_id: u64) -> Option<&'a str> {
pub fn find_extra_resource_key<'a>(
&'a self,
extra_resource_id: u64,
now: Vec<&'a str>,
) -> Option<Vec<&'a str>> {
for value in &self.values {
if let Some(key) = value.find_extra_resource_key(extra_resource_id) {
if let Some(key) = value.find_extra_resource_key(extra_resource_id, now.clone()) {
return Some(key);
}
}
@@ -729,14 +745,20 @@ impl PsbObjectFixed {
}
/// Find the resource's key in object
pub fn find_resource_key<'a>(&'a self, resource_id: u64) -> Option<&'a str> {
pub fn find_resource_key<'a>(
&'a self,
resource_id: u64,
now: Vec<&'a str>,
) -> Option<Vec<&'a str>> {
for (key, value) in &self.values {
let mut now = now.clone();
now.push(key);
if let Some(id) = value.resource_id() {
if id == resource_id {
return Some(key);
return Some(now);
}
}
if let Some(key) = value.find_resource_key(resource_id) {
if let Some(key) = value.find_resource_key(resource_id, now) {
return Some(key);
}
}
@@ -744,14 +766,20 @@ impl PsbObjectFixed {
}
/// Find the extra resource's key in object
pub fn find_extra_resource_key<'a>(&'a self, extra_resource_id: u64) -> Option<&'a str> {
pub fn find_extra_resource_key<'a>(
&'a self,
extra_resource_id: u64,
now: Vec<&'a str>,
) -> Option<Vec<&'a str>> {
for (key, value) in &self.values {
let mut now = now.clone();
now.push(key);
if let Some(id) = value.extra_resource_id() {
if id == extra_resource_id {
return Some(key);
return Some(now);
}
}
if let Some(key) = value.find_extra_resource_key(extra_resource_id) {
if let Some(key) = value.find_extra_resource_key(extra_resource_id, now) {
return Some(key);
}
}
@@ -1021,6 +1049,27 @@ impl VirtualPsbFixed {
Ok(())
}
#[cfg(feature = "json")]
/// Creates a fixed PSB from a JSON object.
pub fn with_json(obj: &JsonValue) -> Result<Self, anyhow::Error> {
let version = obj["version"]
.as_u16()
.ok_or_else(|| anyhow::anyhow!("Invalid PSB version"))?;
let encryption = obj["encryption"]
.as_u16()
.ok_or_else(|| anyhow::anyhow!("Invalid PSB encryption"))?;
let root = PsbObjectFixed::from_json(&obj["data"]);
Ok(Self {
header: PsbHeader {
version,
encryption,
},
resources: Vec::new(),
extra: Vec::new(),
root,
})
}
pub fn set_data(&mut self, data: VirtualPsbFixedData) {
self.header.version = data.version;
self.header.encryption = data.encryption;

View File

@@ -1876,6 +1876,8 @@ pub fn create_file(
}
};
crate::utils::files::make_sure_dir_exists(&output)?;
builder.create_file_filename(
input,
&output,

View File

@@ -4,6 +4,7 @@ use crate::ext::psb::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::*;
use crate::utils::files::*;
use crate::utils::img::*;
use anyhow::Result;
use base64::Engine;
@@ -78,6 +79,21 @@ impl ScriptBuilder for PsbBuilder {
}
None
}
fn can_create_file(&self) -> bool {
true
}
fn create_file<'a>(
&'a self,
filename: &'a str,
writer: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
file_encoding: Encoding,
_config: &ExtraConfig,
) -> Result<()> {
create_file(filename, writer, encoding, file_encoding)
}
}
#[derive(Debug)]
@@ -123,6 +139,7 @@ impl Psb {
pb.to_string_lossy().to_string()
};
let path = folder_path.join(&res.path);
make_sure_dir_exists(&path)?;
let img = ImageData {
width: tlg.width as u32,
height: tlg.height as u32,
@@ -137,6 +154,7 @@ impl Psb {
encode_img(img, outtype, &path.to_string_lossy(), &self.config)?;
} else {
let path = folder_path.join(&res.path);
make_sure_dir_exists(&path)?;
std::fs::write(&path, data)?;
}
Ok(res)
@@ -172,11 +190,30 @@ impl TlgInfo {
}
Self { metadata }
}
fn to_tlg_tags(&self, encoding: Encoding) -> Result<HashMap<Vec<u8>, Vec<u8>>> {
let mut tags = HashMap::new();
for (k, v) in &self.metadata {
let k = if k.starts_with("base64:") {
base64::engine::general_purpose::STANDARD.decode(&k[7..])?
} else {
encode_string(encoding, k, false)?
};
let v = if v.starts_with("base64:") {
base64::engine::general_purpose::STANDARD.decode(&v[7..])?
} else {
encode_string(encoding, v, false)?
};
tags.insert(k, v);
}
Ok(tags)
}
}
#[derive(Debug, Deserialize, Serialize)]
struct Resource {
path: String,
#[serde(skip_serializing_if = "Option::is_none")]
tlg: Option<TlgInfo>,
}
@@ -206,17 +243,15 @@ impl Script for Psb {
pb.set_extension("");
pb
};
if self.psb.resources().len() > 0 || self.psb.extra().len() > 0 {
std::fs::create_dir_all(&folder_path)?;
}
for (i, data) in self.psb.resources().iter().enumerate() {
let i = i as u64;
let res_name = self
.psb
.root()
.find_resource_key(i)
.map(|s| s.to_string())
.find_resource_key(i, vec![])
.map(|s| s.join("/"))
.unwrap_or(format!("res_{}", i));
let res_name = sanitize_path(&res_name);
let res = self.output_resource(&folder_path, res_name, data)?;
resources.push(res);
}
@@ -225,9 +260,10 @@ impl Script for Psb {
let res_name = self
.psb
.root()
.find_resource_key(i)
.map(|s| format!("extra_{}", s))
.find_resource_key(i, vec![])
.map(|s| format!("extra_{}", s.join("/")))
.unwrap_or(format!("extra_res_{}", i));
let res_name = sanitize_path(&res_name);
let res = self.output_resource(&folder_path, res_name, data)?;
extra_resources.push(res);
}
@@ -239,4 +275,91 @@ impl Script for Psb {
file.write_all(&s)?;
Ok(())
}
fn custom_import<'a>(
&'a self,
custom_filename: &'a str,
file: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
output_encoding: Encoding,
) -> Result<()> {
create_file(custom_filename, file, encoding, output_encoding)
}
}
fn read_resource(
folder_path: &std::path::PathBuf,
res: &Resource,
encoding: Encoding,
) -> Result<Vec<u8>> {
if let Some(tlg) = &res.tlg {
let path = folder_path.join(&res.path);
let imgfmt = ImageOutputType::try_from(path.as_path())?;
let mut img = decode_img(imgfmt, &path.to_string_lossy())?;
if img.depth != 8 {
return Err(anyhow::anyhow!(
"Only 8-bit images are supported for TLG conversion"
));
}
let color_type = match img.color_type {
ImageColorType::Bgr => TlgColorType::Bgr24,
ImageColorType::Bgra => TlgColorType::Bgra32,
ImageColorType::Grayscale => TlgColorType::Grayscale8,
ImageColorType::Rgb => {
convert_rgb_to_bgr(&mut img)?;
TlgColorType::Bgr24
}
ImageColorType::Rgba => {
convert_rgba_to_bgra(&mut img)?;
TlgColorType::Bgra32
}
};
let tlg = Tlg {
width: img.width,
height: img.height,
version: 5,
color: color_type,
data: img.data,
tags: tlg.to_tlg_tags(encoding)?,
};
let mut writer = MemWriter::new();
save_tlg(&tlg, &mut writer)?;
Ok(writer.into_inner())
} else {
let path = folder_path.join(&res.path);
Ok(std::fs::read(&path)?)
}
}
fn create_file<'a>(
custom_filename: &'a str,
mut writer: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
output_encoding: Encoding,
) -> Result<()> {
let input = read_file(custom_filename)?;
let s = decode_to_string(output_encoding, &input, true)?;
let data = json::parse(&s)?;
let resources: Vec<Resource> = serde_json::from_str(&data["resources"].dump())?;
let extra_resources: Vec<Resource> = serde_json::from_str(&data["extra_resources"].dump())?;
let mut psb = VirtualPsbFixed::with_json(&data)?;
let folder_path = {
let mut pb = std::path::PathBuf::from(custom_filename);
pb.set_extension("");
pb
};
for res in resources {
let res = read_resource(&folder_path, &res, encoding)?;
psb.resources_mut().push(res);
}
for res in extra_resources {
let res = read_resource(&folder_path, &res, encoding)?;
psb.extra_mut().push(res);
}
let psb = psb.to_psb(false);
let psb_writer = PsbWriter::new(psb, &mut writer);
psb_writer
.finish()
.map_err(|e| anyhow::anyhow!("Failed to write psb: {:?}", e))?;
Ok(())
}

View File

@@ -241,3 +241,98 @@ pub fn make_sure_dir_exists<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<()> {
}
Ok(())
}
/// Replace symbols not allowed in Windows path with underscores.
pub fn sanitize_path(path: &str) -> String {
// Split path into components, preserving separators
if path.is_empty() {
return String::new();
}
let invalid_chars: &[char] = &['<', '>', '"', '|', '?', '*'];
let mut result = String::with_capacity(path.len());
let reserved_names: Vec<String> = {
let mut v = vec!["CON", "PRN", "AUX", "NUL"]
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
for i in 1..=9 {
v.push(format!("COM{}", i));
v.push(format!("LPT{}", i));
}
v
};
let bytes = path.as_bytes();
let len = bytes.len();
let mut start = 0usize;
while start < len {
// find next separator index
let mut end = start;
while end < len && bytes[end] != b'\\' && bytes[end] != b'/' {
end += 1;
}
// segment is path[start..end]
let seg = &path[start..end];
// sanitize segment
let mut s = String::with_capacity(seg.len());
for (i, ch) in seg.chars().enumerate() {
// allow drive letter colon like "C:" (i == 1, first char is ASCII letter)
if ch == ':' {
if i == 1 {
// check first char is ASCII letter
if seg
.chars()
.next()
.map(|c| c.is_ascii_alphabetic())
.unwrap_or(false)
{
s.push(':');
continue;
}
}
// otherwise treat as invalid
s.push('_');
continue;
}
// keep separators out of segment (shouldn't appear here)
// replace control chars and other invalids
if (ch as u32) < 32 || invalid_chars.contains(&ch) {
s.push('_');
} else {
s.push(ch);
}
}
// trim trailing spaces and dots (Windows disallows filenames ending with space or dot)
while s.ends_with(' ') || s.ends_with('.') {
s.pop();
}
if s.is_empty() {
s.push('_');
} else {
// check reserved names (base name before first '.')
let base = s.split('.').next().unwrap_or("").to_ascii_uppercase();
if reserved_names.iter().any(|r| r == &base) {
s = format!("_{}", s);
}
}
result.push_str(&s);
// append separator if present
if end < len {
// keep original separator (preserve '\' or '/')
result.push(path.as_bytes()[end] as char);
start = end + 1;
} else {
start = end;
}
}
result
}