mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-31 05:47:00 -06:00
engine: store replacement images under their true format extension (#1691)
* engine: store replacement images under their true format extension * rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
67b0428657
commit
056771a33a
@@ -222,6 +222,20 @@ pub(crate) fn picture(s: &mut Session, v: &Value) -> CmdResult {
|
||||
place_picture(s, bytes, p::f32(v, "width"), p::str(v, "alt").unwrap_or(""))
|
||||
}
|
||||
|
||||
/// The media extension of an encoded image by its magic bytes, as `image_size` accepted it
|
||||
/// (TIFF included: the `image` crate decodes it).
|
||||
pub(crate) fn image_ext(bytes: &[u8]) -> &'static str {
|
||||
match bytes.get(..4) {
|
||||
Some([0x89, b'P', b'N', b'G']) => "png",
|
||||
Some([0xFF, 0xD8, ..]) => "jpeg",
|
||||
Some([b'G', b'I', b'F', _]) => "gif",
|
||||
Some([b'R', b'I', b'F', b'F']) => "webp",
|
||||
Some([b'B', b'M', ..]) => "bmp",
|
||||
Some([b'I', b'I', 0x2A, 0x00]) | Some([b'M', b'M', 0x00, 0x2A]) => "tiff",
|
||||
_ => "png",
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an encoded image at the selection as an inline picture, `width` points wide (default:
|
||||
/// its natural size at 96 ppi), never wider than the text column. The caret ends after it, so a
|
||||
/// script's next typing doesn't replace it (#965); Picture Format commands still act on it. The
|
||||
@@ -232,14 +246,7 @@ fn place_picture(s: &mut Session, bytes: Vec<u8>, width: Option<f32>, alt: &str)
|
||||
}
|
||||
// Natural size: pixels at 96 ppi, or a metafile's own frame (WMF, EMF: #802).
|
||||
let (natural_w, natural_h, ext) = if let Some((pw, ph)) = wordcraft_render::image_size(&bytes) {
|
||||
let ext = match bytes.get(..4) {
|
||||
Some([0x89, b'P', b'N', b'G']) => "png",
|
||||
Some([0xFF, 0xD8, ..]) => "jpeg",
|
||||
Some([b'G', b'I', b'F', _]) => "gif",
|
||||
Some([b'R', b'I', b'F', b'F']) => "webp",
|
||||
Some([b'B', b'M', ..]) => "bmp",
|
||||
_ => "png",
|
||||
};
|
||||
let ext = image_ext(&bytes);
|
||||
(pw as f32 * 0.75, ph as f32 * 0.75, ext)
|
||||
} else if let Some(size) = wordcraft_render::metafile_size(&bytes) {
|
||||
size
|
||||
|
||||
@@ -220,7 +220,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
return Err(CmdError::Params("`path` or `data` required".into()));
|
||||
};
|
||||
wordcraft_render::image_size(&bytes).ok_or_else(|| CmdError::Failed("not a supported image".into()))?;
|
||||
let key = s.doc.add_media(bytes, "png");
|
||||
let key = s.doc.add_media(bytes.clone(), super::insert::image_ext(&bytes));
|
||||
with_obj(s, |o| {
|
||||
if let InlineObject::Image { media, .. } = o {
|
||||
*media = key.clone();
|
||||
|
||||
@@ -5984,6 +5984,35 @@ fn insert_picture_takes_a_windows_metafile() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Change Picture must keep the replacement's own format: the part's name and content type
|
||||
/// come from the extension, so a JPEG stored as `imageN.png` saves declared `image/png` (Word
|
||||
/// and LibreOffice then show a broken picture).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn picture_change_keeps_the_replacement_format() {
|
||||
let dir = scratch_dir("picture-change-format");
|
||||
let first = dir.join("first.png");
|
||||
std::fs::write(&first, tiny_png()).unwrap();
|
||||
let mut jpeg = Vec::new();
|
||||
image::RgbImage::from_pixel(2, 2, image::Rgb([30, 30, 200])).write_to(&mut std::io::Cursor::new(&mut jpeg), image::ImageFormat::Jpeg).unwrap();
|
||||
let second = dir.join("second.jpg");
|
||||
std::fs::write(&second, &jpeg).unwrap();
|
||||
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
run(&mut s, "insert.picture", json!({"path": first.to_string_lossy()}));
|
||||
run(&mut s, "picture.change", json!({"path": second.to_string_lossy()}));
|
||||
let mut media = String::new();
|
||||
s.doc.objects_in_reading_order(&mut |o| {
|
||||
if let wordcraft_doc::para::InlineObject::Image { media: m, .. } = o {
|
||||
media = m.clone();
|
||||
}
|
||||
});
|
||||
assert!(media.ends_with(".jpeg") || media.ends_with(".jpg"), "the JPEG keeps its format: {media}");
|
||||
let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap();
|
||||
assert_eq!(back.media.get(&media).map(|b| b.as_slice()), Some(jpeg.as_slice()), "the saved part is the JPEG");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_open_repairs_hostile_run_lengths() {
|
||||
use wordcraft_doc::{Block, Document};
|
||||
|
||||
Reference in New Issue
Block a user