use calamine::{open_workbook, Data, Reader, Xlsx}; use printpdf::image::RawImage; use printpdf::{ Mm, Op, PdfDocument, PdfPage, PdfSaveOptions, PdfWarnMsg, RawImageData, RawImageFormat, XObjectTransform, }; use structopt::StructOpt; use ab_glyph::{FontRef, PxScale}; use image::{ImageBuffer, Rgba}; use imageproc::drawing::draw_text_mut; use inflector::Inflector; // A4 dimensions const A4_WIDTH: f32 = 210.0; const A4_HEIGHT: f32 = 297.0; // Configuration for the label const DPI: f32 = 300.0; const MM_TO_PX: f32 = DPI / 25.4; const LABEL_WIDTH: f32 = 210.0 * MM_TO_PX; const LABEL_HEIGHT: f32 = 16.0 * MM_TO_PX; // Elements positionning const MARGIN_LEFT: f32 = 80.0 * MM_TO_PX; const MARGIN_RIGHT: f32 = 10.0 * MM_TO_PX; // const LOGO_WIDTH: u32 = 151; // const LOGO_HEIGHT: u32 = 64; // const LOGO_POS_X: u32 = LABEL_WIDTH as u32 - LOGO_WIDTH - 10; // const LOGO_POS_Y: u32 = 10; const CLIENT_CODE_POS_X: i32 = MARGIN_LEFT as i32; const CLIENT_CODE_POS_Y: i32 = 10; const CLIENT_CODE_SCALE: f32 = 100.0; const SPECIES_VARIETY_POS_X: i32 = MARGIN_LEFT as i32 + 220; const SPECIES_VARIETY_POS_Y: i32 = 20; const SPECIES_VARIETY_SCALE: f32 = 80.0; const DATE_POS_X: i32 = LABEL_WIDTH as i32 - MARGIN_RIGHT as i32 - 150; const DATE_POS_Y: i32 = 10; const DATE_SCALE: f32 = 75.0; const COLOR_TEXT: Rgba = Rgba([0, 0, 0, 255]); const COLOR_BACKGROUND: Rgba = Rgba([255, 255, 255, 255]); #[derive(Debug, Clone)] struct Record { client_code: String, species: String, variety: String, delivery_week: String, } const COLUMN_MAP: [(&str, char); 5] = [ ("client_code", 'A'), ("species", 'B'), ("variety", 'C'), ("sowing_week", 'I'), ("delivery_week", 'J'), ]; fn letter_to_index(col: char) -> usize { (col as u8 - b'A') as usize } fn column_name_to_index(column: &str) -> usize { COLUMN_MAP .map(|(name, col)| (name, letter_to_index(col))) .iter() .find(|(name, _)| *name == column) .unwrap() .1 } fn get_cell_value(row: &[Data], column: &str) -> String { let col_index = column_name_to_index(column); match row.get(col_index) { Some(Data::String(s)) => s.to_string(), Some(Data::Float(f)) => f.to_string(), _ => "".to_string(), } } fn parse_xlsx(delivery_week_filter: &str) -> Result, Box> { let mut workbook: Xlsx<_> = open_workbook("data.xlsx")?; let range = workbook.worksheet_range("Feuille1")?; let records: Vec = range .rows() .filter_map(|row| { let delivery_week = get_cell_value(row, "delivery_week"); if delivery_week.to_lowercase() != delivery_week_filter.to_lowercase() { return None; } Some(Record { client_code: get_cell_value(row, "client_code").to_uppercase(), species: get_cell_value(row, "species").to_title_case(), variety: get_cell_value(row, "variety").to_title_case(), delivery_week: delivery_week.to_uppercase(), }) }) .collect(); Ok(records) } fn generate_label(record: Record) -> ImageBuffer, Vec> { let font_data = include_bytes!("./assets/DejaVuSans.ttf"); let font = FontRef::try_from_slice(font_data).unwrap(); let mut label: ImageBuffer, Vec> = ImageBuffer::new(LABEL_WIDTH as u32, LABEL_HEIGHT as u32); // Fill in the background for pixel in label.pixels_mut() { *pixel = COLOR_BACKGROUND; } // Logo // let logo = image::open("./assets/logo.png").unwrap(); // let resized_logo = image::imageops::resize( // &logo, // LOGO_WIDTH, // LOGO_HEIGHT, // image::imageops::FilterType::Lanczos3, // ); // image::imageops::overlay( // &mut label, // &resized_logo, // LOGO_POS_X as i64, // LOGO_POS_Y as i64, // ); draw_text_mut( &mut label, COLOR_TEXT, CLIENT_CODE_POS_X, CLIENT_CODE_POS_Y, PxScale::from(CLIENT_CODE_SCALE), &font, &record.client_code, ); draw_text_mut( &mut label, COLOR_TEXT, DATE_POS_X, DATE_POS_Y, PxScale::from(DATE_SCALE), &font, &record.delivery_week, ); let species_varieties: String = "".to_owned() + &record.species + " " + &record.variety; draw_text_mut( &mut label, COLOR_TEXT, SPECIES_VARIETY_POS_X, SPECIES_VARIETY_POS_Y, PxScale::from(SPECIES_VARIETY_SCALE), &font, &species_varieties, ); return label; } fn combine_labels( labels: &[ImageBuffer, Vec>], output_dir: &str, ) -> Result<(), Box> { let labels_per_page = (A4_HEIGHT / (LABEL_HEIGHT / MM_TO_PX)) as usize; println!("Labels per page: {}", labels_per_page); // Create a new PDF document let mut doc = PdfDocument::new("Labels Document"); let mut pages = Vec::new(); let mut warnings: Vec = Vec::new(); // Group labels by page for chunk in labels.chunks(labels_per_page) { let mut page_contents = Vec::new(); let mut y_position = 0.0; for label in chunk { let pixels = label.clone().into_raw(); let image = RawImage { pixels: RawImageData::U8(pixels), width: LABEL_WIDTH as usize, height: LABEL_HEIGHT as usize, data_format: RawImageFormat::RGBA8, tag: Vec::new(), }; let image_id = doc.add_image(&image); page_contents.push(Op::UseXobject { id: image_id, transform: XObjectTransform { translate_x: None, translate_y: Some(printpdf::Px(y_position as usize).into_pt(DPI)), rotate: None, scale_x: None, scale_y: None, dpi: None, }, }); // Move y position up for next label y_position += LABEL_HEIGHT; } // Create page and add to pages list let page = PdfPage::new(Mm(A4_WIDTH), Mm(A4_HEIGHT), page_contents); pages.push(page); } // Add all pages to document and save let pdf_bytes: Vec = doc .with_pages(pages) .save(&PdfSaveOptions::default(), &mut warnings); // Write PDF bytes to file std::fs::write("test.pdf", pdf_bytes)?; Ok(()) } #[derive(StructOpt)] struct Opts { #[structopt(short = "o", long, default_value = "output")] output_dir: String, #[structopt(short = "w", long, default_value = "06")] week: String, } fn main() -> Result<(), Box> { let opts = Opts::from_args(); std::fs::create_dir_all(&opts.output_dir).unwrap(); let week_number = "s".to_owned() + &opts.week; let records = parse_xlsx(&week_number)?; let mut labels = Vec::new(); for record in records { labels.push(generate_label(record)); } println!("labels: {}", labels.len()); combine_labels(&labels, &opts.output_dir) }