oxedyne/fe2o3/fe2o3_graphics/tests/heif_whole.rs
6.5 KiB, 15 runs
created by r1870400018:20564, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! A whole photograph, against libheif's own decode of it. |
| 2 | //! |
| 3 | //! `tests/hevc_tiles.rs` holds the FIRST coded tile to ffmpeg's. This holds the assembly: every |
| 4 | //! tile decoded and laid into the grid, checked tile by tile against ffmpeg's own decode of each |
| 5 | //! one. That is what says the tiles are in the right order and at the right offsets -- a grid |
| 6 | //! assembled transposed, or one tile out, is a photograph with its pieces shuffled, and no amount |
| 7 | //! of checking one tile finds it. |
| 8 | //! |
| 9 | //! ffmpeg exposes each tile of a HEIF grid as a stream of its own, which is what makes this |
| 10 | //! possible without a second whole-image decoder. `heif-convert` would be the other way, and on |
| 11 | //! this machine libheif carries no HEVC plugin (`libheif-plugin-libde265`), so it reads the |
| 12 | //! container and then cannot decode a thing. |
| 13 | //! |
| 14 | //! ```bash |
| 15 | //! HEVC_CORPUS=/srv/nfs4/Gallery/2021/2021-01_Jan \ |
| 16 | //! cargo test -p oxedyne_fe2o3_graphics --test heif_whole -- --nocapture |
| 17 | //! ``` |
| 18 | //! |
| 19 | //! **The two agree sample for sample, and are asked to.** Both loop filters run, and the comparison |
| 20 | //! is made on the coded planes rather than on anything converted out of colour difference, so |
| 21 | //! nothing that is a choice rather than a specification is in the way. The assertion is that the |
| 22 | //! worst difference over every tile is nought. What that catches, and nothing else does, is a grid |
| 23 | //! assembled transposed or one tile out of place: the count of tiles is the same either way, so the |
| 24 | //! check that a grid names as many tiles as rows times columns passes regardless -- which is how a |
| 25 | //! reader that had the rows and columns the wrong way round survived until this test existed. |
| 26 | //! |
| 27 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 28 | //! Anthropic Claude |
| 29 | |
| 30 | use oxedyne_fe2o3_core::prelude::*; |
| 31 | use oxedyne_fe2o3_graphics::heif; |
| 32 | |
| 33 | use std::{ |
| 34 | path::{ |
| 35 | Path, |
| 36 | PathBuf, |
| 37 | }, |
| 38 | process::Command, |
| 39 | }; |
| 40 | |
| 41 | const FILES: usize = 3; // photographs a run reads unless it is told otherwise |
| 42 | |
| 43 | // The mean difference a channel may show against the other decoder. Measured across the corpus at |
| 44 | // well under two; the bound is where a fault would have to be visible to exceed it. |
| 45 | const MEAN_BOUND: f64 = 4.0; |
| 46 | |
| 47 | #[test] |
| 48 | fn test_a_whole_photograph_is_the_photograph_00() -> Outcome<()> { |
| 49 | let dir = match std::env::var("HEVC_CORPUS") { |
| 50 | Ok(d) => PathBuf::from(d), |
| 51 | Err(_) => { |
| 52 | println!(" skipped: set HEVC_CORPUS to a directory of HEIC photographs"); |
| 53 | return Ok(()); |
| 54 | }, |
| 55 | }; |
| 56 | if Command::new("ffmpeg").arg("-version").output().is_err() { |
| 57 | println!(" skipped: no ffmpeg to compare against"); |
| 58 | return Ok(()); |
| 59 | } |
| 60 | let want: usize = std::env::var("HEVC_FILES").ok() |
| 61 | .and_then(|s| s.parse().ok()) |
| 62 | .unwrap_or(FILES); |
| 63 | let out = match std::env::var("HOME") { |
| 64 | Ok(home) => PathBuf::from(home).join(".cache/heif-oracle"), |
| 65 | Err(_) => PathBuf::from(".heif-oracle"), |
| 66 | }; |
| 67 | res!(std::fs::create_dir_all(&out), IO, File); |
| 68 | |
| 69 | let mut files: Vec<PathBuf> = Vec::new(); |
| 70 | for entry in walk(&dir) { |
| 71 | if entry.to_string_lossy().to_lowercase().ends_with(".heic") { |
| 72 | files.push(entry); |
| 73 | } |
| 74 | if files.len() >= want { |
| 75 | break; |
| 76 | } |
| 77 | } |
| 78 | let (mut read, mut tiles_checked, mut worst) = (0usize, 0usize, 0i32); |
| 79 | for file in &files { |
| 80 | let bytes = res!(std::fs::read(file), IO, File); |
| 81 | let (assembled, size) = match heif::planes(&bytes) { |
| 82 | Ok(p) => p, |
| 83 | Err(e) => { |
| 84 | println!(" skipped {}: {}", |
| 85 | file.file_name().unwrap_or_default().to_string_lossy(), e.plain()); |
| 86 | continue; |
| 87 | }, |
| 88 | }; |
| 89 | // The photograph is cropped out of the grid's top left, so the grid is at least as big. |
| 90 | let fits = assembled.y.w >= size.0 && assembled.y.h >= size.1; |
| 91 | req!(fits, true, "{}: a grid of {} by {} cannot hold a photograph of {} by {}", |
| 92 | file.display(), assembled.y.w, assembled.y.h, size.0, size.1); |
| 93 | |
| 94 | // And the picture that comes out is the size the container says. |
| 95 | let picture = res!(heif::decode(&bytes)); |
| 96 | req!(picture.width(), size.0, "{} came out the wrong width", file.display()); |
| 97 | req!(picture.height(), size.1, "{} came out the wrong height", file.display()); |
| 98 | if std::env::var("HEVC_DUMP").is_ok() { |
| 99 | let name = out.join(fmt!("{}.png", |
| 100 | file.file_stem().unwrap_or_default().to_string_lossy())); |
| 101 | res!(std::fs::write(&name, res!(oxedyne_fe2o3_graphics::png::encode(&picture))), |
| 102 | IO, File); |
| 103 | println!(" wrote {}", name.display()); |
| 104 | } |
| 105 | |
| 106 | // Now each tile, against ffmpeg's own decode of that tile. |
| 107 | let across = assembled.y.w / 512.max(1); |
| 108 | let mut n = 0usize; |
| 109 | loop { |
| 110 | let raw = out.join("tile.yuv"); |
| 111 | let _ = std::fs::remove_file(&raw); |
| 112 | let run = res!(Command::new("ffmpeg") |
| 113 | .args(["-v", "error", "-i"]) |
| 114 | .arg(file) |
| 115 | .args(["-map", &fmt!("0:v:{}", n), "-frames:v", "1", |
| 116 | "-pix_fmt", "yuv420p", "-f", "rawvideo", "-y"]) |
| 117 | .arg(&raw) |
| 118 | .output(), IO, File); |
| 119 | if !run.status.success() || !raw.exists() { |
| 120 | break; |
| 121 | } |
| 122 | let their = res!(std::fs::read(&raw), IO, File); |
| 123 | // A tile's size comes from how many bytes came back, which is how a tile of any size |
| 124 | // is handled without asking the container about it. |
| 125 | let side = ((their.len() * 2 / 3) as f64).sqrt().round() as usize; |
| 126 | if side == 0 || side * side * 3 / 2 != their.len() { |
| 127 | break; |
| 128 | } |
| 129 | let (col, row) = (n % across.max(1), n / across.max(1)); |
| 130 | let (ox, oy) = (col * side, row * side); |
| 131 | if oy >= assembled.y.h { |
| 132 | break; |
| 133 | } |
| 134 | for y in 0..side { |
| 135 | for x in 0..side { |
| 136 | let mine = match assembled.y.at(ox + x, oy + y) { |
| 137 | Some(v) => v as i32, |
| 138 | None => continue, |
| 139 | }; |
| 140 | let d = (mine - their[y * side + x] as i32).abs(); |
| 141 | worst = worst.max(d); |
| 142 | } |
| 143 | } |
| 144 | tiles_checked += 1; |
| 145 | n += 1; |
| 146 | if n > 256 { |
| 147 | break; |
| 148 | } |
| 149 | } |
| 150 | println!(" {}: {} by {} out of a {} by {} grid, {} tiles checked", |
| 151 | file.file_name().unwrap_or_default().to_string_lossy(), |
| 152 | size.0, size.1, assembled.y.w, assembled.y.h, n); |
| 153 | read += 1; |
| 154 | } |
| 155 | if read == 0 { |
| 156 | println!(" skipped: not one photograph could be compared"); |
| 157 | return Ok(()); |
| 158 | } |
| 159 | println!(" {} photographs, {} tiles, worst sample difference {}", read, tiles_checked, worst); |
| 160 | let placed = tiles_checked > 0; |
| 161 | req!(placed, true, "no tile could be checked against the other decoder"); |
| 162 | req!(worst, 0i32, "a tile is not where the assembly put it, or is not the tile it should be"); |
| 163 | Ok(()) |
| 164 | } |
| 165 | |
| 166 | fn walk(dir: &Path) -> Vec<PathBuf> { |
| 167 | let mut out = Vec::new(); |
| 168 | let mut stack = vec![dir.to_path_buf()]; |
| 169 | while let Some(at) = stack.pop() { |
| 170 | let entries = match std::fs::read_dir(&at) { |
| 171 | Ok(e) => e, |
| 172 | Err(_) => continue, |
| 173 | }; |
| 174 | for entry in entries.flatten() { |
| 175 | let path = entry.path(); |
| 176 | if path.is_dir() { |
| 177 | stack.push(path); |
| 178 | } else { |
| 179 | out.push(path); |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | out.sort(); |
| 184 | out |
| 185 | } |