use std::num::NonZeroU8;
use crate::{
commands::{
ColorPower, DynamicCommandMode, RasterCommand, RasterCommands, VariousModeSettings,
},
media::{LabelType, Media},
raster_image::RasterImage,
};
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CutBehavior {
NoCut,
CutEach,
CutEvery(NonZeroU8),
CutAtEnd,
}
#[derive(Clone, PartialEq, Debug)]
pub struct PrintJob {
pub(crate) no_copies: NonZeroU8,
pub(crate) raster_images: Vec<RasterImage>,
pub(crate) media: Media,
pub(crate) high_dpi: bool,
pub(crate) compressed: bool,
pub(crate) quality_priority: bool,
pub(crate) cut_behavior: CutBehavior,
}
pub(crate) struct PrintJobParts {
pub preamble: RasterCommands,
pub page_data: Vec<RasterCommands>,
}
impl PrintJob {
pub(crate) fn page_count(&self) -> usize {
self.no_copies.get() as usize * self.raster_images.len()
}
pub(crate) fn into_parts(self) -> PrintJobParts {
let mut page_data = Vec::new();
for copy_no in 0..self.no_copies.get() {
for (img_idx, raster_image) in self.raster_images.iter().enumerate() {
use RasterCommand as RC;
let page_no = copy_no as usize * self.raster_images.len() + img_idx;
let mut page_commands = RasterCommands::default();
page_commands.add(RC::SwitchDynamicCommandMode {
command_mode: DynamicCommandMode::Raster,
});
page_commands.add(RC::SwitchAutomaticStatusNotificationMode { notify: true });
page_commands.add(RC::PrintInformation {
media: self.media,
quality_priority: match *raster_image {
RasterImage::Monochrome { .. } => self.quality_priority,
RasterImage::TwoColor { .. } => false,
},
recovery_on: true,
#[allow(clippy::cast_possible_truncation)]
no_lines: raster_image.height() as u32,
first_page: page_no == 0,
});
page_commands.add(RC::VariousMode(VariousModeSettings {
auto_cut: self.cut_behavior != CutBehavior::NoCut,
}));
match self.cut_behavior {
CutBehavior::CutEvery(n) => {
page_commands.add(RC::SpecifyPageNumber { cut_every: n.get() });
}
CutBehavior::CutEach => {
page_commands.add(RC::SpecifyPageNumber { cut_every: 1 });
}
_ => {}
}
page_commands.add(RC::ExpandedMode {
two_color: self.media.supports_color(),
cut_at_end: match self.cut_behavior {
CutBehavior::CutAtEnd => true,
CutBehavior::CutEvery(n) => !self.no_copies.get().is_multiple_of(n.get()),
_ => false,
},
high_dpi: self.high_dpi,
});
page_commands.add(RC::SpecifyMarginAmount {
margin_size: match self.media.label_type() {
LabelType::Continuous => 35,
LabelType::DieCut => 0,
},
});
page_commands.add(RC::SelectCompressionMode {
tiff_compression: false,
});
match raster_image {
RasterImage::Monochrome { black_layer } => {
for line in black_layer {
page_commands.add(RC::RasterGraphicsTransfer {
data: line.to_vec(),
});
}
}
RasterImage::TwoColor {
black_layer,
red_layer,
} => black_layer.iter().zip(red_layer.iter()).for_each(
|(black_line, red_line)| {
page_commands.add(RC::TwoColorRasterGraphicsTransfer {
data: black_line.to_vec(),
color_power: ColorPower::HighEnergy,
});
page_commands.add(RC::TwoColorRasterGraphicsTransfer {
data: red_line.to_vec(),
color_power: ColorPower::LowEnergy,
});
},
),
}
if page_no == self.page_count() - 1 {
page_commands.add(RC::PrintWithFeed);
} else {
page_commands.add(RC::Print);
}
page_data.push(page_commands);
}
}
PrintJobParts {
preamble: RasterCommands::create_preamble(),
page_data,
}
}
#[must_use]
pub fn compile(self) -> Vec<u8> {
let parts = self.into_parts();
let mut bytes = Vec::new();
bytes.append(&mut parts.preamble.build());
parts
.page_data
.into_iter()
.for_each(|p| bytes.append(&mut p.build()));
bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::printjob::PrintJobBuilder;
use image::{DynamicImage, RgbImage};
fn test_image(media: Media) -> DynamicImage {
let width = media.width_dots();
let height = media.length_dots().unwrap_or(100);
DynamicImage::ImageRgb8(RgbImage::from_pixel(
width,
height,
image::Rgb([255, 255, 255]),
))
}
#[test]
fn builder_preserves_all_settings() {
let n3 = NonZeroU8::new(3).unwrap();
let job = PrintJobBuilder::new(Media::C62)
.copies(NonZeroU8::new(5).unwrap())
.high_dpi(true)
.compressed(true)
.quality_priority(false)
.cut_behavior(CutBehavior::CutEvery(n3))
.add_label(test_image(Media::C62))
.build()
.unwrap();
assert_eq!(job.no_copies.get(), 5);
assert!(job.high_dpi);
assert!(job.compressed);
assert!(!job.quality_priority);
assert_eq!(job.cut_behavior, CutBehavior::CutEvery(n3));
}
#[test]
fn builder_settings_after_add_label_preserved() {
let job = PrintJobBuilder::new(Media::C62)
.add_label(test_image(Media::C62))
.copies(NonZeroU8::new(3).unwrap())
.high_dpi(true)
.quality_priority(false)
.cut_behavior(CutBehavior::NoCut)
.build()
.unwrap();
assert_eq!(job.no_copies.get(), 3);
assert!(job.high_dpi);
assert!(!job.quality_priority);
assert_eq!(job.cut_behavior, CutBehavior::NoCut);
}
#[test]
fn add_label_does_not_reset_cut_behavior() {
let job = PrintJobBuilder::new(Media::C62)
.cut_behavior(CutBehavior::CutAtEnd)
.add_label(test_image(Media::C62))
.build()
.unwrap();
assert_eq!(job.cut_behavior, CutBehavior::CutAtEnd);
}
#[test]
fn cut_every_nonzero_cannot_be_zero() {
assert!(NonZeroU8::new(0).is_none());
}
#[test]
fn builder_uses_defaults() {
let job = PrintJobBuilder::new(Media::C62)
.add_label(test_image(Media::C62))
.build()
.unwrap();
assert_eq!(job.no_copies.get(), 1);
assert!(!job.high_dpi);
assert!(!job.compressed);
assert!(job.quality_priority);
assert_eq!(job.cut_behavior, CutBehavior::CutEach); }
#[test]
fn die_cut_defaults_to_cut_at_end() {
let media = Media::D29x90;
let job = PrintJobBuilder::new(media)
.add_label(test_image(media))
.build()
.unwrap();
assert_eq!(job.cut_behavior, CutBehavior::CutAtEnd);
}
#[test]
fn dimension_mismatch_error() {
let wrong_image =
DynamicImage::ImageRgb8(RgbImage::from_pixel(100, 100, image::Rgb([255, 255, 255])));
let result = PrintJobBuilder::new(Media::C62)
.add_label(wrong_image)
.build();
assert!(result.is_err());
}
#[test]
fn page_count_with_copies() {
let job = PrintJobBuilder::new(Media::C62)
.copies(NonZeroU8::new(3).unwrap())
.add_label(test_image(Media::C62))
.add_label(test_image(Media::C62))
.build()
.unwrap();
assert_eq!(job.page_count(), 6); }
#[test]
fn compile_produces_nonempty_output() {
let job = PrintJobBuilder::new(Media::C62)
.add_label(test_image(Media::C62))
.build()
.unwrap();
let bytes = job.compile();
assert!(!bytes.is_empty());
assert!(bytes.len() > 400);
assert!(bytes[..400].iter().all(|&b| b == 0));
}
#[test]
fn into_parts_page_count_matches() {
let job = PrintJobBuilder::new(Media::C62)
.copies(NonZeroU8::new(2).unwrap())
.add_label(test_image(Media::C62))
.add_label(test_image(Media::C62))
.build()
.unwrap();
let expected_pages = job.page_count();
let parts = job.into_parts();
assert_eq!(parts.page_data.len(), expected_pages);
}
}