escpos_md/
printer.rs

1use crate::command::{
2    CharMagnification, Charset, CodeTable, Command, Font, Justification, UnderlineThickness,
3};
4use crate::config::PrinterConfig;
5use crate::error::{Error, Result};
6use crate::instruction::EscposImage;
7use crate::split_words::split_words;
8use codepage_437::{IntoCp437, CP437_CONTROL};
9use std::io;
10
11pub trait PrinterDevice {
12    fn write_all(&mut self, buf: &[u8]) -> io::Result<()>;
13}
14
15impl<T> PrinterDevice for T
16where
17    T: io::Write,
18{
19    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
20        self.write_all(buf)
21    }
22}
23
24#[derive(Clone, Debug)]
25pub struct PrinterState {
26    pub(crate) char_spacing: u8,
27    pub(crate) line_spacing: Option<u8>,
28    pub(crate) font: Font,
29    pub(crate) left_offset: usize,
30    pub(crate) split_words: bool,
31    pub(crate) left_margin: u16,
32    pub(crate) justification: Justification,
33    pub(crate) char_magnification: CharMagnification,
34}
35
36#[derive(Clone, Debug)]
37pub struct Printer<D> {
38    pub(crate) device: D,
39    pub(crate) config: PrinterConfig,
40    pub(crate) state: PrinterState,
41}
42
43impl<D> Printer<D> {
44    pub fn builder() -> PrinterConfig {
45        PrinterConfig::default()
46    }
47
48    fn reduce_spacing_param(spacing: usize) -> Result<u8> {
49        if spacing > u8::MAX as usize {
50            Err(Error::InvalidSpacingParam)
51        } else {
52            Ok(spacing as u8)
53        }
54    }
55
56    pub fn new(device: D, config: PrinterConfig) -> Result<Self> {
57        let state = PrinterState {
58            char_spacing: Self::reduce_spacing_param(config.char_spacing)?,
59            line_spacing: None,
60            font: Font::default(),
61            left_offset: 0,
62            split_words: true,
63            left_margin: 0,
64            justification: Justification::default(),
65            char_magnification: CharMagnification::default(),
66        };
67        Ok(Printer {
68            device,
69            config,
70            state,
71        })
72    }
73
74    pub(crate) fn calc_char_size(&self) -> usize {
75        (self.config.font_widths.get(&self.state.font) + self.state.char_spacing as usize)
76            * self.state.char_magnification.width() as usize
77    }
78
79    pub(crate) fn printable_width(&self) -> usize {
80        self.config.width - (self.state.left_margin as usize).min(self.config.width)
81    }
82}
83
84impl PrinterConfig {
85    pub fn build<D>(&self, device: D) -> Result<Printer<D>> {
86        Printer::new(device, self.clone())
87    }
88}
89
90macro_rules! cmd_fn {
91    ($name:ident, $cmd:ident) => {
92        #[inline]
93        pub fn $name(&mut self) -> Result<&mut Self> {
94            self.command(&Command::$cmd)
95        }
96    };
97    ($name:ident, $cmd:ident, $param:ident, $param_ty:ty) => {
98        #[inline]
99        pub fn $name(&mut self, $param: $param_ty) -> Result<&mut Self> {
100            self.command(&Command::$cmd($param))
101        }
102    };
103}
104
105impl<D> Printer<D>
106where
107    D: PrinterDevice,
108{
109    cmd_fn!(cut, Cut);
110    cmd_fn!(init, Init);
111    cmd_fn!(print_mode_default, PrintModeDefault);
112    cmd_fn!(charset, Charset, charset, Charset);
113    cmd_fn!(code_table, CodeTable, code_table, CodeTable);
114    cmd_fn!(font, Font, font, Font);
115    cmd_fn!(underline, Underline, thickness, UnderlineThickness);
116    cmd_fn!(bold, Bold, enabled, bool);
117    cmd_fn!(double_strike, DoubleStrike, enabled, bool);
118    cmd_fn!(white_black_reverse, WhiteBlackReverse, enabled, bool);
119    cmd_fn!(char_size, CharSize, magnification, CharMagnification);
120    cmd_fn!(split_words, SplitWords, enabled, bool);
121    cmd_fn!(left_margin, LeftMargin, margin, u16);
122    cmd_fn!(justification, Justification, justification, Justification);
123
124    pub fn reset(&mut self) -> Result<&mut Self> {
125        self.state.split_words = true;
126        let og_char_spacing = self.config.char_spacing;
127        self.init()?
128            .print_mode_default()?
129            .white_black_reverse(false)?
130            .double_strike(false)?
131            .char_spacing(og_char_spacing)?
132            .line_spacing(None)?
133            .left_margin(0)?
134            .justification(Justification::default())
135    }
136
137    pub fn print(&mut self, text: impl ToString) -> Result<&mut Self> {
138        let mut content = text.to_string().into_cp437(&CP437_CONTROL)?;
139
140        let new_offset = if self.state.split_words {
141            split_words(
142                &mut content,
143                self.state.left_offset,
144                self.printable_width(),
145                self.calc_char_size(),
146            )
147        } else {
148            (self.state.left_offset + content.len() * self.calc_char_size())
149                % self.printable_width()
150        };
151
152        unsafe {
153            self.raw(content)?;
154        }
155        self.state.left_offset = new_offset;
156        Ok(self)
157    }
158
159    pub fn println(&mut self, text: impl ToString) -> Result<&mut Self> {
160        self.print(text.to_string() + "\n")
161    }
162
163    pub fn feed_lines(&mut self, lines: usize) -> Result<&mut Self> {
164        for _ in 0..lines / u8::MAX as usize {
165            self.command(&Command::FeedLines(u8::MAX))?;
166        }
167        self.command(&Command::FeedLines(lines as u8))
168    }
169
170    pub fn feed_paper(&mut self, units: usize) -> Result<&mut Self> {
171        for _ in 0..units / u8::MAX as usize {
172            self.command(&Command::FeedPaper(u8::MAX))?;
173        }
174        self.command(&Command::FeedPaper(units as u8))
175    }
176
177    pub fn char_spacing(&mut self, char_spacing: usize) -> Result<&mut Self> {
178        self.command(&Command::CharSpacing(Self::reduce_spacing_param(
179            char_spacing,
180        )?))
181    }
182
183    pub fn line_spacing(&mut self, line_spacing: Option<usize>) -> Result<&mut Self> {
184        let cmd = if let Some(line_spacing) = line_spacing {
185            Command::LineSpacing(Self::reduce_spacing_param(line_spacing)?)
186        } else {
187            Command::DefaultLineSpacing
188        };
189        self.command(&cmd)
190    }
191
192    pub fn command(&mut self, cmd: &Command) -> Result<&mut Self> {
193        unsafe {
194            self.raw(&cmd.as_bytes())?;
195        }
196        match cmd {
197            Command::LineSpacing(units) => self.state.line_spacing = Some(*units),
198            Command::DefaultLineSpacing => self.state.line_spacing = None,
199            Command::CharSpacing(units) => self.state.char_spacing = *units,
200            Command::CharSize(magnification) => self.state.char_magnification = *magnification,
201            Command::Font(font) => self.state.font = *font,
202            Command::SplitWords(split) => self.state.split_words = *split,
203            Command::LeftMargin(margin) => self.state.left_margin = *margin,
204            Command::Justification(justification) => self.state.justification = *justification,
205            Command::FeedPaper(_) | Command::FeedLines(_) => {
206                self.state.left_offset = 0;
207            }
208            Command::Init => {
209                self.state.char_magnification = CharMagnification::default();
210                self.state.font = Font::default();
211            }
212            _ => {} // do nothing
213        }
214        Ok(self)
215    }
216
217    pub fn image(&mut self, image: &EscposImage) -> Result<&mut Self> {
218        unsafe {
219            self.raw(image.as_bytes(
220                self.printable_width(),
221                self.state.justification,
222                self.state.line_spacing,
223            ))?;
224        }
225        self.state.left_offset = 0;
226        Ok(self)
227    }
228
229    pub unsafe fn raw(&mut self, data: impl AsRef<[u8]>) -> Result<&mut Self> {
230        self.device.write_all(data.as_ref())?;
231        Ok(self)
232    }
233}