use hardless_escpos::{Align, Font, HriPosition, Node, QrErrorCorrection, Symbology};
use crate::PaperWidth;
pub(crate) const DOTS_PER_COLUMN: u32 = 12;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Style {
pub bold: bool,
pub underline: u8,
pub width: u8,
pub height: u8,
pub font: Font,
}
impl Style {
fn reset() -> Self {
Self {
width: 1,
height: 1,
..Self::default()
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Segment {
Text { text: String, style: Style },
Unknown { label: String, title: String },
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Line {
pub align: Align,
pub segments: Vec<Segment>,
pub columns: u32,
}
impl Line {
pub fn height(&self) -> u8 {
self.segments
.iter()
.filter_map(|s| match s {
Segment::Text { style, .. } => Some(style.height),
Segment::Unknown { .. } => None,
})
.max()
.unwrap_or(1)
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Block {
Line(Line),
Cut {
partial: bool,
},
Qr {
align: Align,
data: Vec<u8>,
text: String,
module_dots: u8,
level: QrErrorCorrection,
},
Barcode {
align: Align,
symbology: Symbology,
data: String,
height_dots: u8,
module_dots: u8,
hri: HriPosition,
},
Image {
align: Align,
width_bytes: u16,
height: u16,
double_width: bool,
double_height: bool,
data: Vec<u8>,
},
}
struct Layout {
columns_font_a: u32,
columns_font_b: u32,
show_unknown: bool,
align: Align,
style: Style,
line: Line,
blocks: Vec<Block>,
qr_data: Option<(Vec<u8>, String)>,
qr_module: u8,
qr_level: QrErrorCorrection,
barcode_height: u8,
barcode_module: u8,
hri: HriPosition,
}
pub(crate) fn layout(nodes: &[Node], paper: PaperWidth, show_unknown: bool) -> Vec<Block> {
let (columns_font_a, columns_font_b) = paper.columns();
let mut l = Layout {
columns_font_a,
columns_font_b,
show_unknown,
align: Align::Left,
style: Style::reset(),
line: Line::default(),
blocks: Vec::new(),
qr_data: None,
qr_module: 3,
qr_level: QrErrorCorrection::L,
barcode_height: 162,
barcode_module: 3,
hri: HriPosition::None,
};
for node in nodes {
l.node(node);
}
l.flush_if_used();
l.blocks
}
impl Layout {
fn columns(&self, font: Font) -> u32 {
match font {
Font::A => self.columns_font_a,
Font::B | Font::C => self.columns_font_b,
}
}
fn node(&mut self, node: &Node) {
match node {
Node::Init => {
self.line = Line::default();
self.align = Align::Left;
self.style = Style::reset();
self.qr_data = None;
self.qr_module = 3;
self.qr_level = QrErrorCorrection::L;
self.barcode_height = 162;
self.barcode_module = 3;
self.hri = HriPosition::None;
}
Node::Text { text } => {
for ch in text.chars() {
self.push_char(ch);
}
}
Node::LineFeed => self.flush(),
Node::CarriageReturn | Node::CodePage { .. } | Node::Status { .. } => {}
Node::Pulse { .. } | Node::QrModel { .. } => {}
Node::Tab => {
let font = self.style.font;
let per_column = self.column_weight(font);
let next = (self.line.columns / (8 * per_column) + 1) * 8 * per_column;
while self.line.columns < next && self.line.columns < self.width_units() {
self.push_char(' ');
}
}
Node::Feed { lines } => {
self.flush_if_used();
for _ in 0..*lines {
self.flush();
}
}
Node::Align { align } => {
self.align = *align;
if self.line.is_empty() {
self.line.align = *align;
}
}
Node::Bold { on } => self.style.bold = *on,
Node::Underline { dots } => self.style.underline = *dots,
Node::Size { width, height } => {
self.style.width = *width;
self.style.height = *height;
}
Node::PrintMode {
font,
bold,
double_height,
double_width,
underline,
} => {
self.style.font = *font;
self.style.bold = *bold;
self.style.height = if *double_height { 2 } else { 1 };
self.style.width = if *double_width { 2 } else { 1 };
self.style.underline = u8::from(*underline);
}
Node::Font { font } => self.style.font = *font,
Node::Cut { partial, .. } => {
self.flush_if_used();
self.blocks.push(Block::Cut { partial: *partial });
}
Node::QrModuleSize { dots } => self.qr_module = (*dots).clamp(1, 16),
Node::QrErrorCorrection { level } => self.qr_level = *level,
Node::QrStore { text, data } => self.qr_data = Some((data.clone(), text.clone())),
Node::QrPrint => {
self.flush_if_used();
match self.qr_data.clone() {
Some((data, text)) => self.blocks.push(Block::Qr {
align: self.align,
data,
text,
module_dots: self.qr_module,
level: self.qr_level,
}),
None => self.unknown("QR", "QR print without stored data"),
}
}
Node::BarcodeHeight { dots } => self.barcode_height = (*dots).max(1),
Node::BarcodeWidth { dots } => self.barcode_module = (*dots).clamp(1, 6),
Node::HriPosition { position } => self.hri = *position,
Node::Barcode { symbology, data } => {
self.flush_if_used();
self.blocks.push(Block::Barcode {
align: self.align,
symbology: *symbology,
data: data.clone(),
height_dots: self.barcode_height,
module_dots: self.barcode_module,
hri: self.hri,
});
}
Node::RasterImage {
width_bytes,
height,
double_width,
double_height,
data,
} => {
self.flush_if_used();
self.blocks.push(Block::Image {
align: self.align,
width_bytes: *width_bytes,
height: *height,
double_width: *double_width,
double_height: *double_height,
data: data.clone(),
});
}
Node::Unknown {
bytes,
command,
warning,
} => {
let label = command.clone().unwrap_or_else(|| "?".into());
let hex: Vec<String> = bytes.iter().take(16).map(|b| format!("{b:02x}")).collect();
let title = format!("{warning} ({})", hex.join(" "));
self.unknown(&label, &title);
}
}
}
fn column_weight(&self, font: Font) -> u32 {
self.width_units() / self.columns(font)
}
fn width_units(&self) -> u32 {
self.columns_font_a * self.columns_font_b
}
fn push_char(&mut self, ch: char) {
let style = self.style;
let units = self.column_weight(style.font) * u32::from(style.width.max(1));
if self.line.columns + units > self.width_units() && !self.line.is_empty() {
self.flush();
}
if self.line.is_empty() {
self.line.align = self.align;
}
self.line.columns += units;
match self.line.segments.last_mut() {
Some(Segment::Text { text, style: s }) if *s == style => text.push(ch),
_ => self.line.segments.push(Segment::Text {
text: ch.to_string(),
style,
}),
}
}
fn unknown(&mut self, label: &str, title: &str) {
if self.show_unknown {
if self.line.is_empty() {
self.line.align = self.align;
}
self.line.segments.push(Segment::Unknown {
label: label.to_owned(),
title: title.to_owned(),
});
}
}
fn flush(&mut self) {
let mut line = std::mem::take(&mut self.line);
if line.is_empty() {
line.align = self.align;
}
self.blocks.push(Block::Line(line));
self.line.align = self.align;
}
fn flush_if_used(&mut self) {
if !self.line.is_empty() {
self.flush();
}
}
}