From 1b6a04ca5504955c571d1c97504fb45ea0befee4 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Mon, 8 Jan 2024 01:21:28 +0400 Subject: Initial vendor packages Signed-off-by: Valentin Popov --- vendor/png/examples/corpus-bench.rs | 198 +++++++++++++++++++ vendor/png/examples/png-generate.rs | 55 ++++++ vendor/png/examples/pngcheck.rs | 381 ++++++++++++++++++++++++++++++++++++ vendor/png/examples/show.rs | 198 +++++++++++++++++++ 4 files changed, 832 insertions(+) create mode 100644 vendor/png/examples/corpus-bench.rs create mode 100644 vendor/png/examples/png-generate.rs create mode 100644 vendor/png/examples/pngcheck.rs create mode 100644 vendor/png/examples/show.rs (limited to 'vendor/png/examples') diff --git a/vendor/png/examples/corpus-bench.rs b/vendor/png/examples/corpus-bench.rs new file mode 100644 index 0000000..b030d6d --- /dev/null +++ b/vendor/png/examples/corpus-bench.rs @@ -0,0 +1,198 @@ +use std::{fs, path::PathBuf}; + +use clap::Parser; +use png::Decoder; + +#[derive(clap::ValueEnum, Clone)] +enum Speed { + Fast, + Default, + Best, +} + +#[derive(clap::ValueEnum, Clone)] +enum Filter { + None, + Sub, + Up, + Average, + Paeth, + Adaptive, +} + +#[derive(clap::Parser)] +struct Args { + directory: Option, + #[clap(short, long, value_enum, default_value_t = Speed::Fast)] + speed: Speed, + #[clap(short, long, value_enum, default_value_t = Filter::Adaptive)] + filter: Filter, +} + +#[inline(never)] +fn run_encode( + args: &Args, + dimensions: (u32, u32), + color_type: png::ColorType, + bit_depth: png::BitDepth, + image: &[u8], +) -> Vec { + let mut reencoded = Vec::new(); + let mut encoder = png::Encoder::new(&mut reencoded, dimensions.0, dimensions.1); + encoder.set_color(color_type); + encoder.set_depth(bit_depth); + encoder.set_compression(match args.speed { + Speed::Fast => png::Compression::Fast, + Speed::Default => png::Compression::Default, + Speed::Best => png::Compression::Best, + }); + encoder.set_filter(match args.filter { + Filter::None => png::FilterType::NoFilter, + Filter::Sub => png::FilterType::Sub, + Filter::Up => png::FilterType::Up, + Filter::Average => png::FilterType::Avg, + Filter::Paeth => png::FilterType::Paeth, + Filter::Adaptive => png::FilterType::Paeth, + }); + encoder.set_adaptive_filter(match args.filter { + Filter::Adaptive => png::AdaptiveFilterType::Adaptive, + _ => png::AdaptiveFilterType::NonAdaptive, + }); + let mut encoder = encoder.write_header().unwrap(); + encoder.write_image_data(&image).unwrap(); + encoder.finish().unwrap(); + reencoded +} + +#[inline(never)] +fn run_decode(image: &[u8], output: &mut [u8]) { + let mut reader = Decoder::new(image).read_info().unwrap(); + reader.next_frame(output).unwrap(); +} + +fn main() { + let mut total_uncompressed = 0; + let mut total_compressed = 0; + let mut total_pixels = 0; + let mut total_encode_time = 0; + let mut total_decode_time = 0; + + let args = Args::parse(); + + println!( + "{:45} Ratio Encode Decode", + "Directory" + ); + println!( + "{:45}------- -------------------- --------------------", + "---------" + ); + + let mut image2 = Vec::new(); + + let mut pending = vec![args.directory.clone().unwrap_or(PathBuf::from("."))]; + while let Some(directory) = pending.pop() { + let mut dir_uncompressed = 0; + let mut dir_compressed = 0; + let mut dir_pixels = 0; + let mut dir_encode_time = 0; + let mut dir_decode_time = 0; + + for entry in fs::read_dir(&directory).unwrap().flatten() { + if entry.file_type().unwrap().is_dir() { + pending.push(entry.path()); + continue; + } + + match entry.path().extension() { + Some(st) if st == "png" => {} + _ => continue, + } + + // Parse + let data = fs::read(entry.path()).unwrap(); + let mut decoder = Decoder::new(&*data); + if decoder.read_header_info().ok().map(|h| h.color_type) + == Some(png::ColorType::Indexed) + { + decoder.set_transformations( + png::Transformations::EXPAND | png::Transformations::STRIP_16, + ); + } + let mut reader = match decoder.read_info() { + Ok(reader) => reader, + Err(_) => continue, + }; + let mut image = vec![0; reader.output_buffer_size()]; + let info = match reader.next_frame(&mut image) { + Ok(info) => info, + Err(_) => continue, + }; + let (width, height) = (info.width, info.height); + let bit_depth = info.bit_depth; + let mut color_type = info.color_type; + + // qoibench expands grayscale to RGB, so we do the same. + if bit_depth == png::BitDepth::Eight { + if color_type == png::ColorType::Grayscale { + image = image.into_iter().flat_map(|v| [v, v, v, 255]).collect(); + color_type = png::ColorType::Rgba; + } else if color_type == png::ColorType::GrayscaleAlpha { + image = image + .chunks_exact(2) + .flat_map(|v| [v[0], v[0], v[0], v[1]]) + .collect(); + color_type = png::ColorType::Rgba; + } + } + + // Re-encode + let start = std::time::Instant::now(); + let reencoded = run_encode(&args, (width, height), color_type, bit_depth, &image); + let elapsed = start.elapsed().as_nanos() as u64; + + // And decode again + image2.resize(image.len(), 0); + let start2 = std::time::Instant::now(); + run_decode(&reencoded, &mut image2); + let elapsed2 = start2.elapsed().as_nanos() as u64; + + assert_eq!(image, image2); + + // Stats + dir_uncompressed += image.len(); + dir_compressed += reencoded.len(); + dir_pixels += (width * height) as u64; + dir_encode_time += elapsed; + dir_decode_time += elapsed2; + } + if dir_uncompressed > 0 { + println!( + "{:45}{:6.2}%{:8} mps {:6.2} GiB/s {:8} mps {:6.2} GiB/s", + directory.display(), + 100.0 * dir_compressed as f64 / dir_uncompressed as f64, + dir_pixels * 1000 / dir_encode_time, + dir_uncompressed as f64 / (dir_encode_time as f64 * 1e-9 * (1 << 30) as f64), + dir_pixels * 1000 / dir_decode_time, + dir_uncompressed as f64 / (dir_decode_time as f64 * 1e-9 * (1 << 30) as f64) + ); + } + + total_uncompressed += dir_uncompressed; + total_compressed += dir_compressed; + total_pixels += dir_pixels; + total_encode_time += dir_encode_time; + total_decode_time += dir_decode_time; + } + + println!(); + println!( + "{:44}{:7.3}%{:8} mps {:6.3} GiB/s {:8} mps {:6.3} GiB/s", + "Total", + 100.0 * total_compressed as f64 / total_uncompressed as f64, + total_pixels * 1000 / total_encode_time, + total_uncompressed as f64 / (total_encode_time as f64 * 1e-9 * (1 << 30) as f64), + total_pixels * 1000 / total_decode_time, + total_uncompressed as f64 / (total_decode_time as f64 * 1e-9 * (1 << 30) as f64) + ); +} diff --git a/vendor/png/examples/png-generate.rs b/vendor/png/examples/png-generate.rs new file mode 100644 index 0000000..9036a04 --- /dev/null +++ b/vendor/png/examples/png-generate.rs @@ -0,0 +1,55 @@ +// For reading and opening files +use png::text_metadata::{ITXtChunk, ZTXtChunk}; +use std::env; +use std::fs::File; +use std::io::BufWriter; + +fn main() { + let path = env::args() + .nth(1) + .expect("Expected a filename to output to."); + let file = File::create(path).unwrap(); + let w = &mut BufWriter::new(file); + + let mut encoder = png::Encoder::new(w, 2, 1); // Width is 2 pixels and height is 1. + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + // Adding text chunks to the header + encoder + .add_text_chunk( + "Testing tEXt".to_string(), + "This is a tEXt chunk that will appear before the IDAT chunks.".to_string(), + ) + .unwrap(); + encoder + .add_ztxt_chunk( + "Testing zTXt".to_string(), + "This is a zTXt chunk that is compressed in the png file.".to_string(), + ) + .unwrap(); + encoder + .add_itxt_chunk( + "Testing iTXt".to_string(), + "iTXt chunks support all of UTF8. Example: हिंदी.".to_string(), + ) + .unwrap(); + + let mut writer = encoder.write_header().unwrap(); + + let data = [255, 0, 0, 255, 0, 0, 0, 255]; // An array containing a RGBA sequence. First pixel is red and second pixel is black. + writer.write_image_data(&data).unwrap(); // Save + + // We can add a tEXt/zTXt/iTXt at any point before the encoder is dropped from scope. These chunks will be at the end of the png file. + let tail_ztxt_chunk = ZTXtChunk::new( + "Comment".to_string(), + "A zTXt chunk after the image data.".to_string(), + ); + writer.write_text_chunk(&tail_ztxt_chunk).unwrap(); + + // The fields of the text chunk are public, so they can be mutated before being written to the file. + let mut tail_itxt_chunk = ITXtChunk::new("Author".to_string(), "सायंतन खान".to_string()); + tail_itxt_chunk.compressed = true; + tail_itxt_chunk.language_tag = "hi".to_string(); + tail_itxt_chunk.translated_keyword = "लेखक".to_string(); + writer.write_text_chunk(&tail_itxt_chunk).unwrap(); +} diff --git a/vendor/png/examples/pngcheck.rs b/vendor/png/examples/pngcheck.rs new file mode 100644 index 0000000..69e95e3 --- /dev/null +++ b/vendor/png/examples/pngcheck.rs @@ -0,0 +1,381 @@ +#![allow(non_upper_case_globals)] + +extern crate getopts; +extern crate glob; +extern crate png; + +use std::env; +use std::fs::File; +use std::io; +use std::io::prelude::*; +use std::path::Path; + +use getopts::{Matches, Options, ParsingStyle}; +use term::{color, Attr}; + +fn parse_args() -> Matches { + let args: Vec = env::args().collect(); + let mut opts = Options::new(); + opts.optflag("c", "", "colorize output (for ANSI terminals)") + .optflag("q", "", "test quietly (output only errors)") + .optflag( + "t", + "", + "print contents of tEXt/zTXt/iTXt chunks (can be used with -q)", + ) + .optflag("v", "", "test verbosely (print most chunk data)") + .parsing_style(ParsingStyle::StopAtFirstFree); + if args.len() > 1 { + match opts.parse(&args[1..]) { + Ok(matches) => return matches, + Err(err) => println!("{}", err), + } + } + println!("{}", opts.usage("Usage: pngcheck [-cpt] [file ...]")); + std::process::exit(0); +} + +#[derive(Clone, Copy)] +struct Config { + quiet: bool, + verbose: bool, + color: bool, + text: bool, +} + +fn display_interlaced(i: bool) -> &'static str { + if i { + "interlaced" + } else { + "non-interlaced" + } +} + +fn display_image_type(bits: u8, color: png::ColorType) -> String { + use png::ColorType::*; + format!( + "{}-bit {}", + bits, + match color { + Grayscale => "grayscale", + Rgb => "RGB", + Indexed => "palette", + GrayscaleAlpha => "grayscale+alpha", + Rgba => "RGB+alpha", + } + ) +} +// channels after expansion of tRNS +fn final_channels(c: png::ColorType, trns: bool) -> u8 { + use png::ColorType::*; + match c { + Grayscale => 1 + u8::from(trns), + Rgb => 3, + Indexed => 3 + u8::from(trns), + GrayscaleAlpha => 2, + Rgba => 4, + } +} +fn check_image>(c: Config, fname: P) -> io::Result<()> { + // TODO improve performance by resusing allocations from decoder + use png::Decoded::*; + let mut t = term::stdout() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "could not open terminal"))?; + let data = &mut vec![0; 10 * 1024][..]; + let mut reader = io::BufReader::new(File::open(&fname)?); + let fname = fname.as_ref().to_string_lossy(); + let n = reader.read(data)?; + let mut buf = &data[..n]; + let mut pos = 0; + let mut decoder = png::StreamingDecoder::new(); + // Image data + let mut width = 0; + let mut height = 0; + let mut color = png::ColorType::Grayscale; + let mut bits = 0; + let mut trns = false; + let mut interlaced = false; + let mut compressed_size = 0; + let mut n_chunks = 0; + let mut have_idat = false; + macro_rules! c_ratio( + // TODO add palette entries to compressed_size + () => ({ + compressed_size as f32/( + height as u64 * + (width as u64 * final_channels(color, trns) as u64 * bits as u64 + 7)>>3 + ) as f32 + }); + ); + let display_error = |err| -> Result<_, io::Error> { + let mut t = term::stdout() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "could not open terminal"))?; + if c.verbose { + if c.color { + print!(": "); + t.fg(color::RED)?; + writeln!(t, "{}", err)?; + t.attr(Attr::Bold)?; + write!(t, "ERRORS DETECTED")?; + t.reset()?; + } else { + println!(": {}", err); + print!("ERRORS DETECTED") + } + println!(" in {}", fname); + } else { + if !c.quiet { + if c.color { + t.fg(color::RED)?; + t.attr(Attr::Bold)?; + write!(t, "ERROR")?; + t.reset()?; + write!(t, ": ")?; + t.fg(color::YELLOW)?; + writeln!(t, "{}", fname)?; + t.reset()?; + } else { + println!("ERROR: {}", fname) + } + } + print!("{}: ", fname); + if c.color { + t.fg(color::RED)?; + writeln!(t, "{}", err)?; + t.reset()?; + } else { + println!("{}", err); + } + } + Ok(()) + }; + + if c.verbose { + print!("File: "); + if c.color { + t.attr(Attr::Bold)?; + write!(t, "{}", fname)?; + t.reset()?; + } else { + print!("{}", fname); + } + print!(" ({}) bytes", data.len()) + } + loop { + if buf.is_empty() { + // circumvent borrow checker + assert!(!data.is_empty()); + let n = reader.read(data)?; + + // EOF + if n == 0 { + println!("ERROR: premature end of file {}", fname); + break; + } + buf = &data[..n]; + } + match decoder.update(buf, &mut Vec::new()) { + Ok((_, ImageEnd)) => { + if !have_idat { + // This isn't beautiful. But it works. + display_error(png::DecodingError::IoError(io::Error::new( + io::ErrorKind::InvalidData, + "IDAT chunk missing", + )))?; + break; + } + if !c.verbose && !c.quiet { + if c.color { + t.fg(color::GREEN)?; + t.attr(Attr::Bold)?; + write!(t, "OK")?; + t.reset()?; + write!(t, ": ")?; + t.fg(color::YELLOW)?; + write!(t, "{}", fname)?; + t.reset()?; + } else { + print!("OK: {}", fname) + } + println!( + " ({}x{}, {}{}, {}, {:.1}%)", + width, + height, + display_image_type(bits, color), + (if trns { "+trns" } else { "" }), + display_interlaced(interlaced), + 100.0 * (1.0 - c_ratio!()) + ) + } else if !c.quiet { + println!(); + if c.color { + t.fg(color::GREEN)?; + t.attr(Attr::Bold)?; + write!(t, "No errors detected ")?; + t.reset()?; + } else { + print!("No errors detected "); + } + println!( + "in {} ({} chunks, {:.1}% compression)", + fname, + n_chunks, + 100.0 * (1.0 - c_ratio!()), + ) + } + break; + } + Ok((n, res)) => { + buf = &buf[n..]; + pos += n; + match res { + Header(w, h, b, c, i) => { + width = w; + height = h; + bits = b as u8; + color = c; + interlaced = i; + } + ChunkBegin(len, type_str) => { + use png::chunk; + n_chunks += 1; + if c.verbose { + let chunk = type_str; + println!(); + print!(" chunk "); + if c.color { + t.fg(color::YELLOW)?; + write!(t, "{:?}", chunk)?; + t.reset()?; + } else { + print!("{:?}", chunk) + } + print!( + " at offset {:#07x}, length {}", + pos - 4, // substract chunk name length + len + ) + } + match type_str { + chunk::IDAT => { + have_idat = true; + compressed_size += len + } + chunk::tRNS => { + trns = true; + } + _ => (), + } + } + ImageData => { + //println!("got {} bytes of image data", data.len()) + } + ChunkComplete(_, type_str) if c.verbose => { + use png::chunk::*; + if type_str == IHDR { + println!(); + print!( + " {} x {} image, {}{}, {}", + width, + height, + display_image_type(bits, color), + (if trns { "+trns" } else { "" }), + display_interlaced(interlaced), + ); + } + } + AnimationControl(actl) => { + println!(); + print!(" {} frames, {} plays", actl.num_frames, actl.num_plays,); + } + FrameControl(fctl) => { + println!(); + println!( + " sequence #{}, {} x {} pixels @ ({}, {})", + fctl.sequence_number, + fctl.width, + fctl.height, + fctl.x_offset, + fctl.y_offset, + /*fctl.delay_num, + fctl.delay_den, + fctl.dispose_op, + fctl.blend_op,*/ + ); + print!( + " {}/{} s delay, dispose: {}, blend: {}", + fctl.delay_num, + if fctl.delay_den == 0 { + 100 + } else { + fctl.delay_den + }, + fctl.dispose_op, + fctl.blend_op, + ); + } + _ => (), + } + //println!("{} {:?}", n, res) + } + Err(err) => { + let _ = display_error(err); + break; + } + } + } + if c.text { + println!("Parsed tEXt chunks:"); + for text_chunk in &decoder.info().unwrap().uncompressed_latin1_text { + println!("{:#?}", text_chunk); + } + + println!("Parsed zTXt chunks:"); + for text_chunk in &decoder.info().unwrap().compressed_latin1_text { + let mut cloned_text_chunk = text_chunk.clone(); + cloned_text_chunk.decompress_text()?; + println!("{:#?}", cloned_text_chunk); + } + + println!("Parsed iTXt chunks:"); + for text_chunk in &decoder.info().unwrap().utf8_text { + let mut cloned_text_chunk = text_chunk.clone(); + cloned_text_chunk.decompress_text()?; + println!("{:#?}", cloned_text_chunk); + } + } + + Ok(()) +} + +fn main() { + let m = parse_args(); + + let config = Config { + quiet: m.opt_present("q"), + verbose: m.opt_present("v"), + color: m.opt_present("c"), + text: m.opt_present("t"), + }; + + for file in m.free { + let result = if file.contains('*') { + glob::glob(&file) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + .and_then(|mut glob| { + glob.try_for_each(|entry| { + entry + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + .and_then(|file| check_image(config, file)) + }) + }) + } else { + check_image(config, &file) + }; + + result.unwrap_or_else(|err| { + println!("{}: {}", file, err); + std::process::exit(1) + }); + } +} diff --git a/vendor/png/examples/show.rs b/vendor/png/examples/show.rs new file mode 100644 index 0000000..d8ddf75 --- /dev/null +++ b/vendor/png/examples/show.rs @@ -0,0 +1,198 @@ +use glium::{ + backend::glutin::Display, + glutin::{ + self, dpi, + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::ControlFlow, + }, + texture::{ClientFormat, RawImage2d}, + BlitTarget, Rect, Surface, +}; +use std::{borrow::Cow, env, fs::File, io, path}; + +/// Load the image using `png` +fn load_image(path: &path::PathBuf) -> io::Result> { + use png::ColorType::*; + let mut decoder = png::Decoder::new(File::open(path)?); + decoder.set_transformations(png::Transformations::normalize_to_color8()); + let mut reader = decoder.read_info()?; + let mut img_data = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut img_data)?; + + let (data, format) = match info.color_type { + Rgb => (img_data, ClientFormat::U8U8U8), + Rgba => (img_data, ClientFormat::U8U8U8U8), + Grayscale => ( + { + let mut vec = Vec::with_capacity(img_data.len() * 3); + for g in img_data { + vec.extend([g, g, g].iter().cloned()) + } + vec + }, + ClientFormat::U8U8U8, + ), + GrayscaleAlpha => ( + { + let mut vec = Vec::with_capacity(img_data.len() * 3); + for ga in img_data.chunks(2) { + let g = ga[0]; + let a = ga[1]; + vec.extend([g, g, g, a].iter().cloned()) + } + vec + }, + ClientFormat::U8U8U8U8, + ), + _ => unreachable!("uncovered color type"), + }; + + Ok(RawImage2d { + data: Cow::Owned(data), + width: info.width, + height: info.height, + format, + }) +} + +fn main_loop(files: Vec) -> io::Result<()> { + let mut files = files.into_iter(); + let image = load_image(&files.next().unwrap())?; + + let event_loop = glutin::event_loop::EventLoop::new(); + let window_builder = glutin::window::WindowBuilder::new().with_title("Show Example"); + let context_builder = glutin::ContextBuilder::new().with_vsync(true); + let display = glium::Display::new(window_builder, context_builder, &event_loop) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + resize_window(&display, &image); + let mut texture = glium::Texture2d::new(&display, image).unwrap(); + draw(&display, &texture); + + event_loop.run(move |event, _, control_flow| match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => exit(control_flow), + Event::WindowEvent { + event: + WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Pressed, + virtual_keycode: code, + .. + }, + .. + }, + .. + } => match code { + Some(VirtualKeyCode::Escape) => exit(control_flow), + Some(VirtualKeyCode::Right) => match &files.next() { + Some(path) => { + match load_image(path) { + Ok(image) => { + resize_window(&display, &image); + texture = glium::Texture2d::new(&display, image).unwrap(); + draw(&display, &texture); + } + Err(err) => { + println!("Error: {}", err); + exit(control_flow); + } + }; + } + None => exit(control_flow), + }, + _ => {} + }, + Event::RedrawRequested(_) => draw(&display, &texture), + _ => {} + }); +} + +fn draw(display: &glium::Display, texture: &glium::Texture2d) { + let frame = display.draw(); + fill_v_flipped( + &texture.as_surface(), + &frame, + glium::uniforms::MagnifySamplerFilter::Linear, + ); + frame.finish().unwrap(); +} + +fn exit(control_flow: &mut ControlFlow) { + *control_flow = ControlFlow::Exit; +} + +fn fill_v_flipped(src: &S1, target: &S2, filter: glium::uniforms::MagnifySamplerFilter) +where + S1: Surface, + S2: Surface, +{ + let src_dim = src.get_dimensions(); + let src_rect = Rect { + left: 0, + bottom: 0, + width: src_dim.0 as u32, + height: src_dim.1 as u32, + }; + let target_dim = target.get_dimensions(); + let target_rect = BlitTarget { + left: 0, + bottom: target_dim.1, + width: target_dim.0 as i32, + height: -(target_dim.1 as i32), + }; + src.blit_color(&src_rect, target, &target_rect, filter); +} + +fn resize_window(display: &Display, image: &RawImage2d<'static, u8>) { + let mut width = image.width; + let mut height = image.height; + if width < 50 && height < 50 { + width *= 10; + height *= 10; + } + display + .gl_window() + .window() + .set_inner_size(dpi::LogicalSize::new(f64::from(width), f64::from(height))); +} + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 2 { + println!("Usage: show files [...]"); + } else { + let mut files = vec![]; + for file in args.iter().skip(1) { + match if file.contains('*') { + (|| -> io::Result<_> { + for entry in glob::glob(file) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err.msg))? + { + files.push( + entry + .map_err(|_| io::Error::new(io::ErrorKind::Other, "glob error"))?, + ) + } + Ok(()) + })() + } else { + files.push(path::PathBuf::from(file)); + Ok(()) + } { + Ok(_) => (), + Err(err) => { + println!("{}: {}", file, err); + break; + } + } + } + // "tests/pngsuite/pngsuite.png" + match main_loop(files) { + Ok(_) => (), + Err(err) => println!("Error: {}", err), + } + } +} -- cgit v1.2.3