1use crate::prelude::*;
21
22use super::{Bitmap, encode};
23
24const BAYER: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
27
28#[derive(Copy, Clone, Debug)]
30pub struct Rgba<'a> {
31 width: u32,
32 height: u32,
33 pixels: &'a [u8],
34}
35
36impl<'a> Rgba<'a> {
37 pub fn new(width: u32, height: u32, pixels: &'a [u8]) -> Option<Self> {
40 let size = u64::from(width) * u64::from(height) * 4;
41 (width > 0 && height > 0 && pixels.len() as u64 == size).then_some(Self {
42 width,
43 height,
44 pixels,
45 })
46 }
47}
48
49#[derive(Copy, Clone, Debug, PartialEq)]
52pub struct Method {
53 pub curve: f32,
56 pub stretch: f32,
60 pub levels: u8,
63 pub lines: Option<Lines>,
65}
66
67impl Method {
68 pub const BAYER: Self = Self {
71 curve: 1.0,
72 stretch: 0.03,
73 levels: 0,
74 lines: None,
75 };
76}
77
78#[derive(Copy, Clone, Debug, PartialEq)]
85pub struct Lines {
86 pub contrast: f32,
88 pub dark: f32,
90}
91
92pub fn convert(
96 image: &Rgba<'_>,
97 (num, den): (u32, u32),
98 methods: &[Method],
99) -> Result<Vec<u8>, String> {
100 if num == 0 || den == 0 {
101 return Err(format!("the scale {num} / {den} isn't a size"));
102 }
103 if methods.is_empty() {
104 return Err("no method to convert with".to_string());
105 }
106 let enlarge = |v: u32| {
107 let scaled = (u64::from(v) * u64::from(num) + u64::from(den / 2)) / u64::from(den);
108 u16::try_from(scaled.max(1)).ok()
109 };
110 let too_big = || {
111 format!(
112 "{}×{} px is too big for a one-bit picture",
113 image.width, image.height
114 )
115 };
116 let source = (
117 u16::try_from(image.width).map_err(|_| too_big())?,
118 u16::try_from(image.height).map_err(|_| too_big())?,
119 );
120 let size = (
121 enlarge(image.width).ok_or_else(too_big)?,
122 enlarge(image.height).ok_or_else(too_big)?,
123 );
124 let art = Art::of(image);
125 let grey = art.enlarged(size, num % den == 0);
126 let styles: Vec<Vec<u8>> = methods
127 .iter()
128 .map(|method| style(&art, &grey, method))
129 .collect();
130 let mask = grey
131 .opaque
132 .iter()
133 .any(|&o| !o)
134 .then(|| grey.bits(|i| grey.opaque[i]));
135 let refs: Vec<&[u8]> = styles.iter().map(Vec::as_slice).collect();
136 encode(size, source, &refs, mask.as_deref()).map_err(ToString::to_string)
137}
138
139struct Art {
141 width: usize,
142 height: usize,
143 light: Vec<f32>,
144 alpha: Vec<f32>,
145}
146
147impl Art {
148 fn of(image: &Rgba<'_>) -> Self {
149 let (light, alpha) = image
150 .pixels
151 .as_chunks::<4>()
152 .0
153 .iter()
154 .map(|p| {
155 let [r, g, b, a] = p.map(f32::from);
156 ((0.299 * r + 0.587 * g + 0.114 * b) / 255.0, a / 255.0)
157 })
158 .unzip();
159 Self {
160 width: image.width as usize,
161 height: image.height as usize,
162 light,
163 alpha,
164 }
165 }
166
167 fn opaque(&self, i: usize) -> bool {
168 self.alpha[i] >= 0.5
169 }
170
171 fn nearest(t: usize, from: usize, to: usize) -> usize {
174 (2 * t + 1) * from / (2 * to)
175 }
176
177 fn enlarged(&self, (width, height): (u16, u16), whole: bool) -> Grey {
181 let (width, height) = (usize::from(width), usize::from(height));
182 let mut light = Vec::with_capacity(width * height);
183 let mut opaque = Vec::with_capacity(width * height);
184 for y in 0..height {
185 for x in 0..width {
186 let (l, a) = if whole {
187 let i = Self::nearest(y, self.height, height) * self.width
188 + Self::nearest(x, self.width, width);
189 (self.light[i], self.alpha[i])
190 } else {
191 self.bilinear(x, y, width, height)
192 };
193 light.push(l);
194 opaque.push(a >= 0.5);
195 }
196 }
197 Grey {
198 width,
199 height,
200 light,
201 opaque,
202 }
203 }
204
205 #[allow(
210 clippy::cast_precision_loss,
211 clippy::cast_possible_truncation,
212 clippy::cast_sign_loss
213 )] fn bilinear(&self, x: usize, y: usize, width: usize, height: usize) -> (f32, f32) {
215 let axis = |t: usize, to: usize, from: usize| {
216 let f = ((t as f32 + 0.5) * from as f32 / to as f32 - 0.5).max(0.0);
219 let low = f.floor() as usize;
220 (low, (low + 1).min(from - 1), f - f.floor())
221 };
222 let (x0, x1, wx) = axis(x, width, self.width);
223 let (y0, y1, wy) = axis(y, height, self.height);
224 let (mut lit, mut alpha) = (0.0, 0.0);
225 for (i, w) in [
226 (y0 * self.width + x0, (1.0 - wx) * (1.0 - wy)),
227 (y0 * self.width + x1, wx * (1.0 - wy)),
228 (y1 * self.width + x0, (1.0 - wx) * wy),
229 (y1 * self.width + x1, wx * wy),
230 ] {
231 lit += self.light[i] * self.alpha[i] * w;
232 alpha += self.alpha[i] * w;
233 }
234 (lit / alpha.max(f32::MIN_POSITIVE), alpha)
235 }
236
237 fn lines(&self, lines: Lines) -> Vec<bool> {
239 let at = |x: usize, y: usize| y * self.width + x;
240 let mut out = vec![false; self.light.len()];
241 for y in 0..self.height {
242 for x in 0..self.width {
243 let i = at(x, y);
244 if !self.opaque(i) {
245 continue;
246 }
247 let mut brightest: f32 = 0.0;
248 for (dx, dy) in [
249 (-1, -1),
250 (0, -1),
251 (1, -1),
252 (-1, 0),
253 (1, 0),
254 (-1, 1),
255 (0, 1),
256 (1, 1),
257 ] {
258 let Some(n) = x
259 .checked_add_signed(dx)
260 .zip(y.checked_add_signed(dy))
261 .filter(|&(nx, ny)| nx < self.width && ny < self.height)
262 .map(|(nx, ny)| at(nx, ny))
263 else {
264 continue;
265 };
266 let light = if self.opaque(n) { self.light[n] } else { 1.0 };
267 brightest = brightest.max(light);
268 }
269 let light = self.light[i];
270 out[i] = light <= lines.dark || brightest - light >= lines.contrast;
271 }
272 }
273 out
274 }
275}
276
277struct Grey {
279 width: usize,
280 height: usize,
281 light: Vec<f32>,
282 opaque: Vec<bool>,
283}
284
285impl Grey {
286 fn bits(&self, set: impl Fn(usize) -> bool) -> Vec<u8> {
288 #[allow(clippy::cast_possible_truncation)] let stride = Bitmap::stride(self.width as u16);
290 let mut rows = vec![0u8; stride * self.height];
291 for y in 0..self.height {
292 for x in 0..self.width {
293 if set(y * self.width + x) {
294 rows[y * stride + x / 8] |= 0x80 >> (x % 8);
295 }
296 }
297 }
298 rows
299 }
300}
301
302fn style(art: &Art, grey: &Grey, method: &Method) -> Vec<u8> {
304 let mut light: Vec<f32> = grey.light.clone();
305 for l in &mut light {
306 *l = l.powf(method.curve);
307 }
308 stretch(&mut light, &grey.opaque, method.stretch);
309 if method.levels >= 2 {
310 let steps = f32::from(method.levels - 1);
311 for l in &mut light {
312 *l = (*l * steps).round() / steps;
313 }
314 }
315 let lines = method.lines.map(|lines| art.lines(lines));
316 grey.bits(|i| {
317 let (x, y) = (i % grey.width, i / grey.width);
318 if !grey.opaque[i] {
319 return true;
320 }
321 if let Some(lines) = &lines {
322 let at = Art::nearest(y, art.height, grey.height) * art.width
323 + Art::nearest(x, art.width, grey.width);
324 if lines[at] {
325 return false;
326 }
327 }
328 light[i] * 16.0 > f32::from(BAYER[y % 4][x % 4]) + 0.5
329 })
330}
331
332#[allow(
336 clippy::cast_precision_loss,
337 clippy::cast_possible_truncation,
338 clippy::cast_sign_loss
339)] fn stretch(light: &mut [f32], opaque: &[bool], share: f32) {
341 let mut sorted: Vec<f32> = light
342 .iter()
343 .zip(opaque)
344 .filter_map(|(&l, &o)| o.then_some(l))
345 .collect();
346 if sorted.is_empty() || share <= 0.0 {
347 return;
348 }
349 sorted.sort_by(f32::total_cmp);
350 let last = (sorted.len() - 1) as f32;
351 let rank = |p: f32| sorted[(last * p.clamp(0.0, 1.0)).round() as usize];
352 let (low, high) = (rank(share), rank(1.0 - share));
353 if high - low <= f32::EPSILON {
354 return;
355 }
356 for l in light {
357 *l = ((*l - low) / (high - low)).clamp(0.0, 1.0);
358 }
359}
360
361#[cfg(feature = "png")]
365pub fn png_pixels(bytes: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
366 let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
367 let mut wanted = png::Transformations::EXPAND;
368 wanted.insert(png::Transformations::STRIP_16);
369 decoder.set_transformations(wanted);
370 let mut reader = decoder.read_info().map_err(|e| e.to_string())?;
371 let size = reader
372 .output_buffer_size()
373 .ok_or("the picture is too large to decode")?;
374 let mut buf = vec![0; size];
375 let info = reader.next_frame(&mut buf).map_err(|e| e.to_string())?;
376 buf.truncate(info.buffer_size());
377 let rgba = match info.color_type {
378 png::ColorType::Rgba => buf,
379 png::ColorType::Rgb => buf
380 .as_chunks::<3>()
381 .0
382 .iter()
383 .flat_map(|&[r, g, b]| [r, g, b, 255])
384 .collect(),
385 png::ColorType::GrayscaleAlpha => buf
386 .as_chunks::<2>()
387 .0
388 .iter()
389 .flat_map(|&[v, a]| [v, v, v, a])
390 .collect(),
391 png::ColorType::Grayscale => buf.iter().flat_map(|&v| [v, v, v, 255]).collect(),
392 png::ColorType::Indexed => return Err("a palette the decoder didn't expand".to_string()),
393 };
394 Ok((info.width, info.height, rgba))
395}
396
397#[cfg(test)]
398#[cfg(feature = "png")]
399mod png_tests {
400 use super::*;
401
402 fn png_file(
404 kind: png::ColorType,
405 depth: png::BitDepth,
406 pixels: &[u8],
407 palette: Option<&[u8]>,
408 ) -> Vec<u8> {
409 let mut out = Vec::new();
410 {
411 let mut encoder = png::Encoder::new(&mut out, 2, 1);
412 encoder.set_color(kind);
413 encoder.set_depth(depth);
414 if let Some(palette) = palette {
415 encoder.set_palette(palette.to_vec());
416 }
417 let mut writer = encoder.write_header().unwrap();
418 writer.write_image_data(pixels).unwrap();
419 }
420 out
421 }
422
423 #[test]
424 fn every_kind_of_png_reads_as_rgba() {
425 use png::{BitDepth::Eight, BitDepth::Sixteen, ColorType};
426 let rgba = [10, 20, 30, 40, 50, 60, 70, 80];
427 let cases = [
428 (png_file(ColorType::Rgba, Eight, &rgba, None), rgba),
429 (
430 png_file(ColorType::Rgb, Eight, &[10, 20, 30, 50, 60, 70], None),
431 [10, 20, 30, 255, 50, 60, 70, 255],
432 ),
433 (
434 png_file(ColorType::GrayscaleAlpha, Eight, &[9, 99, 200, 7], None),
435 [9, 9, 9, 99, 200, 200, 200, 7],
436 ),
437 (
438 png_file(ColorType::Grayscale, Eight, &[9, 200], None),
439 [9, 9, 9, 255, 200, 200, 200, 255],
440 ),
441 (
442 png_file(ColorType::Grayscale, Sixteen, &[9, 1, 200, 2], None),
443 [9, 9, 9, 255, 200, 200, 200, 255],
444 ),
445 (
446 png_file(
447 ColorType::Indexed,
448 Eight,
449 &[1, 0],
450 Some(&[1, 2, 3, 4, 5, 6]),
451 ),
452 [4, 5, 6, 255, 1, 2, 3, 255],
453 ),
454 ];
455 for (file, expected) in cases {
456 assert_eq!(png_pixels(&file), Ok((2, 1, expected.to_vec())));
457 }
458 assert!(png_pixels(b"not a png").is_err());
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 const PLAIN: Method = Method {
468 curve: 1.0,
469 stretch: 0.0,
470 levels: 0,
471 lines: None,
472 };
473
474 fn grey(v: u8) -> [u8; 4] {
476 [v, v, v, 255]
477 }
478
479 const CLEAR: [u8; 4] = [0, 0, 0, 0];
480
481 fn file(width: u32, pixels: &[[u8; 4]], scale: (u32, u32), methods: &[Method]) -> Vec<u8> {
483 let bytes: Vec<u8> = pixels.iter().flatten().copied().collect();
484 let height = u32::try_from(pixels.len()).unwrap() / width;
485 let image = Rgba::new(width, height, &bytes).unwrap();
486 convert(&image, scale, methods).unwrap()
487 }
488
489 fn rows(file: &[u8], n: usize) -> Vec<String> {
491 let bitmap = Bitmap::parse(file).unwrap();
492 (0..bitmap.height())
493 .map(|y| {
494 (0..bitmap.width())
495 .map(|x| match bitmap.pixel(n, x, y) {
496 Some(true) => '.',
497 Some(false) => '#',
498 None => ' ',
499 })
500 .collect()
501 })
502 .collect()
503 }
504
505 #[test]
506 fn half_grey_is_the_bayer_checker() {
507 let f = file(2, &[grey(128); 4], (2, 1), &[PLAIN]);
509 assert_eq!(rows(&f, 0), [".#.#", "#.#.", ".#.#", "#.#."]);
510 let bitmap = Bitmap::parse(&f).unwrap();
511 assert_eq!((bitmap.width(), bitmap.height()), (4, 4));
512 assert_eq!(bitmap.source_size(), (2, 2));
513 assert_eq!(bitmap.mask(), None);
514 }
515
516 #[test]
517 fn each_grey_has_its_share_of_white() {
518 let f = file(1, &[grey(64)], (4, 1), &[PLAIN]);
520 assert_eq!(rows(&f, 0), [".#.#", "####", ".#.#", "####"]);
521 let f = file(1, &[grey(0)], (4, 1), &[PLAIN]);
522 assert_eq!(rows(&f, 0), ["####"; 4]);
523 let f = file(1, &[grey(255)], (4, 1), &[PLAIN]);
524 assert_eq!(rows(&f, 0), ["...."; 4]);
525 }
526
527 #[test]
528 fn a_whole_scale_doubles_pixels() {
529 let f = file(2, &[grey(0), grey(255)], (2, 1), &[PLAIN]);
530 assert_eq!(rows(&f, 0), ["##..", "##.."]);
531 let f = file(2, &[grey(0), grey(255)], (6, 2), &[PLAIN]);
532 assert_eq!(rows(&f, 0), ["###...", "###...", "###..."]);
533 }
534
535 #[test]
536 fn a_scale_between_whole_numbers_resamples_smoothly() {
537 let f = file(
540 4,
541 &[grey(0), grey(0), grey(255), grey(255)],
542 (5, 4),
543 &[PLAIN],
544 );
545 assert_eq!(rows(&f, 0), ["##..."]);
546 let f = file(
547 4,
548 &[grey(0), grey(0), grey(255), grey(255)].repeat(4),
549 (5, 4),
550 &[PLAIN],
551 );
552 assert_eq!(rows(&f, 0), ["##...", "###..", "##...", "###..", "##..."]);
553 assert_eq!(Bitmap::parse(&f).unwrap().source_size(), (4, 4));
554 }
555
556 #[test]
557 fn see_through_pixels_are_masked_and_dont_darken_their_neighbours() {
558 let f = file(2, &[grey(255), CLEAR], (2, 1), &[PLAIN]);
559 assert_eq!(rows(&f, 0), [".. ", ".. "]);
560 let bitmap = Bitmap::parse(&f).unwrap();
561 assert_eq!(bitmap.mask(), Some(&[0b1100_0000, 0b1100_0000][..]));
562 assert_eq!(bitmap.style(0), Some(&[0xf0, 0xf0][..]));
563 let f = file(4, &[grey(255), grey(255), CLEAR, CLEAR], (5, 4), &[PLAIN]);
566 assert_eq!(rows(&f, 0), ["... "]);
567 }
568
569 #[test]
570 fn the_stretch_makes_the_darkest_black_and_the_lightest_white() {
571 let pixels = [grey(100), grey(120), grey(140), grey(160)];
572 let stretched = Method {
573 stretch: 0.03,
574 ..PLAIN
575 };
576 let f = file(4, &pixels, (1, 1), &[PLAIN, stretched]);
577 assert_eq!(rows(&f, 0), [".#.#"]);
579 assert_eq!(rows(&f, 1), ["##.."]);
581 assert_eq!(Method::BAYER, stretched);
582 let f = file(1, &[grey(128)], (4, 1), &[stretched]);
584 assert_eq!(rows(&f, 0), [".#.#", "#.#.", ".#.#", "#.#."]);
585 }
586
587 #[test]
588 fn the_curve_lightens_and_the_levels_round() {
589 let curved = Method {
590 curve: 0.5,
591 ..PLAIN
592 };
593 let f = file(1, &[grey(64)], (4, 1), &[curved]);
595 assert_eq!(rows(&f, 0), [".#.#", "#.#.", ".#.#", "#.#."]);
596 let rounded = Method { levels: 3, ..PLAIN };
597 let f = file(1, &[grey(153)], (4, 1), &[rounded]);
599 assert_eq!(rows(&f, 0), [".#.#", "#.#.", ".#.#", "#.#."]);
600 let f = file(1, &[grey(200)], (4, 1), &[rounded]);
601 assert_eq!(rows(&f, 0), ["...."; 4]);
602 }
603
604 #[test]
605 fn dark_lines_are_drawn_on_top() {
606 let lined = Method {
607 lines: Some(Lines {
608 contrast: 0.3,
609 dark: 0.15,
610 }),
611 ..PLAIN
612 };
613 let mut pixels = [grey(255); 9];
615 pixels[4] = grey(150);
616 let f = file(3, &pixels, (2, 1), &[PLAIN, lined]);
617 assert_eq!(rows(&f, 0)[2], "...#..");
619 let dot = ["......", "......", "..##..", "..##..", "......", "......"];
620 assert_eq!(rows(&f, 1), dot);
621 let mut pixels = [grey(128); 9];
624 pixels[0] = grey(30);
625 let f = file(3, &pixels, (1, 1), &[lined]);
626 assert_eq!(rows(&f, 0), ["##.", "#.#", ".#."]);
627 let f = file(2, &[grey(128), CLEAR], (1, 1), &[lined]);
630 assert_eq!(rows(&f, 0), ["# "]);
631 }
632
633 #[test]
634 fn every_method_is_a_style_sharing_one_mask() {
635 let methods = [PLAIN, Method::BAYER, Method { levels: 5, ..PLAIN }];
636 let f = file(2, &[grey(10), CLEAR, grey(240), grey(90)], (1, 1), &methods);
637 let bitmap = Bitmap::parse(&f).unwrap();
638 assert_eq!(bitmap.styles(), 3);
639 assert_eq!(bitmap.mask(), Some(&[0b1000_0000, 0b1100_0000][..]));
640 }
641
642 #[test]
643 fn bad_input_is_refused() {
644 let px = grey(0);
645 assert!(Rgba::new(1, 1, &px).is_some());
646 assert!(Rgba::new(2, 1, &px).is_none());
647 assert!(Rgba::new(0, 0, &[]).is_none());
648 let image = Rgba::new(1, 1, &px).unwrap();
649 assert!(convert(&image, (0, 1), &[PLAIN]).is_err());
650 assert!(convert(&image, (1, 0), &[PLAIN]).is_err());
651 assert!(convert(&image, (1, 1), &[]).is_err());
652 let wide = vec![0u8; 70_000 * 4];
653 let image = Rgba::new(70_000, 1, &wide).unwrap();
654 assert!(convert(&image, (1, 1), &[PLAIN]).is_err());
655 let image = Rgba::new(1, 1, &px).unwrap();
656 assert!(convert(&image, (70_000, 1), &[PLAIN]).is_err());
657 let f = convert(&image, (1, 4), &[PLAIN]).unwrap();
659 assert_eq!(Bitmap::parse(&f).unwrap().width(), 1);
660 }
661
662 fn art(width: usize, light: &[f32]) -> Art {
664 Art {
665 width,
666 height: light.len() / width,
667 light: light.to_vec(),
668 alpha: vec![1.0; light.len()],
669 }
670 }
671
672 fn close(a: f32, b: f32) -> bool {
673 (a - b).abs() < 1e-5
674 }
675
676 #[test]
677 fn the_resample_weighs_the_four_pixels_around_each_centre() {
678 let a = art(2, &[0.0, 1.0, 0.5, 0.25]);
681 let (light, alpha) = a.bilinear(1, 3, 5, 5);
682 let expected = 0.9 * 0.1 * 0.0 + 0.1 * 0.1 * 1.0 + 0.9 * 0.9 * 0.5 + 0.1 * 0.9 * 0.25;
683 assert!(close(light, expected), "{light}");
684 assert!(close(alpha, 1.0));
685 let (light, _) = a.bilinear(3, 1, 5, 5);
686 let expected = 0.1 * 0.9 * 0.0 + 0.9 * 0.9 * 1.0 + 0.1 * 0.1 * 0.5 + 0.9 * 0.1 * 0.25;
687 assert!(close(light, expected), "{light}");
688 assert!(close(a.bilinear(0, 0, 5, 5).0, 0.0));
690 assert!(close(a.bilinear(4, 4, 5, 5).0, 0.25));
691 let mut half = art(2, &[0.6, 0.0]);
694 half.alpha = vec![1.0, 0.0];
695 let (light, alpha) = half.bilinear(1, 0, 4, 1);
696 assert!(close(alpha, 0.75), "{alpha}");
697 assert!(close(light, 0.6), "{light}");
698 let (light, alpha) = half.bilinear(2, 0, 4, 1);
699 assert!(close(alpha, 0.25), "{alpha}");
700 assert!(close(light, 0.6), "{light}");
701 }
702
703 #[test]
704 fn a_pixel_less_than_half_opaque_is_see_through() {
705 let f = file(
706 2,
707 &[[255, 255, 255, 100], [255, 255, 255, 160]],
708 (1, 1),
709 &[PLAIN],
710 );
711 assert_eq!(rows(&f, 0), [" ."]);
712 }
713
714 #[test]
715 fn each_of_the_eight_neighbours_can_make_a_line() {
716 let lined = Method {
717 lines: Some(Lines {
718 contrast: 0.3,
719 dark: 0.15,
720 }),
721 ..PLAIN
722 };
723 for n in [0, 1, 2, 3, 5, 6, 7, 8] {
726 let mut pixels = [grey(128); 9];
727 pixels[n] = grey(255);
728 let f = file(3, &pixels, (1, 1), &[PLAIN, lined]);
729 assert_eq!(rows(&f, 0)[1].chars().nth(1), Some('.'), "neighbour {n}");
730 assert_eq!(rows(&f, 1)[1].chars().nth(1), Some('#'), "neighbour {n}");
731 }
732 }
733
734 #[test]
735 fn lines_land_on_the_nearest_art_pixel_when_resampled() {
736 let lined = Method {
737 lines: Some(Lines {
738 contrast: 0.3,
739 dark: 0.15,
740 }),
741 ..PLAIN
742 };
743 let pixels = [grey(255), grey(255), grey(150), grey(255)];
747 let f = file(4, &pixels, (5, 4), &[lined]);
748 assert_eq!(rows(&f, 0), ["..##."]);
749 assert_eq!(Art::nearest(2, 4, 5), 2);
750 assert_eq!(Art::nearest(4, 4, 5), 3);
751 assert_eq!(Art::nearest(5, 3, 6), 2);
752 }
753
754 #[test]
755 fn rows_wider_than_a_byte_are_packed_in_order() {
756 let (b, w) = (grey(0), grey(255));
758 let f = file(5, &[w, b, w, b, b, b, b, b, b, w], (2, 1), &[PLAIN]);
759 assert_eq!(
760 rows(&f, 0),
761 ["..##..####", "..##..####", "########..", "########.."]
762 );
763 let bitmap = Bitmap::parse(&f).unwrap();
764 assert_eq!(
765 bitmap.style(0),
766 Some(
767 &[
768 0b1100_1100,
769 0b0000_0000,
770 0b1100_1100,
771 0,
772 0,
773 0b1100_0000,
774 0,
775 0b1100_0000
776 ][..]
777 )
778 );
779 }
780
781 #[test]
782 fn the_stretch_takes_the_share_from_each_end() {
783 let mut light: Vec<f32> = (0..100u8).map(|i| f32::from(i) / 100.0).collect();
784 let opaque = vec![true; 100];
785 stretch(&mut light, &opaque, 0.03);
786 assert!(close(light[3], 0.0) && close(light[2], 0.0));
788 assert!(close(light[96], 1.0) && close(light[99], 1.0));
789 assert!(close(light[95], 0.92 / 0.93), "{}", light[95]);
790 assert!(close(light[50], 0.47 / 0.93), "{}", light[50]);
791 let mut light = vec![0.0, 0.4, 0.6, 1.0];
793 stretch(&mut light, &[false, true, true, false], 0.03);
794 assert!(close(light[1], 0.0) && close(light[2], 1.0));
795 }
796
797 #[test]
798 fn a_grey_exactly_on_a_rank_is_black_there() {
799 let rounded = Method {
802 levels: 33,
803 ..PLAIN
804 };
805 let f = file(1, &[grey(135)], (4, 1), &[rounded]);
806 assert_eq!(rows(&f, 0), [".#.#", "#.#.", ".#.#", "#.#."]);
807 }
808}