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:
PaulandCodex authored and GitHub committed 2026-10-14 17:43:05 -06:00
1 parent 0b432e3834
commit 9ed9650cad
3 files changed
+44 -3

No files matched your search

+11 -2
View File
@@ -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);
+14
View File
@@ -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.
+19 -1
View File
@@ -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();