Implemented: Queues, QR Codes, BARCodes
Implemented QR and barcode
This commit is contained in:
parent
410e203f9e
commit
280d2fcbab
2 changed files with 255 additions and 41 deletions
230
src/escpos.rs
230
src/escpos.rs
|
|
@ -1,46 +1,246 @@
|
||||||
use crate::dithering::atkinson_mono;
|
use crate::dithering::atkinson_mono;
|
||||||
use image::{DynamicImage, imageops::FilterType};
|
use image::{DynamicImage, imageops::FilterType};
|
||||||
|
use std::{collections::HashMap, io::Write, ops::AddAssign};
|
||||||
|
|
||||||
const MAX_WIDTH: u32 = 384;
|
const MAX_WIDTH: u32 = 384;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EscPosError {
|
||||||
|
EmptyQueue,
|
||||||
|
InvalidQueueIndex,
|
||||||
|
InvalidBitmapMode,
|
||||||
|
InvalidBarcodeLength(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum QREcc {
|
||||||
|
Low = 48,
|
||||||
|
Medium = 49,
|
||||||
|
Quartile = 50,
|
||||||
|
High = 51,
|
||||||
|
}
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum BARTextPosition {
|
||||||
|
Hidden = 0,
|
||||||
|
Above = 1,
|
||||||
|
Below = 2,
|
||||||
|
Both = 3,
|
||||||
|
}
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum BARType {
|
||||||
|
CODE128 = 0x49,
|
||||||
|
EAN13 = 0x43,
|
||||||
|
UPCA = 0x41,
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ImageOrientation {
|
pub enum ImageOrientation {
|
||||||
Preserve,
|
Preserve,
|
||||||
Largest,
|
Largest,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn escpos_raster(
|
pub struct Job {
|
||||||
|
content: Vec<u8>,
|
||||||
|
pub ready: bool,
|
||||||
|
}
|
||||||
|
pub struct Printer {
|
||||||
|
pub queue: Vec<Job>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_numeric(s: &[u8]) -> bool {
|
||||||
|
s.iter().all(|&b| b.is_ascii_digit())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Job {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Job {
|
||||||
|
content: vec![0x1B, b'@'],
|
||||||
|
ready: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_bitmap(
|
||||||
|
&mut self,
|
||||||
img: &DynamicImage,
|
img: &DynamicImage,
|
||||||
orientation: Option<ImageOrientation>,
|
orientation: Option<ImageOrientation>,
|
||||||
mode: Option<u8>,
|
mode: Option<u8>,
|
||||||
) -> Vec<u8> {
|
) -> Result<(), EscPosError> {
|
||||||
let mode = mode.unwrap_or(0);
|
let mode = mode.unwrap_or(0);
|
||||||
assert!(mode <= 3 || (48..=51).contains(&mode));
|
if !(mode <= 3 || (48..=51).contains(&mode)) {
|
||||||
|
return Err(EscPosError::InvalidBitmapMode);
|
||||||
|
}
|
||||||
|
|
||||||
let (w, h) = (img.width(), img.height());
|
let (w, h) = (img.width(), img.height());
|
||||||
let swap = matches!(orientation, Some(ImageOrientation::Largest))
|
let swap = matches!(orientation, Some(ImageOrientation::Largest))
|
||||||
&& h.min(MAX_WIDTH) * w > w.min(MAX_WIDTH) * h;
|
&& h.min(MAX_WIDTH) * w > w.min(MAX_WIDTH) * h;
|
||||||
|
|
||||||
let (w, h) = if swap { (h, w) } else { (w, h) };
|
let scale = if swap {
|
||||||
let (w, h) = (MAX_WIDTH, h.saturating_mul(MAX_WIDTH).div_ceil(w));
|
MAX_WIDTH as f32 / h as f32
|
||||||
|
} else {
|
||||||
|
MAX_WIDTH as f32 / w as f32
|
||||||
|
};
|
||||||
|
|
||||||
let img = if swap { &img.rotate90() } else { &img };
|
let (w, h) = if swap {
|
||||||
let img = img.resize(w, h, FilterType::Triangle);
|
((h as f32 * scale) as u32, MAX_WIDTH)
|
||||||
|
} else {
|
||||||
|
(MAX_WIDTH, (h as f32 * scale) as u32)
|
||||||
|
};
|
||||||
|
|
||||||
|
let img =
|
||||||
|
DynamicImage::ImageRgba8(image::imageops::resize(img, w, h, FilterType::Triangle));
|
||||||
|
|
||||||
|
let img = if swap { img.rotate90() } else { img };
|
||||||
let mono = atkinson_mono(&img);
|
let mono = atkinson_mono(&img);
|
||||||
let (width_px, height_px) = (img.width() as u16, img.height() as u16);
|
let width_bytes = ((w + 7) >> 3) as u16;
|
||||||
let width_bytes = ((width_px + 7) >> 3) as u16;
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(8 + mono.len());
|
let buf = &mut self.content;
|
||||||
out.extend_from_slice(&[
|
buf.reserve(8 + mono.len());
|
||||||
|
buf.extend_from_slice(&[
|
||||||
0x1D,
|
0x1D,
|
||||||
0x76,
|
0x76,
|
||||||
0x30,
|
0x30,
|
||||||
mode,
|
mode,
|
||||||
width_bytes as u8,
|
width_bytes as u8,
|
||||||
(width_bytes >> 8) as u8,
|
(width_bytes >> 8) as u8,
|
||||||
height_px as u8,
|
h as u8,
|
||||||
(height_px >> 8) as u8,
|
(h >> 8) as u8,
|
||||||
]);
|
]);
|
||||||
out.extend_from_slice(&mono);
|
buf.extend_from_slice(&mono);
|
||||||
out
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn write_qr(
|
||||||
|
&mut self,
|
||||||
|
text: String,
|
||||||
|
size: Option<u8>,
|
||||||
|
ecc: Option<QREcc>,
|
||||||
|
) -> Result<(), EscPosError> {
|
||||||
|
let buf = &mut self.content;
|
||||||
|
let ecc = ecc.unwrap_or(QREcc::Medium);
|
||||||
|
let size = match size {
|
||||||
|
Some(v) => v.clamp(6, 15),
|
||||||
|
None => 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = text.as_bytes();
|
||||||
|
let len = text.len() + 3;
|
||||||
|
buf.extend_from_slice(&[
|
||||||
|
// Size
|
||||||
|
0x1D,
|
||||||
|
0x28,
|
||||||
|
0x6B,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x31,
|
||||||
|
0x43,
|
||||||
|
size,
|
||||||
|
// Error correction level
|
||||||
|
0x1D,
|
||||||
|
0x28,
|
||||||
|
0x6B,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x31,
|
||||||
|
0x45,
|
||||||
|
ecc as u8,
|
||||||
|
// Storing data...
|
||||||
|
0x1D,
|
||||||
|
0x28,
|
||||||
|
0x6B,
|
||||||
|
(len & 0xFF) as u8,
|
||||||
|
((len >> 8) & 0xFF) as u8,
|
||||||
|
0x31,
|
||||||
|
0x50,
|
||||||
|
0x30,
|
||||||
|
]);
|
||||||
|
buf.extend(text);
|
||||||
|
buf.extend(&[0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30, 0x0A]); // Print!
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
pub fn write_barcode(
|
||||||
|
&mut self,
|
||||||
|
text: String,
|
||||||
|
height: Option<u8>,
|
||||||
|
mod_width: Option<u8>,
|
||||||
|
text_position: Option<BARTextPosition>,
|
||||||
|
bar_type: Option<BARType>,
|
||||||
|
) -> Result<(), EscPosError> {
|
||||||
|
let height: u8 = height.unwrap_or(80);
|
||||||
|
let mod_width: u8 = mod_width.unwrap_or(2);
|
||||||
|
let text_position = text_position.unwrap_or(BARTextPosition::Below) as u8;
|
||||||
|
let bar_type = bar_type.unwrap_or(BARType::CODE128);
|
||||||
|
|
||||||
|
let text = text.as_bytes();
|
||||||
|
let len = text.len() as u8;
|
||||||
|
let is_num = is_numeric(text);
|
||||||
|
match bar_type {
|
||||||
|
BARType::UPCA if len != 11 || !is_num => Err(EscPosError::InvalidBarcodeLength(
|
||||||
|
"UCPA Requires 11 numbers.".to_string(),
|
||||||
|
)),
|
||||||
|
|
||||||
|
BARType::EAN13 if len != 12 || !is_num => Err(EscPosError::InvalidBarcodeLength(
|
||||||
|
"EAN13 Requires 12 numbers.".to_string(),
|
||||||
|
)),
|
||||||
|
_ => Ok(()),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let buf = &mut self.content;
|
||||||
|
buf.extend([
|
||||||
|
0x1D,
|
||||||
|
0x68,
|
||||||
|
height,
|
||||||
|
0x1D,
|
||||||
|
0x77,
|
||||||
|
mod_width,
|
||||||
|
0x1d,
|
||||||
|
0x48,
|
||||||
|
text_position,
|
||||||
|
0x1D,
|
||||||
|
0x6B,
|
||||||
|
bar_type as u8,
|
||||||
|
len,
|
||||||
|
]);
|
||||||
|
buf.extend(text);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Printer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Printer { queue: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_job(&mut self) -> Result<&mut Job, EscPosError> {
|
||||||
|
self.queue.push(Job::new());
|
||||||
|
self.queue.last_mut().ok_or(EscPosError::InvalidQueueIndex)
|
||||||
|
}
|
||||||
|
pub fn print_job(&mut self, writer: &mut impl Write) -> Result<(), EscPosError> {
|
||||||
|
let page_feed: u8 = 0x0A;
|
||||||
|
let job = self
|
||||||
|
.queue
|
||||||
|
.extract_if(.., |j| j.ready)
|
||||||
|
.next()
|
||||||
|
.ok_or(EscPosError::EmptyQueue)?;
|
||||||
|
|
||||||
|
// writer.write(&[page_feed]).unwrap(); // FIXME: remove unwraps
|
||||||
|
writer.write(&job.content).unwrap();
|
||||||
|
// writer.write(&[page_feed]).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn export_job(&mut self) -> Result<Vec<u8>, EscPosError> {
|
||||||
|
let page_feed: u8 = 0x0A;
|
||||||
|
let job = self
|
||||||
|
.queue
|
||||||
|
.extract_if(.., |j| j.ready)
|
||||||
|
.next()
|
||||||
|
.ok_or(EscPosError::EmptyQueue)?;
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(2 + job.content.len());
|
||||||
|
out.push(page_feed);
|
||||||
|
out.extend(job.content);
|
||||||
|
out.push(page_feed);
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
src/main.rs
26
src/main.rs
|
|
@ -2,7 +2,9 @@ mod dithering;
|
||||||
mod escpos;
|
mod escpos;
|
||||||
|
|
||||||
use image::{ImageError, ImageReader};
|
use image::{ImageError, ImageReader};
|
||||||
use std::{env, fs, io, io::Write, process};
|
use std::{env, process};
|
||||||
|
|
||||||
|
use crate::escpos::Printer;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
@ -18,10 +20,22 @@ fn main() {
|
||||||
.and_then(|v| v.decode())
|
.and_then(|v| v.decode())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut escpos = escpos::escpos_raster(&img, Some(escpos::ImageOrientation::Largest), Some(0));
|
let mut printer = Printer::new();
|
||||||
for _ in 0..2 {
|
let job = printer.new_job().unwrap();
|
||||||
escpos.push('\n' as u8);
|
// job.write_qr("hi".to_string(), None, None).unwrap();
|
||||||
}
|
job.write_barcode(
|
||||||
|
"hhhhhhhhhhh".to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(escpos::BARType::UPCA),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// job.write_bitmap(&img, None, None).unwrap();
|
||||||
|
job.ready = true;
|
||||||
|
|
||||||
io::stdout().write(&escpos).unwrap();
|
println!("{}", printer.queue.len());
|
||||||
|
|
||||||
|
let mut out = std::fs::File::create("/dev/usb/lp0").unwrap();
|
||||||
|
printer.print_job(&mut out).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue