mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-22 18:22:22 +00:00
fix(media): preserve Unicode prefixes in image-sequence filenames
Compute the final ASCII frame-number boundary without slicing inside a UTF-8 character. Preserve Unicode prefixes and add parser and public image-sequence import regressions, including missing frames and timeline placement. Fixes #1565 Co-authored-by: Codex <[email protected]>
This commit is contained in:
1 parent
0b432e3834
commit
9ed9650cad
3 files changed
+44
-3
No files matched your search
@@ -74,11 +74,20 @@ fn item_of(r: &Value) -> ItemId {
|
||||
|
||||
#[test]
|
||||
fn import_as_image_sequence_with_missing_frames() {
|
||||
let dir = tmp_dir("imgseq-import");
|
||||
check_image_sequence_with_missing_frames("shot_");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_unicode_image_sequence_with_missing_frames() {
|
||||
check_image_sequence_with_missing_frames("é");
|
||||
}
|
||||
|
||||
fn check_image_sequence_with_missing_frames(prefix: &str) {
|
||||
let dir = tmp_dir(&format!("imgseq-import-{prefix}"));
|
||||
let mut s = demo();
|
||||
s.execute("prefs.set", json!({"key": "media.indeterminateTimebase", "value": "25"})).unwrap();
|
||||
// frames 1..=6 with 4 missing, plus an unrelated file
|
||||
let paths = write_frames(&dir, "shot_", &[1, 2, 3, 5, 6], "png");
|
||||
let paths = write_frames(&dir, prefix, &[1, 2, 3, 5, 6], "png");
|
||||
write_frames(&dir, "other_", &[1], "png");
|
||||
let r = s.execute("file.import", json!({"paths": [paths[0]], "imageSequence": true})).unwrap();
|
||||
let id = item_of(&r);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# filmcraft-media
|
||||
|
||||
Media sources, still-image and image-sequence import, generated media, audio streams, and decoded
|
||||
frame caching for FilmCraft.
|
||||
|
||||
## Image-sequence filenames
|
||||
|
||||
`sequence::Numbered` identifies the last run of 1–18 ASCII frame-number digits in a filename's
|
||||
stem when the extension is a recognized still-image format. `/` and `\` separate directory
|
||||
components. A prefix may contain other UTF-8 characters, including accented letters, CJK and emoji:
|
||||
`é0001.png`, `图0001.png` and `🎞️0001.png`. Parsing preserves the prefix, number padding and suffix
|
||||
when generating sibling-frame paths. Sibling recognition compares the prefix's UTF-8 bytes exactly.
|
||||
Stems without ASCII digits or with a final frame-number run longer than 18 characters are not numbered
|
||||
image-sequence filenames.
|
||||
@@ -46,7 +46,7 @@ impl Numbered {
|
||||
}
|
||||
let stem = &name[..ext_at];
|
||||
let end = stem.rfind(|c: char| c.is_ascii_digit())? + 1;
|
||||
let start = stem[..end].rfind(|c: char| !c.is_ascii_digit()).map_or(0, |i| i + 1);
|
||||
let start = stem[..end].trim_end_matches(|c: char| c.is_ascii_digit()).len();
|
||||
let digits = &stem[start..end];
|
||||
if digits.len() > 18 {
|
||||
return None;
|
||||
@@ -271,6 +271,24 @@ mod tests {
|
||||
assert_eq!((w.dir.as_str(), w.prefix.as_str(), w.number), ("C:\\renders\\", "out.", 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_unicode_prefix() {
|
||||
for prefix in ["é", "图", "🎞️", "é1a_", "1é", "e\u{301}"] {
|
||||
let path = format!("/shots/{prefix}0001.png");
|
||||
let numbered = Numbered::parse(&path).unwrap();
|
||||
assert_eq!(numbered.prefix, prefix);
|
||||
assert_eq!(numbered.number, 1);
|
||||
assert_eq!(numbered.digits, 4);
|
||||
assert_eq!(numbered.path(2), format!("/shots/{prefix}0002.png"));
|
||||
assert_eq!(numbered.number_of(&format!("{prefix}0002.png")), Some(2));
|
||||
}
|
||||
let numbered = Numbered::parse("/shots/é0001.png").unwrap();
|
||||
assert_eq!(numbered.number_of("e\u{301}0002.png"), None, "prefixes must match exactly");
|
||||
for name in ["é١٠٠١.png", "图0001.png"] {
|
||||
assert_eq!(Numbered::parse(name), None, "frame numbers require ASCII digits");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_with_gaps() {
|
||||
let n = Numbered::parse("/d/s_003.png").unwrap();
|
||||
|
||||
Reference in new issue
Block a user