escpos-rust 0.0.2

Control esc/pos printers with rust update from escpos-rs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
extern crate serde;
extern crate codepage_437;
extern crate image;
extern crate qrcode;

pub use self::print_data::{PrintData, PrintDataBuilder};
pub use self::justification::{Justification};
pub use self::escpos_image::EscposImage;

mod print_data;
mod justification;
mod escpos_image;

use qrcode::QrCode;
use codepage_437::{IntoCp437, CP437_CONTROL};
use crate::{
    Error, PrinterProfile,
    command::{Command, Font}
};
use serde::{Serialize, Deserialize};
use std::collections::HashSet;

/// Templates for recurrent prints
///
/// 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).
///
/// It is not adviced to construct the variants of the enum manually, read the available functions to guarantee a predictable outcome.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "kind")]
pub enum Instruction {
    /// Compound instruction, composed of multiple instructions that must be executed sequentially
    Compound {
        instructions: Vec<Instruction>
    },
    /// An instruction consisting of a single `esc/pos` command
    Command {
        command: Command
    },
    /// Short for jumping a specified number of lines
    VSpace {
        lines: u8
    },
    /// Raw text
    Text {
        /// Content to be printed
        content: String,
        /// Indicates if markdown translation should be applied
        markdown: bool,
        /// Font to be used with this text
        font: Font,
        /// Justification of the content
        justification: Justification,
        /// Maps a string to be replaced, to a description of the string
        replacements: Option<HashSet<String>>
    },
    /// 2 column table
    DuoTable {
        /// Name of the table. Required for attaching tuples for printing
        name: String,
        /// Header to be displayed on the table
        header: (String, String),
        /// Font used for the table
        font: Font
    },
    /// Table with three columns. Might be to tight for 50mm printers
    TrioTable {
        name: String,
        header: (String, String, String)
    },
    /// Fancy table for really detailed prints
    QuadTable {
        name: String,
        header: (String, String, String)
    },
    /// Contains a static image, that is, does not change with different printing mechanisms
    Image {
        /// Inner image
        image: EscposImage
    },
    /// Prints a QR Code. This field is dynamic
    QRCode {
        /// Name of the QR code, to be searched in the qr code content list
        name: String
    },
    /// Cuts the paper in place. Only for supported printers
    Cut
}

/// Instruction addition
impl std::ops::Add<Instruction> for Instruction {
    type Output = Instruction;
    fn add(self, rhs: Instruction) -> Self::Output {
        match self {
            Instruction::Compound{mut instructions} => {
                match rhs {
                    Instruction::Compound{instructions: mut rhs_instructions} => {
                        instructions.append(&mut rhs_instructions);
                    },
                    rhs => {
                        instructions.push(rhs);
                    }
                }
                Instruction::Compound{instructions}
            },
            lhs => {
                let mut instructions = vec![lhs];
                match rhs {
                    Instruction::Compound{instructions: mut rhs_instructions} => {
                        instructions.append(&mut rhs_instructions);
                    },
                    rhs => {
                        instructions.push(rhs);
                    }
                }
                Instruction::Compound{instructions}
            }
        }
    }
}

/// From iterator operation for a vector of instructions
impl std::iter::FromIterator<Instruction> for Option<Instruction> {
    fn from_iter<I: IntoIterator<Item=Instruction>>(iter: I) -> Self {
        let mut r = None;

        for elem in iter {
            if let Some(rd) = &mut r {
                *rd += elem;
            } else {
                r = Some(elem);
            }
        }

        r
    }
}

/// Mutable addition for instructions
impl std::ops::AddAssign for Instruction {
    fn add_assign(&mut self, other: Self) {
        // Now we deal with this thing
        if !self.is_compound() {
            // It was not a compound element, so we make it such
            *self = Instruction::Compound{instructions: vec![self.clone()]};
        }

        match self {
            Instruction::Compound{instructions} => {
                match other {
                    Instruction::Compound{instructions: mut other_instructions} => {
                        instructions.append(&mut other_instructions);
                    },
                    other => {
                        instructions.push(other);
                    }
                }
            },
            _ => panic!("Impossible error")
        }
    }
}

impl Instruction {
    /// Returns true if the instruction is compund
    pub fn is_compound(&self) -> bool {
        matches!(self, Instruction::Compound{..})
    }

    /// Returns true if the instruction is text
    pub fn is_text(&self) -> bool {
        matches!(self, Instruction::Text{..})
    }

    /// Sends simple text to the printer.
    ///
    /// Straightfoward text printing. The `replacements` set specifies which contents of the string should be replaced in a per-impresion basis.
    pub fn text<A: Into<String>>(content: A, font: Font, justification: Justification, replacements: Option<HashSet<String>>) -> Instruction {
        Instruction::Text {
            content: content.into(),
            markdown: false,
            font,
            justification,
            replacements
        }
    }

    /// Sends markdown text to the printer
    ///
    /// 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)
    ///  * Bold font, with **
    ///  * Italics, with _
    pub fn markdown(content: String, font: Font, justification: Justification, replacements: Option<HashSet<String>>) -> Instruction {
        Instruction::Text {
            content,
            markdown: true,
            font,
            justification,
            replacements
        }
    }

    /// Prints an image
    ///
    /// For a more precise control of position in the image, it is easier to edit the input image beforehand.
    pub fn image(image: EscposImage) -> Result<Instruction, Error> {
        Ok(Instruction::Image {
            image
        })
    }

    /// Creates a new QR code that does not change through different print steps
    pub fn qr_code(content: String) -> Result<Instruction, Error> {
        let code = QrCode::new(content.as_bytes()).unwrap();
        // Render the bits into an image.
        let img = code.render::<image::Rgba<u8>>().build();

        let escpos_image = EscposImage::new(
            image::DynamicImage::ImageRgba8(img),//.write_to(&mut content, image::ImageOutputFormat::Png).unwrap();
            128,
            Justification::Center
        )?;
        
        Instruction::image(escpos_image)
    }

    /// Creates a dynamic qr code instruction, which requires a string at printing time
    pub fn dynamic_qr_code<A: Into<String>>(name: A) -> Instruction {
        Instruction::QRCode{name: name.into()}
    }

    /// Executes a raw escpos command.
    pub fn command(command: Command) -> Instruction {
        Instruction::Command {
            command
        }
    }

    /// Creates a table with two columns.
    pub fn duo_table<A: Into<String>, B: Into<String>, C: Into<String>>(name: A, header: (B, C), font: Font) -> Instruction {
        Instruction::DuoTable {
            name: name.into(),
            header: (header.0.into(), header.1.into()),
            font
        }
    }

    /// Creates a table with three columns
    pub fn trio_table<A: Into<String>, B: Into<String>, C: Into<String>, D: Into<String>>(name: A, header: (B, C, D)) -> Instruction {
        Instruction::TrioTable {
            name: name.into(),
            header: (header.0.into(), header.1.into(), header.2.into())
        }
    }

    /// Creates a table with four columns
    ///
    /// Tables with four columns can be quite tight in 80mm printers, and unthinkable in 58mm ones or smaller. Use with caution!
    pub fn quad_table<A: Into<String>, B: Into<String>, C: Into<String>, D: Into<String>>(name: A, header: (B, C, D)) -> Instruction {
        Instruction::QuadTable {
            name: name.into(),
            header: (header.0.into(), header.1.into(), header.2.into())
        }
    }

    /// Cuts the paper (if supported)
    pub fn cut() -> Instruction {
        Instruction::Cut
    }

    /// Moves the paper a certain amount of vertical spaces
    pub fn vspace(lines: u8) -> Instruction {
        Instruction::VSpace{lines}
    }

    /// Main serialization function
    ///
    /// 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.
    pub(crate) fn to_vec(&self, printer_profile: &PrinterProfile, print_data: Option<&PrintData>) -> Result<Vec<u8>, Error> {
        let mut target = Vec::new();
        match self {
            Instruction::Compound{instructions} => {
                for instruction in instructions {
                    target.append(&mut instruction.to_vec(printer_profile, print_data)?);
                }
            },
            Instruction::Cut => {
                target.extend_from_slice(&Command::Cut.as_bytes());
            },
            Instruction::Command{command} => {
                target.append(&mut command.as_bytes());
            }
            Instruction::VSpace{lines} => {
                target.append(&mut vec![b'\n'; *lines as usize])
            },
            Instruction::Image{image} => {
                target.extend_from_slice(&image.feed(printer_profile.width));
            },
            Instruction::QRCode{name} => {
                let print_data = print_data.ok_or(Error::NoPrintData)?;
                if let Some(qr_contents) = &print_data.qr_contents {
                    if let Some(qr_content) = qr_contents.get(name) {
                        target.extend_from_slice(&Instruction::qr_code(qr_content.clone())?.to_vec(printer_profile, Some(print_data))?)
                    } else {
                        return Err(Error::NoQrContent(name.clone()))
                    }
                } else {
                    return Err(Error::NoQrContents)
                }
            },
            // Text serialization for the printer
            Instruction::Text{content, markdown, font, justification, replacements: self_replacements} => {
                // We setup the font, mainly
                target.append(&mut Command::SelectFont{font: font.clone()}.as_bytes());

                // We extract the width for this font
                let width = match printer_profile.columns_per_font.get(&font) {
                    Some(w) => *w,
                    None => return Err(Error::NoWidth)
                };

                let mut replaced_string = content.clone();
                // First of all, we replace all the replacements
                if let Some(self_replacements) = &self_replacements {
                    if !self_replacements.is_empty() {
                        let print_data = print_data.ok_or(Error::NoPrintData)?;

                        for key in self_replacements.iter() {
                            if let Some(replacement) = print_data.replacements.get(key) {
                                replaced_string = replaced_string.as_str().replace(key, replacement);
                            } else {
                                return Err(Error::NoReplacementFound(key.clone()))
                            }
                        }
                    }
                }

                // Now, we demarkdownize the string
                let demarkdown_string = if *markdown {
                    // We tokenize the string
                    let mut _tmp = String::new();
                    panic!("Not implemented the markdown thingy, is too hard!");
                } else {
                    replaced_string
                };

                // Now, we tokenize by spaces, using the width and justification
                let mut result = Command::Reset.as_bytes();
                // Line to control the text
                let mut line = String::new();
                let tokens = demarkdown_string.split_whitespace();
                let mut width_count = 0;
                
                for token in tokens {
                    if width_count + token.len() + 1 > (width as usize) {
                        // We have to create a new line, this does not fit.
                        width_count = token.len();
                        // Now we actually format the line
                        let mut tmp = match justification {
                            Justification::Left => format!("{}\n", line),
                            Justification::Right => format!("{:>1$}\n", line, width as usize),
                            Justification::Center => format!("{:^1$}\n", line, width as usize)
                        }.into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?;
                        result.append(&mut tmp);

                        // And we start the new line
                        line = token.to_string();
                    } else {
                        width_count += token.len();
                        if !line.is_empty() {
                            width_count += 1;
                            line += " ";
                        }
                        line += token;
                    }
                }

                // Last, we deal with the last line
                if !line.is_empty() {
                    let mut tmp = match justification {
                        Justification::Left => format!("{}\n", line),
                        Justification::Right => format!("{:>1$}\n", line, width as usize),
                        Justification::Center => format!("{:^1$}\n", line, width as usize)
                    }.into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?;
                    result.append(&mut tmp);
                }
                
                target.append(&mut result);
            },
            Instruction::DuoTable{name, header, font} => {
                // We extract the width for this font
                let width = match printer_profile.columns_per_font.get(&font) {
                    Some(w) => *w,
                    None => return Err(Error::NoWidth)
                };
                //First, the headers
                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)?);

                // Now, the line too
                target.append(&mut vec![b'-'; width as usize]);
                target.push(b'\n');
                
                // Now we actually look up the table
                let print_data = print_data.ok_or(Error::NoPrintData)?;

                if let Some(tables) = &print_data.duo_tables {
                    if let Some(table) = tables.get(name) {
                        for row in table {
                            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)?)
                        }
                    } else {
                        return Err(Error::NoTableFound(name.clone()))
                    }
                } else {
                    return Err(Error::NoTables)
                }
            },
            Instruction::TrioTable{name, header} => {
                // First, we will determine the proper alignment for the middle component
                let print_data = print_data.ok_or(Error::NoPrintData)?;

                let mut max_left: usize = header.0.len();
                let mut max_middle: usize = header.1.len();
                let mut max_right: usize = header.2.len();

                if let Some(tables) = &print_data.trio_tables {
                    if let Some(table) = tables.get(name) {
                        for row in table {
                            if row.0.len() > max_left {
                                max_left = row.0.len();
                            }
                            if row.1.len() > max_middle {
                                max_middle = row.1.len();
                            }
                            if row.2.len() > max_right {
                                max_right = row.2.len();
                            }
                        }
                    } else {
                        return Err(Error::NoTableFound(name.clone()))
                    }
                } else {
                    return Err(Error::NoTables)
                }

                // We chose a font
                let width = match printer_profile.columns_per_font.get(&Font::FontA) {
                    Some(w) => *w,
                    None => return Err(Error::NoWidth)
                } as usize;

                let (max_left, max_right) = if max_left + max_middle + max_right + 2 <= width {
                    // Todo va excelentemente bien.
                    (max_left, max_right)
                } else if max_middle + max_right + 2 <= width  && width - max_middle - max_right - 2 > 2 {
                    // I am sorry, Mr. left side.
                    (width - max_middle - max_right - 2, max_right)
                } else {
                    // Unluckily, we try to go for thirds
                    let third = width / 3;
                    if width % 3 == 0 {
                        (third, third)
                    } else if width % 3 == 1 {
                        (third, third)
                    } else {
                        (third, third)
                    }
                };

                // We go with the headers
                target.extend_from_slice(
                    &trio_row(header.clone(), width, max_left, max_right)
                .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);

                // Now, the line too
                target.append(&mut vec![b'-'; width]);
                target.push(b'\n');
                
                // Now we actually look up the table
                if let Some(tables) = &print_data.trio_tables {
                    if let Some(table) = tables.get(name) {
                        for row in table {
                            target.extend_from_slice(
                                &trio_row(row.clone(), width, max_left, max_right)
                            .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
                        }
                    } else {
                        return Err(Error::NoTableFound(name.clone()))
                    }
                } else {
                    return Err(Error::NoTables)
                }
            },
            Instruction::QuadTable{name, header} => {
                // First, we will determine the proper alignment for the middle component
                let print_data = print_data.ok_or(Error::NoPrintData)?;

                let mut max_left: usize = header.0.len();
                let mut max_middle: usize = header.1.len();
                let mut max_right: usize = header.2.len();
                if let Some(tables) = &print_data.quad_tables {
                    if let Some(table) = tables.get(name) {
                        for row in table {
                            if row.1.len() > max_left {
                                max_left = row.1.len();
                            }
                            if row.2.len() > max_middle {
                                max_middle = row.2.len();
                            }
                            if row.3.len() > max_right {
                                max_right = row.3.len();
                            }
                        }
                    } else {
                        return Err(Error::NoTableFound(name.clone()))
                    }
                } else {
                    return Err(Error::NoTables)
                }

                // We chose a font
                let width = match printer_profile.columns_per_font.get(&Font::FontA) {
                    Some(w) => *w,
                    None => return Err(Error::NoWidth)
                } as usize;

                let (max_left, max_right) = if max_left + max_middle + max_right + 2 <= width {
                    // Todo va excelentemente bien.
                    (max_left, max_right)
                } else if max_middle + max_right + 2 <= width  && width - max_middle - max_right - 2 > 2 {
                    // I am sorry, Mr. left side.
                    (width - max_middle - max_right - 2, max_right)
                } else {
                    // Unluckily, we try to go for thirds
                    let third = width / 3;
                    // This spacing algorithm requires work
                    if width % 3 == 0 {
                        (third, third)
                    } else if width % 3 == 1 {
                        (third, third)
                    } else {
                        (third, third)
                    }
                };

                // We go with the headers
                target.extend_from_slice(
                    &trio_row((header.0.clone(), header.1.clone(), header.2.clone()), width, max_left, max_right)
                .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);

                // Now, the line too
                target.append(&mut vec![b'-'; width]);
                target.push(b'\n');
                
                // Now we actually look up the table
                if let Some(tables) = &print_data.quad_tables {
                    if let Some(table) = tables.get(name) {
                        for row in table {
                            // First row
                            target.extend_from_slice(&Command::SelectFont{font: Font::FontB}.as_bytes());
                            target.extend_from_slice(&format!("{}\n", row.0).into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
                            target.extend_from_slice(&Command::SelectFont{font: Font::FontA}.as_bytes());
                            // Now the three columns
                            target.extend_from_slice(
                                &trio_row((row.1.clone(), row.2.clone(), row.3.clone()), width, max_left, max_right)
                            .into_cp437(&CP437_CONTROL).map_err(|_| Error::Encoding)?);
                        }
                    } else {
                        return Err(Error::NoTableFound(name.clone()))
                    }
                } else {
                    return Err(Error::NoTables)
                }
            }
        }
        Ok(target)
    }
}

// Auxiliar function to obtain three-row formatted string
fn trio_row(mut row: (String, String, String), width: usize, max_left: usize, max_right: usize) -> String {
    if row.0.len() > max_left {
        row.0.replace_range((max_left-2).., "..");
    }
    if row.1.len() > width - max_left - max_right - 2 {
        row.1.replace_range((width - max_left - max_right - 2).., "..");
    }
    if row.2.len() > max_left {
        row.2.replace_range((max_right-2).., "..");
    }
    row.0.truncate(max_left);
    row.2.truncate(max_right);
    row.1.truncate(width - max_left - max_right - 2);

    format!("{:<3$}{:^4$}{:>5$}\n",
        row.0, row.1, row.2, // Words
        max_left, width - max_left - max_right, max_right // Lengths
    )
}