escpos_rust/
instruction.rs

1extern crate serde;
2extern crate codepage_437;
3extern crate image;
4extern crate qrcode;
5
6pub use self::print_data::{PrintData, PrintDataBuilder};
7pub use self::justification::{Justification};
8pub use self::escpos_image::EscposImage;
9
10mod print_data;
11mod justification;
12mod escpos_image;
13
14use qrcode::QrCode;
15use codepage_437::{IntoCp437, CP437_CONTROL};
16use crate::{
17    Error, PrinterProfile,
18    command::{Command, Font}
19};
20use serde::{Serialize, Deserialize};
21use std::collections::HashSet;
22
23/// Templates for recurrent prints
24///
25/// The [Instruction](crate::Instruction) structure allows the creation of template prints, which could contain certain data that should change between prints (be it text, tables, or even qr codes).
26///
27/// It is not adviced to construct the variants of the enum manually, read the available functions to guarantee a predictable outcome.
28#[derive(Serialize, Deserialize, Clone, Debug)]
29#[serde(tag = "kind")]
30pub enum Instruction {
31    /// Compound instruction, composed of multiple instructions that must be executed sequentially
32    Compound {
33        instructions: Vec<Instruction>
34    },
35    /// An instruction consisting of a single `esc/pos` command
36    Command {
37        command: Command
38    },
39    /// Short for jumping a specified number of lines
40    VSpace {
41        lines: u8
42    },
43    /// Raw text
44    Text {
45        /// Content to be printed
46        content: String,
47        /// Indicates if markdown translation should be applied
48        markdown: bool,
49        /// Font to be used with this text
50        font: Font,
51        /// Justification of the content
52        justification: Justification,
53        /// Maps a string to be replaced, to a description of the string
54        replacements: Option<HashSet<String>>
55    },
56    /// 2 column table
57    DuoTable {
58        /// Name of the table. Required for attaching tuples for printing
59        name: String,
60        /// Header to be displayed on the table
61        header: (String, String),
62        /// Font used for the table
63        font: Font
64    },
65    /// Table with three columns. Might be to tight for 50mm printers
66    TrioTable {
67        name: String,
68        header: (String, String, String)
69    },
70    /// Fancy table for really detailed prints
71    QuadTable {
72        name: String,
73        header: (String, String, String)
74    },
75    /// Contains a static image, that is, does not change with different printing mechanisms
76    Image {
77        /// Inner image
78        image: EscposImage
79    },
80    /// Prints a QR Code. This field is dynamic
81    QRCode {
82        /// Name of the QR code, to be searched in the qr code content list
83        name: String
84    },
85    /// Cuts the paper in place. Only for supported printers
86    Cut
87}
88
89/// Instruction addition
90impl std::ops::Add<Instruction> for Instruction {
91    type Output = Instruction;
92    fn add(self, rhs: Instruction) -> Self::Output {
93        match self {
94            Instruction::Compound{mut instructions} => {
95                match rhs {
96                    Instruction::Compound{instructions: mut rhs_instructions} => {
97                        instructions.append(&mut rhs_instructions);
98                    },
99                    rhs => {
100                        instructions.push(rhs);
101                    }
102                }
103                Instruction::Compound{instructions}
104            },
105            lhs => {
106                let mut instructions = vec![lhs];
107                match rhs {
108                    Instruction::Compound{instructions: mut rhs_instructions} => {
109                        instructions.append(&mut rhs_instructions);
110                    },
111                    rhs => {
112                        instructions.push(rhs);
113                    }
114                }
115                Instruction::Compound{instructions}
116            }
117        }
118    }
119}
120
121/// From iterator operation for a vector of instructions
122impl std::iter::FromIterator<Instruction> for Option<Instruction> {
123    fn from_iter<I: IntoIterator<Item=Instruction>>(iter: I) -> Self {
124        let mut r = None;
125
126        for elem in iter {
127            if let Some(rd) = &mut r {
128                *rd += elem;
129            } else {
130                r = Some(elem);
131            }
132        }
133
134        r
135    }
136}
137
138/// Mutable addition for instructions
139impl std::ops::AddAssign for Instruction {
140    fn add_assign(&mut self, other: Self) {
141        // Now we deal with this thing
142        if !self.is_compound() {
143            // It was not a compound element, so we make it such
144            *self = Instruction::Compound{instructions: vec![self.clone()]};
145        }
146
147        match self {
148            Instruction::Compound{instructions} => {
149                match other {
150                    Instruction::Compound{instructions: mut other_instructions} => {
151                        instructions.append(&mut other_instructions);
152                    },
153                    other => {
154                        instructions.push(other);
155                    }
156                }
157            },
158            _ => panic!("Impossible error")
159        }
160    }
161}
162
163impl Instruction {
164    /// Returns true if the instruction is compund
165    pub fn is_compound(&self) -> bool {
166        matches!(self, Instruction::Compound{..})
167    }
168
169    /// Returns true if the instruction is text
170    pub fn is_text(&self) -> bool {
171        matches!(self, Instruction::Text{..})
172    }
173
174    /// Sends simple text to the printer.
175    ///
176    /// Straightfoward text printing. The `replacements` set specifies which contents of the string should be replaced in a per-impresion basis.
177    pub fn text<A: Into<String>>(content: A, font: Font, justification: Justification, replacements: Option<HashSet<String>>) -> Instruction {
178        Instruction::Text {
179            content: content.into(),
180            markdown: false,
181            font,
182            justification,
183            replacements
184        }
185    }
186
187    /// Sends markdown text to the printer
188    ///
189    /// Allows markdown to be sent to the printer. Not everything is supported, so far the following list works (if the printer supports the corresponding fonts)
190    ///  * Bold font, with **
191    ///  * Italics, with _
192    pub fn markdown(content: String, font: Font, justification: Justification, replacements: Option<HashSet<String>>) -> Instruction {
193        Instruction::Text {
194            content,
195            markdown: true,
196            font,
197            justification,
198            replacements
199        }
200    }
201
202    /// Prints an image
203    ///
204    /// For a more precise control of position in the image, it is easier to edit the input image beforehand.
205    pub fn image(image: EscposImage) -> Result<Instruction, Error> {
206        Ok(Instruction::Image {
207            image
208        })
209    }
210
211    /// Creates a new QR code that does not change through different print steps
212    pub fn qr_code(content: String) -> Result<Instruction, Error> {
213        let code = QrCode::new(content.as_bytes()).unwrap();
214        // Render the bits into an image.
215        let img = code.render::<image::Rgba<u8>>().build();
216
217        let escpos_image = EscposImage::new(
218            image::DynamicImage::ImageRgba8(img),//.write_to(&mut content, image::ImageOutputFormat::Png).unwrap();
219            128,
220            Justification::Center
221        )?;
222        
223        Instruction::image(escpos_image)
224    }
225
226    /// Creates a dynamic qr code instruction, which requires a string at printing time
227    pub fn dynamic_qr_code<A: Into<String>>(name: A) -> Instruction {
228        Instruction::QRCode{name: name.into()}
229    }
230
231    /// Executes a raw escpos command.
232    pub fn command(command: Command) -> Instruction {
233        Instruction::Command {
234            command
235        }
236    }
237
238    /// Creates a table with two columns.
239    pub fn duo_table<A: Into<String>, B: Into<String>, C: Into<String>>(name: A, header: (B, C), font: Font) -> Instruction {
240        Instruction::DuoTable {
241            name: name.into(),
242            header: (header.0.into(), header.1.into()),
243            font
244        }
245    }
246
247    /// Creates a table with three columns
248    pub fn trio_table<A: Into<String>, B: Into<String>, C: Into<String>, D: Into<String>>(name: A, header: (B, C, D)) -> Instruction {
249        Instruction::TrioTable {
250            name: name.into(),
251            header: (header.0.into(), header.1.into(), header.2.into())
252        }
253    }
254
255    /// Creates a table with four columns
256    ///
257    /// Tables with four columns can be quite tight in 80mm printers, and unthinkable in 58mm ones or smaller. Use with caution!
258    pub fn quad_table<A: Into<String>, B: Into<String>, C: Into<String>, D: Into<String>>(name: A, header: (B, C, D)) -> Instruction {
259        Instruction::QuadTable {
260            name: name.into(),
261            header: (header.0.into(), header.1.into(), header.2.into())
262        }
263    }
264
265    /// Cuts the paper (if supported)
266    pub fn cut() -> Instruction {
267        Instruction::Cut
268    }
269
270    /// Moves the paper a certain amount of vertical spaces
271    pub fn vspace(lines: u8) -> Instruction {
272        Instruction::VSpace{lines}
273    }
274
275    /// Main serialization function
276    ///
277    /// This function turns the instruction structure into the sequence of bytes required to print the information, according to the ESCP/POS protocol. [PrintData](crate::PrintData) might be required if some of the information for printing is dynamic.
278    pub(crate) fn to_vec(&self, printer_profile: &PrinterProfile, print_data: Option<&PrintData>) -> Result<Vec<u8>, Error> {
279        let mut target = Vec::new();
280        match self {
281            Instruction::Compound{instructions} => {
282                for instruction in instructions {
283                    target.append(&mut instruction.to_vec(printer_profile, print_data)?);
284                }
285            },
286            Instruction::Cut => {
287                target.extend_from_slice(&Command::Cut.as_bytes());
288            },
289            Instruction::Command{command} => {
290                target.append(&mut command.as_bytes());
291            }
292            Instruction::VSpace{lines} => {
293                target.append(&mut vec![b'\n'; *lines as usize])
294            },
295            Instruction::Image{image} => {
296                target.extend_from_slice(&image.feed(printer_profile.width));
297            },
298            Instruction::QRCode{name} => {
299                let print_data = print_data.ok_or(Error::NoPrintData)?;
300                if let Some(qr_contents) = &print_data.qr_contents {
301                    if let Some(qr_content) = qr_contents.get(name) {
302                        target.extend_from_slice(&Instruction::qr_code(qr_content.clone())?.to_vec(printer_profile, Some(print_data))?)
303                    } else {
304                        return Err(Error::NoQrContent(name.clone()))
305                    }
306                } else {
307                    return Err(Error::NoQrContents)
308                }
309            },
310            // Text serialization for the printer
311            Instruction::Text{content, markdown, font, justification, replacements: self_replacements} => {
312                // We setup the font, mainly
313                target.append(&mut Command::SelectFont{font: font.clone()}.as_bytes());
314
315                // We extract the width for this font
316                let width = match printer_profile.columns_per_font.get(&font) {
317                    Some(w) => *w,
318                    None => return Err(Error::NoWidth)
319                };
320
321                let mut replaced_string = content.clone();
322                // First of all, we replace all the replacements
323                if let Some(self_replacements) = &self_replacements {
324                    if !self_replacements.is_empty() {
325                        let print_data = print_data.ok_or(Error::NoPrintData)?;
326
327                        for key in self_replacements.iter() {
328                            if let Some(replacement) = print_data.replacements.get(key) {
329                                replaced_string = replaced_string.as_str().replace(key, replacement);
330                            } else {
331                                return Err(Error::NoReplacementFound(key.clone()))
332                            }
333                        }
334                    }
335                }
336
337                // Now, we demarkdownize the string
338                let demarkdown_string = if *markdown {
339                    // We tokenize the string
340                    let mut _tmp = String::new();
341                    panic!("Not implemented the markdown thingy, is too hard!");
342                } else {
343                    replaced_string
344                };
345
346                // Now, we tokenize by spaces, using the width and justification
347                let mut result = Command::Reset.as_bytes();
348                // Line to control the text
349                let mut line = String::new();
350                let tokens = demarkdown_string.split_whitespace();
351                let mut width_count = 0;
352                
353                for token in tokens {
354                    if width_count + token.len() + 1 > (width as usize) {
355                        // We have to create a new line, this does not fit.
356                        width_count = token.len();
357                        // Now we actually format the line
358                        let mut tmp = match justification {
359                            Justification::Left => format!("{}\n", line),
360                            Justification::Right => format!("{:>1$}\n", line, width as usize),
361                            Justification::Center => format!("{:^1$}\n", line, width as usize)
362                        }.into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?;
363                        result.append(&mut tmp);
364
365                        // And we start the new line
366                        line = token.to_string();
367                    } else {
368                        width_count += token.len();
369                        if !line.is_empty() {
370                            width_count += 1;
371                            line += " ";
372                        }
373                        line += token;
374                    }
375                }
376
377                // Last, we deal with the last line
378                if !line.is_empty() {
379                    let mut tmp = match justification {
380                        Justification::Left => format!("{}\n", line),
381                        Justification::Right => format!("{:>1$}\n", line, width as usize),
382                        Justification::Center => format!("{:^1$}\n", line, width as usize)
383                    }.into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?;
384                    result.append(&mut tmp);
385                }
386                
387                target.append(&mut result);
388            },
389            Instruction::DuoTable{name, header, font} => {
390                // We extract the width for this font
391                let width = match printer_profile.columns_per_font.get(&font) {
392                    Some(w) => *w,
393                    None => return Err(Error::NoWidth)
394                };
395                //First, the headers
396                target.extend_from_slice(&format!("{}{:>2$}\n", header.0, header.1, (width as usize) - header.0.len()).into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
397
398                // Now, the line too
399                target.append(&mut vec![b'-'; width as usize]);
400                target.push(b'\n');
401                
402                // Now we actually look up the table
403                let print_data = print_data.ok_or(Error::NoPrintData)?;
404
405                if let Some(tables) = &print_data.duo_tables {
406                    if let Some(table) = tables.get(name) {
407                        for row in table {
408                            target.extend_from_slice(&format!("{}{:>2$}\n", row.0, row.1, (width as usize) - row.0.len()).into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?)
409                        }
410                    } else {
411                        return Err(Error::NoTableFound(name.clone()))
412                    }
413                } else {
414                    return Err(Error::NoTables)
415                }
416            },
417            Instruction::TrioTable{name, header} => {
418                // First, we will determine the proper alignment for the middle component
419                let print_data = print_data.ok_or(Error::NoPrintData)?;
420
421                let mut max_left: usize = header.0.len();
422                let mut max_middle: usize = header.1.len();
423                let mut max_right: usize = header.2.len();
424
425                if let Some(tables) = &print_data.trio_tables {
426                    if let Some(table) = tables.get(name) {
427                        for row in table {
428                            if row.0.len() > max_left {
429                                max_left = row.0.len();
430                            }
431                            if row.1.len() > max_middle {
432                                max_middle = row.1.len();
433                            }
434                            if row.2.len() > max_right {
435                                max_right = row.2.len();
436                            }
437                        }
438                    } else {
439                        return Err(Error::NoTableFound(name.clone()))
440                    }
441                } else {
442                    return Err(Error::NoTables)
443                }
444
445                // We chose a font
446                let width = match printer_profile.columns_per_font.get(&Font::FontA) {
447                    Some(w) => *w,
448                    None => return Err(Error::NoWidth)
449                } as usize;
450
451                let (max_left, max_right) = if max_left + max_middle + max_right + 2 <= width {
452                    // Todo va excelentemente bien.
453                    (max_left, max_right)
454                } else if max_middle + max_right + 2 <= width  && width - max_middle - max_right - 2 > 2 {
455                    // I am sorry, Mr. left side.
456                    (width - max_middle - max_right - 2, max_right)
457                } else {
458                    // Unluckily, we try to go for thirds
459                    let third = width / 3;
460                    if width % 3 == 0 {
461                        (third, third)
462                    } else if width % 3 == 1 {
463                        (third, third)
464                    } else {
465                        (third, third)
466                    }
467                };
468
469                // We go with the headers
470                target.extend_from_slice(
471                    &trio_row(header.clone(), width, max_left, max_right)
472                .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
473
474                // Now, the line too
475                target.append(&mut vec![b'-'; width]);
476                target.push(b'\n');
477                
478                // Now we actually look up the table
479                if let Some(tables) = &print_data.trio_tables {
480                    if let Some(table) = tables.get(name) {
481                        for row in table {
482                            target.extend_from_slice(
483                                &trio_row(row.clone(), width, max_left, max_right)
484                            .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
485                        }
486                    } else {
487                        return Err(Error::NoTableFound(name.clone()))
488                    }
489                } else {
490                    return Err(Error::NoTables)
491                }
492            },
493            Instruction::QuadTable{name, header} => {
494                // First, we will determine the proper alignment for the middle component
495                let print_data = print_data.ok_or(Error::NoPrintData)?;
496
497                let mut max_left: usize = header.0.len();
498                let mut max_middle: usize = header.1.len();
499                let mut max_right: usize = header.2.len();
500                if let Some(tables) = &print_data.quad_tables {
501                    if let Some(table) = tables.get(name) {
502                        for row in table {
503                            if row.1.len() > max_left {
504                                max_left = row.1.len();
505                            }
506                            if row.2.len() > max_middle {
507                                max_middle = row.2.len();
508                            }
509                            if row.3.len() > max_right {
510                                max_right = row.3.len();
511                            }
512                        }
513                    } else {
514                        return Err(Error::NoTableFound(name.clone()))
515                    }
516                } else {
517                    return Err(Error::NoTables)
518                }
519
520                // We chose a font
521                let width = match printer_profile.columns_per_font.get(&Font::FontA) {
522                    Some(w) => *w,
523                    None => return Err(Error::NoWidth)
524                } as usize;
525
526                let (max_left, max_right) = if max_left + max_middle + max_right + 2 <= width {
527                    // Todo va excelentemente bien.
528                    (max_left, max_right)
529                } else if max_middle + max_right + 2 <= width  && width - max_middle - max_right - 2 > 2 {
530                    // I am sorry, Mr. left side.
531                    (width - max_middle - max_right - 2, max_right)
532                } else {
533                    // Unluckily, we try to go for thirds
534                    let third = width / 3;
535                    // This spacing algorithm requires work
536                    if width % 3 == 0 {
537                        (third, third)
538                    } else if width % 3 == 1 {
539                        (third, third)
540                    } else {
541                        (third, third)
542                    }
543                };
544
545                // We go with the headers
546                target.extend_from_slice(
547                    &trio_row((header.0.clone(), header.1.clone(), header.2.clone()), width, max_left, max_right)
548                .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
549
550                // Now, the line too
551                target.append(&mut vec![b'-'; width]);
552                target.push(b'\n');
553                
554                // Now we actually look up the table
555                if let Some(tables) = &print_data.quad_tables {
556                    if let Some(table) = tables.get(name) {
557                        for row in table {
558                            // First row
559                            target.extend_from_slice(&Command::SelectFont{font: Font::FontB}.as_bytes());
560                            target.extend_from_slice(&format!("{}\n", row.0).into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
561                            target.extend_from_slice(&Command::SelectFont{font: Font::FontA}.as_bytes());
562                            // Now the three columns
563                            target.extend_from_slice(
564                                &trio_row((row.1.clone(), row.2.clone(), row.3.clone()), width, max_left, max_right)
565                            .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
566                        }
567                    } else {
568                        return Err(Error::NoTableFound(name.clone()))
569                    }
570                } else {
571                    return Err(Error::NoTables)
572                }
573            }
574        }
575        Ok(target)
576    }
577}
578
579// Auxiliar function to obtain three-row formatted string
580fn trio_row(mut row: (String, String, String), width: usize, max_left: usize, max_right: usize) -> String {
581    if row.0.len() > max_left {
582        row.0.replace_range((max_left-2).., "..");
583    }
584    if row.1.len() > width - max_left - max_right - 2 {
585        row.1.replace_range((width - max_left - max_right - 2).., "..");
586    }
587    if row.2.len() > max_left {
588        row.2.replace_range((max_right-2).., "..");
589    }
590    row.0.truncate(max_left);
591    row.2.truncate(max_right);
592    row.1.truncate(width - max_left - max_right - 2);
593
594    format!("{:<3$}{:^4$}{:>5$}\n",
595        row.0, row.1, row.2, // Words
596        max_left, width - max_left - max_right, max_right // Lengths
597    )
598}