Functional xml parser and sed integration

This commit is contained in:
Jurn Wubben 2026-01-24 20:26:42 +01:00
parent a9f193ecc7
commit 3e6ce77549
4 changed files with 1553 additions and 63 deletions

1035
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,4 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
nom = "8.0.0"
image = "0.25.9"
quick-xml = { version = "0.39.0", features = ["serialize"] }
serde = { version = "1.0.228", features = ["derive"] }
# nom = "8.0.0"

305
src/escpos.rs Normal file
View file

@ -0,0 +1,305 @@
use std::{error::Error, fmt::Display};
use image::{
DynamicImage, GrayImage,
imageops::{self, FilterType, colorops},
};
use serde::Deserialize;
// TYPES
#[derive(Debug)]
pub enum EscPosError {
InvalidBarcodeLength(String),
}
#[repr(u8)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum QREcc {
Low = 48,
Medium = 49,
Quartile = 50,
High = 51,
}
#[repr(u8)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BARTextPosition {
Hidden = 0,
Above = 1,
Below = 2,
Both = 3,
}
#[repr(u8)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BARType {
Code128 = 0x49,
Ean13 = 0x43,
Upca = 0x41,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageOrientation {
Preserve,
Largest,
LargestOrientation,
}
#[repr(u8)]
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum JustifyOrientation {
#[default]
Left = 0,
Center = 1,
Right = 2,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TextEffect {
Bold,
DoubleHeight,
DoubleWidth,
// InvertColor,
Justify(JustifyOrientation),
}
pub struct EscPosBuilder {
pub content: Vec<u8>,
max_width: u16,
}
fn is_numeric(s: &[u8]) -> bool {
s.iter().all(|&b| b.is_ascii_digit())
}
fn bitmap_auto_brighten(gray: &mut GrayImage) {
let avg = gray.pixels().map(|p| p.0[0] as u32).sum::<u32>() / gray.len() as u32;
if avg < 100 {
colorops::brighten_in_place(gray, 40);
colorops::contrast_in_place(gray, 25.0);
} else if avg > 180 {
colorops::brighten_in_place(gray, -15);
}
}
fn bitmap_pack_bits(w: usize, h: usize, gray: &GrayImage) -> Vec<u8> {
let pitch = (w + 7) >> 3; // pixels per row
let mut bits = vec![0u8; pitch * h];
for y in 0..h {
for x in 0..w {
let byte = y * pitch + (x >> 3); // get index of byte that contains bit for the current pixel
let bit = 7 - (x & 7); // get bit position in byte
if gray[(x as u32, y as u32)].0[0] == 0 {
// if the pixel is black, set it to one
bits[byte] |= 1 << bit;
}
}
}
bits
}
impl Copy for JustifyOrientation {}
impl Clone for JustifyOrientation {
fn clone(&self) -> Self {
*self
}
}
impl Error for EscPosError {}
impl Display for EscPosError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
todo!()
}
}
impl EscPosBuilder {
fn extend(&mut self, slice: &[u8]) {
self.content.extend([0x1B, b'@']);
self.content.extend(slice);
}
pub fn new(max_width: u16) -> Self {
EscPosBuilder {
content: vec![0x1B, b'@'],
max_width,
}
}
pub fn write_feed(&mut self, amount: Option<u8>) {
let amount = amount.unwrap_or(1);
self.extend(&[0x1B, b'd', amount])
}
pub fn write_justify(&mut self, justify: JustifyOrientation) {
self.extend(&[0x1B, b'a', justify as u8]);
}
pub fn write_text(&mut self, text: &str, text_effect: Option<&[TextEffect]>) -> () {
let mut buf: Vec<u8> = vec![];
buf.reserve(12 + text.len());
for i in text_effect.unwrap_or(&[]) {
match i {
TextEffect::Bold => buf.extend(&[0x1B, b'!', 8]),
TextEffect::DoubleWidth => buf.extend(&[0x1B, b'!', 0x10]),
TextEffect::DoubleHeight => buf.extend(&[0x1B, b'!', 0x20]),
TextEffect::Justify(orientation) => buf.extend(&[0x1B, b'a', *orientation as u8]),
}
}
buf.extend(text.as_bytes());
self.extend(&buf);
}
pub fn write_bitmap(
&mut self,
img: &DynamicImage,
orientation: Option<ImageOrientation>,
) -> () {
let orientation = orientation.unwrap_or(ImageOrientation::Preserve);
let mw = self.max_width as u32;
let mut image = img.clone();
if matches!(orientation, ImageOrientation::LargestOrientation)
&& image.width() > image.height()
{
image = image.rotate90();
}
let iw = image.width();
let ih = image.height();
let w = if iw > mw || !matches!(orientation, ImageOrientation::Preserve) {
mw
} else {
iw
};
let h = (ih * w) / iw;
let img = image.resize(w, h, FilterType::Triangle);
let mut gray = img.to_luma8();
imageops::dither(&mut gray, &imageops::colorops::BiLevel);
bitmap_auto_brighten(&mut gray);
let w = img.width();
let h = img.height();
let bits = bitmap_pack_bits(w as usize, h as usize, &gray);
let mut buf: Vec<u8> = vec![];
let width_bytes = ((w + 7) >> 3) as u16;
let height_u16 = h as u16;
buf.extend(&[
0x1D,
b'v',
b'0',
0,
(width_bytes & 0xFF) as u8,
((width_bytes >> 8) & 0xFF) as u8,
(height_u16 & 0xFF) as u8,
((height_u16 >> 8) & 0xFF) as u8,
]);
buf.extend(bits);
self.extend(&buf);
}
pub fn write_qr(&mut self, text: &str, size: Option<u8>, ecc: Option<QREcc>) -> () {
let mut buf: Vec<u8> = vec![];
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(&[
// 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!
self.extend(&buf);
}
pub fn write_barcode(
&mut self,
text: &str,
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 mut buf: Vec<u8> = vec![];
buf.extend([
0x1D,
0x68,
height,
0x1D,
0x77,
mod_width,
0x1d,
0x48,
text_position,
0x1D,
0x6B,
bar_type as u8,
len,
]);
buf.extend(text);
self.extend(&buf);
Ok(())
}
}

View file

@ -1,70 +1,218 @@
use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, take_until},
character::complete::{line_ending, not_line_ending},
combinator::{map, success},
error::Error,
multi::fold,
sequence::{delimited, preceded},
};
use std::vec::Vec;
use quick_xml::de::from_str;
use serde::de::IntoDeserializer;
use serde::{Deserialize, de};
use std::io::{Read, Write, stdout};
use std::{error::Error, io::stdin};
enum Fragment<'a> {
Literal(&'a str),
Header(&'a str),
mod escpos;
use escpos::{BARTextPosition, BARType, EscPosBuilder, ImageOrientation, QREcc, TextEffect};
use crate::escpos::JustifyOrientation;
#[derive(Debug, Deserialize)]
struct Document {
#[serde(rename = "$value")]
pub children: Vec<Node>,
}
fn main() {
let input = concat!(
// "Hallo daar!\n",
// "Hoe gaat het?\n",
"# Met mij gaat het goed\n",
"Met jou\n",
"## Ja ook goed\n",
// "Echt?\n",
// "### Wow (not matched because we only accept 1 or 2 '#')\n",
// "#StartOfFileHeaderNoSpace <-- not matched because we require a space after '#'\n",
// " ## Indented header\n"
);
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Node {
#[serde(rename = "$text")]
PlainText(String),
let vec = Vec::<u8>::new();
use nom::bytes::complete::tag;
use nom::sequence::preceded;
#[serde(rename = "h")]
Heading(Heading),
println!("{:?}", parse_fragment(input))
#[serde(rename = "p")]
Text(Text),
#[serde(rename = "justify")]
Justify(Justify),
#[serde(rename = "img")]
Img(Img),
#[serde(rename = "qr")]
Qr(Qr),
#[serde(rename = "bar")]
Barcode(Barcode),
#[serde(rename = "br")]
Feed(Feed),
}
fn parse_header<'a>(input: &'a str) -> IResult<&'a str, &str, ()> {
delimited(tag::<&str, &str, ()>("# "), not_line_ending, line_ending).parse(input)
}
fn parse_fragment<'a>(input: &'a str) -> IResult<&str, Fragment, ()> {
alt((
map(parse_header, Fragment::Header),
map(not_line_ending, Fragment::Literal),
))
.parse(input)
}
fn parse_string<'a>(input: &'a str) -> IResult<&'a str, Vec<Fragment<'a>>, ()> {
fold(
0..,
parse_fragment,
Vec::<u8>::new(),
|mut vector, fragment| vector,
)
#[derive(Debug, Deserialize)]
struct Justify {
#[serde(rename = "@align")]
pub align: JustifyOrientation,
#[serde(rename = "$value")]
pub value: Vec<Node>,
}
// fn parse_fragment<'a, E>(input: &'a str) -> IResult<&'a str, StringFragment<'a>, E>
// where
// E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
// {
// alt((map(parse_header, Fragment::Header)))
// }
//
// fn parse_header<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
// preceded(
// permutation((char('\n'), many_m_n(1, 2, char('#')))),
// take_until("\n"),
// )
// .parse(input)
// }
#[derive(Debug, Deserialize)]
struct Heading {
#[serde(rename = "$value")]
pub value: String,
}
#[derive(Debug, Deserialize)]
struct Text {
#[serde(rename = "@effects")]
#[serde(deserialize_with = "deserialize_effects")]
#[serde(default)]
pub style: Option<Vec<TextEffect>>,
#[serde(rename = "$value")]
pub value: String,
}
#[derive(Debug, Deserialize)]
struct Img {
#[serde(rename = "@src")]
pub src: String,
#[serde(rename = "@orientation")]
#[serde(default)]
pub orientation: Option<ImageOrientation>,
}
#[derive(Debug, Deserialize)]
struct Qr {
#[serde(rename = "@size")]
#[serde(default)]
pub size: Option<u8>,
#[serde(rename = "@ecc")]
#[serde(default)]
pub ecc: Option<QREcc>,
#[serde(rename = "$value")]
pub value: String,
}
#[derive(Debug, Deserialize)]
struct Barcode {
#[serde(rename = "@height")]
#[serde(default)]
pub height: Option<u8>,
#[serde(rename = "@mod_width")]
#[serde(default)]
pub mod_width: Option<u8>,
#[serde(rename = "@text_position")]
#[serde(default)]
pub text_position: Option<BARTextPosition>,
#[serde(rename = "@bar_type")]
#[serde(default)]
pub bar_type: Option<BARType>,
#[serde(rename = "$value")]
pub value: String,
}
#[derive(Debug, Deserialize)]
struct Feed {
#[serde(rename = "@amount")]
#[serde(default)]
pub amount: Option<u8>,
}
fn deserialize_effects<'de, D>(deserializer: D) -> Result<Option<Vec<TextEffect>>, D::Error>
where
D: de::Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
let mut out: Vec<TextEffect> = Vec::new();
if let Some(s) = opt {
for raw in s.split(';') {
let tok = raw.trim();
if tok.is_empty() {
continue;
}
let deserialized = TextEffect::deserialize(tok.into_deserializer())?;
out.push(deserialized);
}
}
Ok(Some(out))
}
fn handle_node(node: Node, builder: &mut EscPosBuilder) -> Result<(), Box<dyn Error>> {
match node {
Node::PlainText(ref t) => {
builder.write_text(t.trim(), None);
builder.write_feed(None);
}
Node::Heading(t) => {
builder.write_text(
&t.value,
Some(&[TextEffect::DoubleHeight, TextEffect::DoubleWidth]),
);
builder.write_feed(None);
}
Node::Text(t) => {
if let Some(ref list) = t.style {
builder.write_text(&t.value, Some(list));
} else {
builder.write_text(&t.value, None);
}
builder.write_feed(None);
}
Node::Justify(t) => {
builder.write_justify(t.align);
for i in t.value {
handle_node(i, builder)?;
}
builder.write_justify(JustifyOrientation::default());
}
Node::Img(img) => {
let dynimg = image::open(&img.src)?;
builder.write_bitmap(&dynimg, img.orientation);
}
Node::Qr(q) => {
let value = q.value;
let value = value.trim();
if value.is_empty() {
return Ok(());
};
builder.write_qr(&value, q.size, q.ecc);
}
Node::Barcode(b) => {
let value = b.value;
let value = value.trim();
if value.is_empty() {
return Ok(());
};
builder.write_barcode(&value, b.height, b.mod_width, b.text_position, b.bar_type)?;
}
Node::Feed(f) => {
builder.write_feed(f.amount);
}
}
Ok(())
}
fn handle_document(doc: Document, builder: &mut EscPosBuilder) -> Result<(), Box<dyn Error>> {
for node in doc.children {
handle_node(node, builder)?;
}
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let mut xml: String = String::new();
stdin().read_to_string(&mut xml)?;
let doc: Document = from_str::<Document>(&xml)?;
let mut builder = EscPosBuilder::new(384);
handle_document(doc, &mut builder)?;
stdout().write(&builder.content)?;
Ok(())
}