diff --git a/.github/skills/tspl-reference/SKILL.md b/.github/skills/tspl-reference/SKILL.md new file mode 100644 index 0000000..1166089 --- /dev/null +++ b/.github/skills/tspl-reference/SKILL.md @@ -0,0 +1,109 @@ +--- +name: tspl-reference +description: "Look up TSPL/TSPL2 command specifications from official TSC programming manuals. Use when: implementing TSPL commands, debugging TSPL parsing, verifying parameter syntax, checking defaults, coordinate units, text or bitmap font behavior, graphics, bitmap modes, barcode/QR/PDF417/DataMatrix/Aztec/MaxiCode semantics, or fixing TSPL rendering differences." +argument-hint: "TSPL command name or rendering question (e.g. 'SIZE units', 'TEXT alignment', 'BARCODE parameters')" +--- + +# TSPL Official Reference Lookup + +## Purpose + +Fetch and interpret TSPL/TSPL2 command specifications from the official TSC programming manual to resolve ambiguities in labelize's TSPL parser and renderer. + +## When to Use + +- Implementing or fixing a TSPL command parser +- A `.tspl` label renders incorrectly +- Unsure about command syntax, optional parameters, defaults, ranges, or units +- Text, graphics, bitmap, or barcode behavior is unclear +- Comparing labelize output with a real TSC printer, TSC docs, or Zebra TSPL compatibility docs + +## Official Documentation + +Primary source: + +``` +https://fs.tscprinters.com/en/dl/4/2541 +``` + +Known direct PDF URL: + +``` +https://fs.tscprinters.com/system/files/31-0000001-00_tspl_tspl2_programming_3_0.pdf +``` + +If the direct PDF stops working, start from the TSC downloads page and search for `TSPL/ TSPL2 Programming Manual (English)`: + +``` +https://usca.tscprinters.com/en/downloads +``` + +Supplementary compatibility reference: + +``` +https://www.zebra.com/content/dam/support-dam/en/documentation/unrestricted/guide/software/zd100series-zd230series-zd888series-proman-en.pdf +``` + +## Common TSPL Command Areas + +| Area | Commands | +|------|----------| +| Setup and media | `SIZE`, `GAP`, `BLINE`, `OFFSET`, `SPEED`, `DENSITY`, `DIRECTION`, `REFERENCE`, `SHIFT`, `CODEPAGE`, `CLS`, `PRINT` | +| Text | `TEXT`, `BLOCK`, resident bitmap fonts, downloaded fonts, code pages | +| Graphics | `BAR`, `BOX`, `CIRCLE`, `ELLIPSE`, `ERASE`, `REVERSE`, `BITMAP`, `PUTBMP`, `PUTPCX`, `PUTPNG` | +| 1D barcodes | `BARCODE`, `TLC39`, `RSS` | +| 2D barcodes | `QRCODE`, `PDF417`, `MPDF417`, `DMATRIX`, `MAXICODE`, `AZTEC` | +| Memory/files | `DOWNLOAD`, `EOP`, `FILES`, `KILL`, `MOVE`, `RUN` | +| Device control | `FEED`, `BACKFEED`, `FORMFEED`, `HOME`, `CUT`, `SELFTEST`, `SOUND` | + +## Procedure + +1. **Identify the command** — Extract the TSPL command name from the user's question, fixture, or parser code. TSPL command names are case-insensitive in practice; normalize to uppercase when searching. + +2. **Fetch the official manual** — Use the web or fetch tool to load the TSC manual PDF. Search within the PDF for the command heading, then read the full command section. + +3. **Extract key details:** + - Syntax line and exact parameter order + - Parameter types, required vs optional fields, defaults, ranges, and units + - Coordinate behavior, especially dot units and effects of `REFERENCE`, `SHIFT`, and `DIRECTION` + - Bitmap and barcode mode semantics + - Printer-model or TSPL-vs-TSPL2 compatibility notes + +4. **Compare with labelize implementation:** + - Parser: `src/parsers/tspl_parser.rs` + - Rendered elements: `src/elements/` + - Renderer: `src/drawers/renderer.rs` + - Barcode encoders: `src/barcodes/` + - TSPL parser tests: `tests/unit_tspl_parser.rs` + - Fixture labels: `testdata/tspl/` + +5. **Report discrepancies** between the official spec and labelize's behavior before editing code. Include parameter position, official default, current implementation, and the expected rendering effect. + +6. **Verify focused changes** with TSPL-specific tests first: + ```bash + cargo test --test unit_tspl_parser + cargo test --test unit_renderer + ``` + + For parser or rendering changes with wider impact, also run: + ```bash + cargo test + ``` + +## Labelize TSPL Notes + +- `SIZE` may update per-label `DrawerOptions`; explicit CLI/API dimensions can override it per axis. +- Coordinates are interpreted in dots after applying `REFERENCE` and `SHIFT`. +- Supported render-focused commands currently include `SIZE`, `DIRECTION`, `REFERENCE`, `SHIFT`, `CLS`, `PRINT`, `TEXT`, `BAR`, `BOX`, `CIRCLE`, `ELLIPSE`, `ERASE`, `REVERSE`, `BITMAP`, `BARCODE`, `QRCODE`, and `PDF417`. +- Some TSPL commands may be accepted as device no-ops because they affect printer hardware rather than rendered output. +- Do not add command behavior from secondary references without checking the official TSC manual first. + +## Example + +User asks: "Why is TSPL TEXT too large?" + +1. Fetch the TSC TSPL/TSPL2 Programming Manual. +2. Search for the `TEXT` command and resident font table. +3. Check font width/height, x/y multiplication, rotation, and alignment parameters. +4. Compare with `parse_text`, `tspl_font_info`, and TSPL font metrics in `src/elements/font.rs`. +5. Add or adjust focused tests in `tests/unit_tspl_parser.rs`, then run `cargo test --test unit_tspl_parser`. diff --git a/README.md b/README.md index 6ffeda9..9d9e7aa 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Labelize — ZPL / EPL Label Renderer +# Labelize — ZPL / EPL / TSPL Label Renderer [![Crates.io](https://img.shields.io/crates/v/labelize)](https://crates.io/crates/labelize) [![License](https://img.shields.io/github/license/GOODBOY008/labelize)](LICENSE) [![Build](https://img.shields.io/github/actions/workflow/status/GOODBOY008/labelize/ci.yml?branch=main)](https://github.com/GOODBOY008/labelize/actions) -> **Turn ZPL/EPL into pixels — label rendering, simplified.** +> **Turn ZPL/EPL/TSPL into pixels — label rendering, simplified.** -Labelize is a fast, open-source Rust engine that parses **ZPL** (Zebra Programming Language) and **EPL** (Eltron Programming Language) label data and renders it to **PNG** or **PDF**. Use it as a **CLI tool**, an **HTTP microservice**, or embed it as a **Rust library** — no printer hardware required. +Labelize is a fast, open-source Rust engine that parses **ZPL** (Zebra Programming Language), **EPL** (Eltron Programming Language), and **TSPL** label data and renders it to **PNG** or **PDF**. Use it as a **CLI tool**, an **HTTP microservice**, or embed it as a **Rust library** — no printer hardware required. If you need a self-hosted, offline alternative to [Labelary](http://labelary.com/) for previewing and converting thermal label formats, Labelize has you covered. @@ -39,11 +39,12 @@ Benchmarked against the Labelary API on the same set of labels: - **ZPL Parser** — 30+ ZPL commands: text, barcodes, graphics, stored formats, graphic fields, field blocks, and more - **EPL Parser** — EPL command support for text, barcodes, line draw, and reference points +- **TSPL Parser** — render-focused support for `SIZE`, text, graphics, bitmaps, QR/PDF417, and common 1D barcodes - **10 Barcode Symbologies** — Code 128, Code 39, EAN-13, Interleaved 2-of-5, PDF417, Aztec, DataMatrix, QR Code, MaxiCode - **PNG & PDF Output** — Monochrome 1-bit PNG or single-page embedded PDF output -- **CLI Tool** — Convert ZPL/EPL files from the command line with format auto-detection, multi-label support, and customizable label dimensions +- **CLI Tool** — Convert ZPL/EPL/TSPL files from the command line with format auto-detection, multi-label support, and customizable label dimensions - **HTTP Microservice** — RESTful API for label conversion with format detection via `Content-Type` header; deploy anywhere with Docker or bare metal -- **Web Playground** — Built-in browser UI at `GET /` — paste or open a `.zpl`/`.epl` file, choose a label size (4×6, 4×4, etc.), render PNG inline, and download PNG or PDF with one click +- **Web Playground** — Built-in browser UI at `GET /` — paste or open a `.zpl`/`.epl`/`.tspl` file, choose a label size (4×6, 4×4, etc.), render PNG inline, and download PNG or PDF with one click - **Embedded Fonts** — Zero runtime font dependencies; bundles Helvetica Bold Condensed, DejaVu Sans Mono, and ZPL GS fonts - **Rust Library** — Integrate label rendering directly into your Rust application via the public API @@ -64,6 +65,7 @@ cargo install --path . ```bash labelize convert label.zpl # → label.png (format auto-detected) labelize convert label.epl # EPL works too +labelize convert label.tspl # TSPL SIZE controls dimensions unless overridden labelize convert label.zpl -t pdf # output as PDF labelize convert label.zpl --width 100 --height 62 --dpmm 12 # custom dimensions ``` @@ -74,7 +76,7 @@ labelize convert label.zpl --width 100 --height 62 --dpmm 12 # custom dimension labelize serve --port 8080 ``` -Open **http://localhost:8080/** in your browser to use the built-in **interactive playground** — paste ZPL/EPL, pick a label size, and render PNG instantly. Download PNG or PDF directly from the page. +Open **http://localhost:8080/** in your browser to use the built-in **interactive playground** — paste ZPL/EPL/TSPL, pick a label size, and render PNG instantly. Download PNG or PDF directly from the page. ```bash # Convert via REST API @@ -90,16 +92,16 @@ curl -X POST http://localhost:8080/convert \ Usage: labelize Commands: - convert Convert a ZPL/EPL file to PNG or PDF + convert Convert a ZPL/EPL/TSPL file to PNG or PDF serve Start HTTP server for label conversion Convert Options: - Input file path (.zpl or .epl) + Input file path (.zpl, .epl, or .tspl) -o, --output Output file path (default: input stem + .png/.pdf) - -f, --format Input format override: zpl | epl + -f, --format Input format override: zpl | epl | tspl -t, --type Output type: png | pdf [default: png] - --width Label width in mm [default: 102] - --height Label height in mm [default: 152] + --width Label width override in mm + --height Label height override in mm --dpmm Dots per mm [default: 8] Serve Options: @@ -124,7 +126,7 @@ Serve Options: | `dpmm` | 8 | Dots per mm | | `output` | png | Output format: png/pdf | -Set `Content-Type: application/zpl` or `Content-Type: application/epl` to select the parser. +Set `Content-Type: application/zpl`, `application/epl`, or `application/tspl` to select the parser. For TSPL, `SIZE` supplies render dimensions unless `width` or `height` is provided explicitly. ## Library Usage @@ -143,7 +145,7 @@ renderer.draw_label_as_png(&labels[0], &mut buf, DrawerOptions::default()).unwra std::fs::write("output.png", buf.into_inner()).unwrap(); ``` -## Supported ZPL & EPL Commands +## Supported ZPL, EPL & TSPL Commands ### ZPL Commands @@ -159,10 +161,14 @@ std::fs::write("output.png", buf.into_inner()).unwrap(); `N` (new label) · `A` (text) · `B` (barcode) · `LO` (line draw) · `R` (reference point) · `P` (print) +### TSPL Commands + +`SIZE` · `DIRECTION` · `REFERENCE` · `SHIFT` · `CLS` · `PRINT` · `TEXT` · `BAR` · `BOX` · `CIRCLE` · `ELLIPSE` · `ERASE` · `REVERSE` · `BITMAP` · `BARCODE` · `QRCODE` · `PDF417` + ## Architecture ``` - ZPL/EPL input + ZPL/EPL/TSPL input │ ▼ ┌─────────┐ ┌──────────┐ ┌─────────┐ @@ -226,7 +232,7 @@ cargo build --release ## Related Projects & Keywords -Looking for a **ZPL renderer**, **ZPL to PNG converter**, **ZPL to PDF**, **EPL parser**, **Zebra label preview**, **thermal label rendering**, or **Labelary alternative**? Labelize covers all of these. +Looking for a **ZPL renderer**, **ZPL to PNG converter**, **ZPL to PDF**, **EPL parser**, **TSPL parser**, **Zebra label preview**, **thermal label rendering**, or **Labelary alternative**? Labelize covers all of these. ## Contributing diff --git a/docs/TSPL_COMMANDS_REFERENCE.md b/docs/TSPL_COMMANDS_REFERENCE.md new file mode 100644 index 0000000..2893458 --- /dev/null +++ b/docs/TSPL_COMMANDS_REFERENCE.md @@ -0,0 +1,5 @@ +# TSPL Commands Reference + +Source: [TSPL/ TSPL2 Programming Manual 3.0 (2026)](https://fs.tscprinters.com/en/dl/4/2541) +Source: [TSPL/ TSPL2 Programming Manual (2009)](https://cleversoftsolutions.com/descargas/utilidades/Impresoras/Tsc/TSPL_TSPL2_Programming2.pdf) +Source: [Zebra TSPL Programming Guide](https://www.zebra.com/content/dam/support-dam/en/documentation/unrestricted/guide/software/zd100series-zd230series-zd888series-proman-en.pdf) diff --git a/docs/USAGE.md b/docs/USAGE.md index 49ebfd9..cf464d9 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -38,6 +38,13 @@ labelize convert label.epl # → label.png (auto-detected from extension) ``` +### Convert a TSPL file to PNG + +```bash +labelize convert label.tspl +# → label.png (TSPL SIZE controls dimensions unless overridden) +``` + ### Specify output path ```bash @@ -55,6 +62,7 @@ labelize convert label.zpl -t pdf ```bash labelize convert data.txt --format zpl +labelize convert data.txt --format tspl ``` ### Custom label dimensions @@ -65,10 +73,10 @@ labelize convert label.zpl --width 100 --height 150 --dpmm 12 | Option | Default | Description | |-------------|---------|------------------------------------------| -| `--width` | 102 | Label width in mm | -| `--height` | 152 | Label height in mm | +| `--width` | 102 or TSPL `SIZE` | Label width override in mm | +| `--height` | 152 or TSPL `SIZE` | Label height override in mm | | `--dpmm` | 8 | Dots per mm (6, 8, 12, or 24) | -| `-f, --format` | auto | Input format: `zpl` or `epl` | +| `-f, --format` | auto | Input format: `zpl`, `epl`, or `tspl` | | `-t, --type` | png | Output type: `png` or `pdf` | | `-o, --output` | auto | Output file path | @@ -123,6 +131,15 @@ curl -X POST http://localhost:8080/convert \ -o label.png ``` +### Convert TSPL to PNG via HTTP + +```bash +curl -X POST http://localhost:8080/convert \ + -H "Content-Type: application/tspl" \ + --data-binary @label.tspl \ + -o label.png +``` + ### Convert to PDF via HTTP Add `?output=pdf` to the URL: @@ -145,8 +162,8 @@ curl -X POST "http://localhost:8080/convert?width=100&height=62&dpmm=12" \ | Parameter | Default | Description | |-----------|---------|-------------------------| -| `width` | 102 | Label width in mm | -| `height` | 152 | Label height in mm | +| `width` | 102 or TSPL `SIZE` | Label width override in mm | +| `height` | 152 or TSPL `SIZE` | Label height override in mm | | `dpmm` | 8 | Dots per mm | | `output` | png | Output format: png/pdf | @@ -262,3 +279,14 @@ fn main() { | `LO` | Line draw (graphic box) | | `R` | Reference point | | `P` | Print label | + +## Supported TSPL Commands + +| Category | Commands | +|----------|----------| +| **Layout** | `SIZE` `DIRECTION` `REFERENCE` `SHIFT` `CLS` `PRINT` | +| **Text** | `TEXT` | +| **Graphics** | `BAR` `BOX` `CIRCLE` `ELLIPSE` `ERASE` `REVERSE` `BITMAP` | +| **Barcodes** | `BARCODE` (`128`, `EAN128`, `39`, `39C`, `EAN13`) `QRCODE` `PDF417` | + +Printer-control commands such as `GAP`, `SPEED`, `DENSITY`, and `SET` are parsed as no-ops because they do not affect rendered output. diff --git a/src/assets/fonts/TSS16.BF2 b/src/assets/fonts/TSS16.BF2 new file mode 100644 index 0000000..fe04c10 Binary files /dev/null and b/src/assets/fonts/TSS16.BF2 differ diff --git a/src/assets/fonts/TSS24.BF2 b/src/assets/fonts/TSS24.BF2 new file mode 100644 index 0000000..7924d6f Binary files /dev/null and b/src/assets/fonts/TSS24.BF2 differ diff --git a/src/assets/fonts/WenQuanYiBitmapSong12px.ttf b/src/assets/fonts/WenQuanYiBitmapSong12px.ttf new file mode 100644 index 0000000..0fa26bb Binary files /dev/null and b/src/assets/fonts/WenQuanYiBitmapSong12px.ttf differ diff --git a/src/assets/fonts/WenQuanYiBitmapSong16px.ttf b/src/assets/fonts/WenQuanYiBitmapSong16px.ttf new file mode 100644 index 0000000..e74d9fb Binary files /dev/null and b/src/assets/fonts/WenQuanYiBitmapSong16px.ttf differ diff --git a/src/assets/mod.rs b/src/assets/mod.rs index eb29963..5947f43 100644 --- a/src/assets/mod.rs +++ b/src/assets/mod.rs @@ -2,3 +2,5 @@ pub static FONT_HELVETICA_BOLD: &[u8] = include_bytes!("fonts/HelveticaBoldConde pub static FONT_DEJAVU_SANS_MONO: &[u8] = include_bytes!("fonts/DejaVuSansMono.ttf"); pub static FONT_DEJAVU_SANS_MONO_BOLD: &[u8] = include_bytes!("fonts/DejaVuSansMonoBold.ttf"); pub static FONT_ZPL_GS: &[u8] = include_bytes!("fonts/ZplGSCustom.ttf"); +pub static WENQUANYI_BITMAP_SONG_12PX: &[u8] = include_bytes!("fonts/WenQuanYiBitmapSong12px.ttf"); +pub static WENQUANYI_BITMAP_SONG_16PX: &[u8] = include_bytes!("fonts/WenQuanYiBitmapSong16px.ttf"); diff --git a/src/drawers/renderer.rs b/src/drawers/renderer.rs index 74e94f2..e7df2f0 100644 --- a/src/drawers/renderer.rs +++ b/src/drawers/renderer.rs @@ -23,6 +23,11 @@ static FONT_DEJAVU_MONO: &[u8] = crate::assets::FONT_DEJAVU_SANS_MONO; static FONT_DEJAVU_BOLD: &[u8] = crate::assets::FONT_DEJAVU_SANS_MONO_BOLD; static FONT_GS: &[u8] = crate::assets::FONT_ZPL_GS; +// Chinese font use for TSPL +// Original SIMSUN is strict prohibited for commercial use, so we use open-source substitute from WenQuanYi project +static WENQUANYI_BITMAP_SONG_12PX: &[u8] = crate::assets::WENQUANYI_BITMAP_SONG_12PX; +static WENQUANYI_BITMAP_SONG_16PX: &[u8] = crate::assets::WENQUANYI_BITMAP_SONG_16PX; + pub struct Renderer; impl Default for Renderer { @@ -145,6 +150,10 @@ impl Renderer { self.draw_graphic_circle(canvas, gc); Ok(()) } + LabelElement::GraphicEllipse(ge) => { + self.draw_graphic_ellipse(canvas, ge); + Ok(()) + } LabelElement::DiagonalLine(dl) => { self.draw_diagonal_line(canvas, dl); Ok(()) @@ -308,11 +317,8 @@ impl Renderer { let h = gb.height.max(gb.border_thickness); let border = gb.border_thickness; - if gb.corner_rounding > 0 { - // ZPL corner_rounding 1-8: radius = (shorter_side / 2) * (rounding / 8) - let shorter = w.min(h); - let radius = - ((shorter as f64 / 2.0) * (gb.corner_rounding as f64 / 8.0)).round() as i32; + let radius = graphic_box_corner_radius(gb, w, h); + if radius > 0 { draw_rounded_rect(canvas, x, y, w, h, border, radius, color); } else { // Draw box with border @@ -370,6 +376,47 @@ impl Renderer { } } + fn draw_graphic_ellipse( + &self, + canvas: &mut RgbaImage, + ge: &crate::elements::graphic_ellipse::GraphicEllipse, + ) { + let color = line_color_to_rgba(ge.line_color); + let x = ge.position.x; + let y = ge.position.y; + let width = ge.width.max(1); + let height = ge.height.max(1); + let thickness = ge.border_thickness.max(1) as f32; + let rx = width as f32 / 2.0; + let ry = height as f32 / 2.0; + let cx = x as f32 + rx; + let cy = y as f32 + ry; + let inner_rx = (rx - thickness).max(0.0); + let inner_ry = (ry - thickness).max(0.0); + + for py in y.max(0)..(y + height).min(canvas.height() as i32) { + for px in x.max(0)..(x + width).min(canvas.width() as i32) { + let dx = (px as f32 + 0.5 - cx) / rx; + let dy = (py as f32 + 0.5 - cy) / ry; + if dx * dx + dy * dy > 1.0 { + continue; + } + + let inside_inner = if inner_rx > 0.0 && inner_ry > 0.0 { + let ix = (px as f32 + 0.5 - cx) / inner_rx; + let iy = (py as f32 + 0.5 - cy) / inner_ry; + ix * ix + iy * iy <= 1.0 + } else { + false + }; + + if !inside_inner { + canvas.put_pixel(px as u32, py as u32, color); + } + } + } + } + fn draw_diagonal_line( &self, canvas: &mut RgbaImage, @@ -451,17 +498,41 @@ impl Renderer { continue; } let val = (gf.data[idx] >> (7 - x % 8)) & 1; - if val != 0 { - for my in 0..mag_y { - for mx in 0..mag_x { - let px = base_x + x * mag_x + mx; - let py = base_y + y * mag_y + my; - if px >= 0 - && py >= 0 - && (px as u32) < canvas.width() - && (py as u32) < canvas.height() - { - canvas.put_pixel(px as u32, py as u32, black); + for my in 0..mag_y { + for mx in 0..mag_x { + let px = base_x + x * mag_x + mx; + let py = base_y + y * mag_y + my; + if px < 0 + || py < 0 + || (px as u32) >= canvas.width() + || (py as u32) >= canvas.height() + { + continue; + } + + match gf.mode { + crate::elements::graphic_field::GraphicFieldMode::Or => { + if val != 0 { + canvas.put_pixel(px as u32, py as u32, black); + } + } + crate::elements::graphic_field::GraphicFieldMode::Overwrite => { + let color = if val != 0 { + black + } else { + Rgba([255, 255, 255, 255]) + }; + canvas.put_pixel(px as u32, py as u32, color); + } + crate::elements::graphic_field::GraphicFieldMode::Xor => { + if val != 0 { + let bg = *canvas.get_pixel(px as u32, py as u32); + canvas.put_pixel( + px as u32, + py as u32, + Rgba([255 - bg[0], 255 - bg[1], 255 - bg[2], bg[3]]), + ); + } } } } @@ -521,6 +592,7 @@ impl Renderer { &img, bc.barcode.orientation, bc.barcode.line_above, + bc.barcode.line_alignment, bc.width, ); } @@ -544,6 +616,7 @@ impl Renderer { &img, bc.barcode.orientation, bc.barcode.line_above, + bc.barcode.line_alignment, bc.width, ); } @@ -574,6 +647,7 @@ impl Renderer { &img, bc.barcode.orientation, bc.barcode.line_above, + bc.barcode.line_alignment, bc.width, ); } @@ -599,6 +673,7 @@ impl Renderer { &img, bc.barcode.orientation, bc.barcode.line_above, + bc.barcode.line_alignment, bc.width, ); } @@ -692,7 +767,13 @@ impl Renderer { (pos.x - quiet_zone_px, pos.y + bc.height - quiet_zone_px) }; - overlay_at(canvas, &img, draw_x, draw_y); + let draw_pos = LabelPosition { + x: draw_x, + y: draw_y, + calculate_from_bottom: false, + automatic_position: false, + }; + overlay_with_rotation(canvas, &img, &draw_pos, bc.barcode.orientation); Ok(()) } @@ -713,10 +794,26 @@ fn get_ttf_font_data(name: &str) -> &'static [u8] { "0" => FONT_HELVETICA, "B" | "D" | "P" | "Q" | "S" => FONT_DEJAVU_BOLD, "GS" => FONT_GS, + "TST16.BF2" | "TTT16.BF2" | "TSS16.BF2" => WENQUANYI_BITMAP_SONG_12PX, + "TSS24.BF2" | "TTT24.BF2" | "TST24.BF2" => WENQUANYI_BITMAP_SONG_16PX, _ => FONT_DEJAVU_MONO, } } +fn graphic_box_corner_radius(gb: &crate::elements::graphic_box::GraphicBox, w: i32, h: i32) -> i32 { + if let Some(radius) = gb.corner_radius_dots { + return radius.max(0); + } + + if gb.corner_rounding > 0 { + // ZPL corner_rounding 1-8: radius = (shorter_side / 2) * (rounding / 8). + let shorter = w.min(h); + return ((shorter as f64 / 2.0) * (gb.corner_rounding as f64 / 8.0)).round() as i32; + } + + 0 +} + fn measure_text_width(text: &str, font: &FontRef, scale: PxScale) -> f32 { use ab_glyph::{Font, ScaleFont}; let scaled = font.as_scaled(scale); @@ -877,6 +974,7 @@ fn get_text_top_left_pos( // ^FO: position is top-left of the field area. Handle justification parameter. let x = match text.alignment { crate::elements::field_alignment::FieldAlignment::Right => x - w, + crate::elements::field_alignment::FieldAlignment::Center => x - w / 2.0, _ => x, }; return (x, y); @@ -1182,6 +1280,7 @@ fn draw_barcode_interpretation_line( barcode_img: &RgbaImage, orientation: FieldOrientation, line_above: bool, + alignment: crate::elements::field_alignment::FieldAlignment, module_width: i32, ) { let font_data = FONT_DEJAVU_MONO; @@ -1253,7 +1352,7 @@ fn draw_barcode_interpretation_line( match orientation { FieldOrientation::Normal => { - let cx = pos.x + (bw - text_width as i32) / 2; + let cx = aligned_interpretation_line_pos(pos.x, bw, text_width as i32, alignment); let ty = if line_above { pos.y - font_size as i32 - 2 } else { @@ -1277,10 +1376,10 @@ fn draw_barcode_interpretation_line( _ => buf, }; - // Position: center text along the barcode edge let (tx, ty) = match orientation { FieldOrientation::Rotated90 => { - let cy = pos.y + (bw - text_width as i32) / 2; + let cy = + aligned_interpretation_line_pos(pos.y, bw, text_width as i32, alignment); if line_above { (pos.x + bh + 2, cy) } else { @@ -1288,7 +1387,8 @@ fn draw_barcode_interpretation_line( } } FieldOrientation::Rotated180 => { - let cx = pos.x + (bw - text_width as i32) / 2; + let cx = + aligned_interpretation_line_pos(pos.x, bw, text_width as i32, alignment); if line_above { (cx, pos.y + bh + 2) } else { @@ -1296,7 +1396,8 @@ fn draw_barcode_interpretation_line( } } FieldOrientation::Rotated270 => { - let cy = pos.y + (bw - text_width as i32) / 2; + let cy = + aligned_interpretation_line_pos(pos.y, bw, text_width as i32, alignment); if line_above { (pos.x - rotated.width() as i32 - 2, cy) } else { @@ -1309,3 +1410,19 @@ fn draw_barcode_interpretation_line( } } } + +fn aligned_interpretation_line_pos( + origin: i32, + span: i32, + text_width: i32, + alignment: crate::elements::field_alignment::FieldAlignment, +) -> i32 { + match alignment { + crate::elements::field_alignment::FieldAlignment::Right => origin + span - text_width, + crate::elements::field_alignment::FieldAlignment::Center + | crate::elements::field_alignment::FieldAlignment::Auto => { + origin + (span - text_width) / 2 + } + crate::elements::field_alignment::FieldAlignment::Left => origin, + } +} diff --git a/src/elements/barcode_128.rs b/src/elements/barcode_128.rs index 9575a83..628415c 100644 --- a/src/elements/barcode_128.rs +++ b/src/elements/barcode_128.rs @@ -1,3 +1,4 @@ +use super::field_alignment::FieldAlignment; use super::field_orientation::FieldOrientation; use super::label_position::LabelPosition; use super::reverse_print::ReversePrint; @@ -17,6 +18,7 @@ pub struct Barcode128 { pub height: i32, pub line: bool, pub line_above: bool, + pub line_alignment: FieldAlignment, pub check_digit: bool, pub mode: BarcodeMode, } diff --git a/src/elements/barcode_2of5.rs b/src/elements/barcode_2of5.rs index 3516501..81a4889 100644 --- a/src/elements/barcode_2of5.rs +++ b/src/elements/barcode_2of5.rs @@ -1,3 +1,4 @@ +use super::field_alignment::FieldAlignment; use super::field_orientation::FieldOrientation; use super::label_position::LabelPosition; use super::reverse_print::ReversePrint; @@ -8,6 +9,7 @@ pub struct Barcode2of5 { pub height: i32, pub line: bool, pub line_above: bool, + pub line_alignment: FieldAlignment, pub check_digit: bool, } diff --git a/src/elements/barcode_39.rs b/src/elements/barcode_39.rs index 5feab0c..56ffea0 100644 --- a/src/elements/barcode_39.rs +++ b/src/elements/barcode_39.rs @@ -1,3 +1,4 @@ +use super::field_alignment::FieldAlignment; use super::field_orientation::FieldOrientation; use super::label_position::LabelPosition; use super::reverse_print::ReversePrint; @@ -8,6 +9,7 @@ pub struct Barcode39 { pub height: i32, pub line: bool, pub line_above: bool, + pub line_alignment: FieldAlignment, pub check_digit: bool, } diff --git a/src/elements/barcode_ean13.rs b/src/elements/barcode_ean13.rs index 21a7ef5..81fa016 100644 --- a/src/elements/barcode_ean13.rs +++ b/src/elements/barcode_ean13.rs @@ -1,3 +1,4 @@ +use super::field_alignment::FieldAlignment; use super::field_orientation::FieldOrientation; use super::label_position::LabelPosition; use super::reverse_print::ReversePrint; @@ -8,6 +9,7 @@ pub struct BarcodeEan13 { pub height: i32, pub line: bool, pub line_above: bool, + pub line_alignment: FieldAlignment, } #[derive(Clone, Debug)] diff --git a/src/elements/barcode_qr.rs b/src/elements/barcode_qr.rs index b99b51c..faf9edc 100644 --- a/src/elements/barcode_qr.rs +++ b/src/elements/barcode_qr.rs @@ -1,3 +1,4 @@ +use super::field_orientation::FieldOrientation; use super::label_position::LabelPosition; use super::reverse_print::ReversePrint; @@ -21,6 +22,7 @@ pub enum QrCharacterMode { #[derive(Clone, Debug)] pub struct BarcodeQr { pub magnification: i32, + pub orientation: FieldOrientation, } #[derive(Clone, Debug)] diff --git a/src/elements/field_alignment.rs b/src/elements/field_alignment.rs index 148d2ba..740a347 100644 --- a/src/elements/field_alignment.rs +++ b/src/elements/field_alignment.rs @@ -4,4 +4,5 @@ pub enum FieldAlignment { Left = 0, Right = 1, Auto = 2, + Center = 3, } diff --git a/src/elements/font.rs b/src/elements/font.rs index dba9c8c..3fa4716 100644 --- a/src/elements/font.rs +++ b/src/elements/font.rs @@ -36,6 +36,16 @@ fn bitmap_font_sizes() -> &'static HashMap<&'static str, [f64; 2]> { m.insert("G", [60.0, 40.0]); m.insert("H", [21.0, 13.0]); m.insert("GS", [24.0, 24.0]); + // TSPL resident bitmap fonts. The manual lists width × height; this map stores + // [height, width] to match the existing ZPL bitmap font table shape. + m.insert("TSPL_1", [12.0, 8.0]); + m.insert("TSPL_2", [20.0, 12.0]); + m.insert("TSPL_3", [24.0, 16.0]); + m.insert("TSPL_4", [32.0, 24.0]); + m.insert("TSPL_5", [48.0, 32.0]); + m.insert("TSPL_6", [19.0, 14.0]); + m.insert("TSPL_7", [27.0, 21.0]); + m.insert("TSPL_8", [25.0, 14.0]); m }) } @@ -121,6 +131,8 @@ impl FontInfo { // ~12px per character advance vs our DejaVu's ~10px. // 1.931 × 1.2 = 2.317 2.317 + } else if self.name.ends_with(".BF2") { + 1.0 } else { // Bitmap fonts A-H use DejaVu Sans Mono (Regular or Bold). // ab_glyph scales advances as: h_advance = h_advance_unscaled / height_unscaled * scale_x diff --git a/src/elements/graphic_box.rs b/src/elements/graphic_box.rs index 84a85b8..54ff9ed 100644 --- a/src/elements/graphic_box.rs +++ b/src/elements/graphic_box.rs @@ -9,6 +9,9 @@ pub struct GraphicBox { pub width: i32, pub height: i32, pub border_thickness: i32, + /// ZPL ^GB corner rounding value, where 1-8 is converted to a side-relative radius. pub corner_rounding: i32, + /// Direct corner radius in dots for languages whose box radius is absolute, such as TSPL. + pub corner_radius_dots: Option, pub line_color: LineColor, } diff --git a/src/elements/graphic_ellipse.rs b/src/elements/graphic_ellipse.rs new file mode 100644 index 0000000..733fb2a --- /dev/null +++ b/src/elements/graphic_ellipse.rs @@ -0,0 +1,13 @@ +use super::label_position::LabelPosition; +use super::line_color::LineColor; +use super::reverse_print::ReversePrint; + +#[derive(Clone, Debug)] +pub struct GraphicEllipse { + pub reverse_print: ReversePrint, + pub position: LabelPosition, + pub width: i32, + pub height: i32, + pub border_thickness: i32, + pub line_color: LineColor, +} diff --git a/src/elements/graphic_field.rs b/src/elements/graphic_field.rs index 17d474a..10114b1 100644 --- a/src/elements/graphic_field.rs +++ b/src/elements/graphic_field.rs @@ -8,11 +8,19 @@ pub enum GraphicFieldFormat { AR = 3, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GraphicFieldMode { + Overwrite, + Or, + Xor, +} + #[derive(Clone, Debug)] pub struct GraphicField { pub reverse_print: ReversePrint, pub position: LabelPosition, pub format: GraphicFieldFormat, + pub mode: GraphicFieldMode, pub data_bytes: i32, pub total_bytes: i32, pub row_bytes: i32, diff --git a/src/elements/label_element.rs b/src/elements/label_element.rs index 6f08724..9f6251f 100644 --- a/src/elements/label_element.rs +++ b/src/elements/label_element.rs @@ -10,6 +10,7 @@ use super::field_block::FieldBlock; use super::graphic_box::GraphicBox; use super::graphic_circle::GraphicCircle; use super::graphic_diagonal_line::GraphicDiagonalLine; +use super::graphic_ellipse::GraphicEllipse; use super::graphic_field::GraphicField; use super::graphic_symbol::GraphicSymbol; use super::maxicode::{Maxicode, MaxicodeWithData}; @@ -23,6 +24,7 @@ pub enum LabelElement { Text(TextField), GraphicBox(GraphicBox), GraphicCircle(GraphicCircle), + GraphicEllipse(GraphicEllipse), DiagonalLine(GraphicDiagonalLine), GraphicField(GraphicField), Barcode128(Barcode128WithData), @@ -61,6 +63,7 @@ impl LabelElement { LabelElement::Text(t) => t.reverse_print.value, LabelElement::GraphicBox(g) => g.reverse_print.value, LabelElement::GraphicCircle(g) => g.reverse_print.value, + LabelElement::GraphicEllipse(g) => g.reverse_print.value, LabelElement::DiagonalLine(g) => g.reverse_print.value, LabelElement::GraphicField(g) => g.reverse_print.value, LabelElement::Barcode128(b) => b.reverse_print.value, diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 8641d23..b9b3d42 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -16,6 +16,7 @@ pub mod font; pub mod graphic_box; pub mod graphic_circle; pub mod graphic_diagonal_line; +pub mod graphic_ellipse; pub mod graphic_field; pub mod graphic_symbol; pub mod label_element; diff --git a/src/lib.rs b/src/lib.rs index 496a677..47f8417 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,4 +20,5 @@ pub use error::LabelizeError; pub use images::monochrome::encode_png; pub use images::pdf::encode_pdf; pub use parsers::epl_parser::EplParser; +pub use parsers::tspl_parser::{TsplParsedLabel, TsplParser}; pub use parsers::zpl_parser::ZplParser; diff --git a/src/main.rs b/src/main.rs index a0b880b..da16c2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,15 +7,17 @@ use std::path::{Path, PathBuf}; #[cfg(feature = "cli")] use clap::{Parser, Subcommand, ValueEnum}; -#[cfg(any(feature = "cli", feature = "serve"))] -use labelize::{DrawerOptions, EplParser, LabelInfo, Renderer, ZplParser}; +#[cfg(feature = "cli")] +use labelize::{ + DrawerOptions, EplParser, LabelInfo, Renderer, TsplParsedLabel, TsplParser, ZplParser, +}; #[cfg(feature = "cli")] #[derive(Parser)] #[command( name = "labelize", version, - about = "Turn ZPL/EPL into pixels — label rendering, simplified." + about = "Turn ZPL/EPL/TSPL into pixels — label rendering, simplified." )] struct Cli { #[command(subcommand)] @@ -23,10 +25,11 @@ struct Cli { } #[cfg(feature = "cli")] -#[derive(Clone, Copy, ValueEnum)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] enum InputFormat { Zpl, Epl, + Tspl, } #[cfg(feature = "cli")] @@ -39,9 +42,9 @@ enum OutputType { #[cfg(feature = "cli")] #[derive(Subcommand)] enum Commands { - /// Convert a ZPL/EPL file to PNG or PDF + /// Convert a ZPL/EPL/TSPL file to PNG or PDF Convert { - /// Input file path (.zpl or .epl) + /// Input file path (.zpl, .epl, or .tspl) input: PathBuf, /// Output file path (default: input stem + .png/.pdf) @@ -56,13 +59,13 @@ enum Commands { #[arg(short = 't', long = "type", default_value = "png")] output_type: OutputType, - /// Label width in mm - #[arg(long, default_value_t = 102.0)] - width: f64, + /// Label width override in mm + #[arg(long)] + width: Option, - /// Label height in mm - #[arg(long, default_value_t = 152.0)] - height: f64, + /// Label height override in mm + #[arg(long)] + height: Option, /// Dots per mm (6, 8, 12, or 24) #[arg(long, default_value_t = 8)] @@ -131,18 +134,94 @@ fn detect_format(path: &Path, override_fmt: Option) -> InputFormat let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); match ext.to_lowercase().as_str() { "epl" => InputFormat::Epl, + "tspl" => InputFormat::Tspl, _ => InputFormat::Zpl, } } +#[cfg(feature = "cli")] +fn detect_format_from_content_type(content_type: &str) -> InputFormat { + let content_type = content_type.to_ascii_lowercase(); + if content_type.contains("tspl") { + InputFormat::Tspl + } else if content_type.contains("epl") { + InputFormat::Epl + } else { + InputFormat::Zpl + } +} + +fn default_width() -> f64 { + 102.0 +} + +fn default_height() -> f64 { + 152.0 +} + +fn default_dpmm() -> i32 { + 8 +} + #[cfg(feature = "cli")] fn parse_labels(content: &[u8], format: InputFormat) -> Result, String> { match format { InputFormat::Epl => EplParser::new().parse(content), + InputFormat::Tspl => TsplParser::new().parse(content), InputFormat::Zpl => ZplParser::new().parse(content), } } +#[cfg(feature = "cli")] +fn parse_labels_with_options( + content: &[u8], + format: InputFormat, + base_options: DrawerOptions, + width_override: Option, + height_override: Option, +) -> Result, String> { + match format { + InputFormat::Tspl => Ok(TsplParser::new() + .parse_with_options(content, base_options)? + .into_iter() + .map(|mut parsed| { + parsed.drawer_options = apply_dimension_overrides( + parsed.drawer_options, + width_override, + height_override, + ); + parsed + }) + .collect()), + _ => Ok(parse_labels(content, format)? + .into_iter() + .map(|label| TsplParsedLabel { + label, + drawer_options: apply_dimension_overrides( + base_options.clone(), + width_override, + height_override, + ), + }) + .collect()), + } +} + +#[cfg(feature = "cli")] +fn apply_dimension_overrides( + mut options: DrawerOptions, + width_override: Option, + height_override: Option, +) -> DrawerOptions { + if let Some(width) = width_override { + options.label_width_mm = width; + } + if let Some(height) = height_override { + options.label_height_mm = height; + } + options +} + #[cfg(feature = "cli")] fn output_extension(output_type: OutputType) -> &'static str { match output_type { @@ -195,28 +274,27 @@ fn convert_file( output: Option<&Path>, format: Option, output_type: OutputType, - width: f64, - height: f64, + width: Option, + height: Option, dpmm: i32, ) -> Result<(), String> { let content = fs::read(input).map_err(|e| format!("Failed to read input file: {}", e))?; let fmt = detect_format(input, format); - let labels = parse_labels(&content, fmt)?; + let base_options = DrawerOptions { + label_width_mm: width.unwrap_or_else(default_width), + label_height_mm: height.unwrap_or_else(default_height), + dpmm, + ..Default::default() + }; + let labels = parse_labels_with_options(&content, fmt, base_options, width, height)?; if labels.is_empty() { return Err("No labels found in input".to_string()); } - let options = DrawerOptions { - label_width_mm: width, - label_height_mm: height, - dpmm, - ..Default::default() - }; - let multi = labels.len() > 1; - for (i, label) in labels.iter().enumerate() { + for (i, parsed) in labels.iter().enumerate() { let out_path = match output { Some(p) if !multi => p.to_path_buf(), Some(p) => { @@ -231,7 +309,7 @@ fn convert_file( None => default_output_path(input, output_type, if multi { Some(i) } else { None }), }; - let data = render_label(label, &options, output_type)?; + let data = render_label(&parsed.label, &parsed.drawer_options, output_type)?; fs::write(&out_path, data).map_err(|e| format!("Failed to write output file: {}", e))?; println!("Converted {} -> {}", input.display(), out_path.display()); } @@ -271,26 +349,16 @@ async fn serve(host: String, port: u16) { #[derive(serde::Deserialize)] struct ConvertParams { - #[serde(default = "default_width")] - width: f64, - #[serde(default = "default_height")] - height: f64, + #[serde(default)] + width: Option, + #[serde(default)] + height: Option, #[serde(default = "default_dpmm")] dpmm: i32, #[serde(default)] output: Option, } - fn default_width() -> f64 { - 102.0 - } - fn default_height() -> f64 { - 152.0 - } - fn default_dpmm() -> i32 { - 8 - } - async fn convert_handler( headers: HeaderMap, Query(params): Query, @@ -302,36 +370,35 @@ async fn serve(host: String, port: u16) { .and_then(|v| v.to_str().ok()) .unwrap_or(""); - let labels = if content_type.contains("epl") { - EplParser::new().parse(&body) - } else { - ZplParser::new().parse(&body) + let format = detect_format_from_content_type(content_type); + let base_options = DrawerOptions { + label_width_mm: params.width.unwrap_or_else(default_width), + label_height_mm: params.height.unwrap_or_else(default_height), + dpmm: params.dpmm, + ..Default::default() }; + let labels = + parse_labels_with_options(&body, format, base_options, params.width, params.height); let labels = match labels { Ok(l) => l, Err(e) => return (StatusCode::BAD_REQUEST, e).into_response(), }; - let label = match labels.into_iter().next() { + let parsed = match labels.into_iter().next() { Some(l) => l, None => { return (StatusCode::BAD_REQUEST, "No labels found".to_string()).into_response() } }; - let options = DrawerOptions { - label_width_mm: params.width, - label_height_mm: params.height, - dpmm: params.dpmm, - ..Default::default() - }; - let want_pdf = params.output.as_deref() == Some("pdf"); let renderer = Renderer::new(); let mut buf = Cursor::new(Vec::new()); - if let Err(e) = renderer.draw_label_as_png(&label, &mut buf, options.clone()) { + if let Err(e) = + renderer.draw_label_as_png(&parsed.label, &mut buf, parsed.drawer_options.clone()) + { return (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(); } @@ -347,7 +414,7 @@ async fn serve(host: String, port: u16) { } }; let mut pdf_buf = Cursor::new(Vec::new()); - match labelize::encode_pdf(&img, &options, &mut pdf_buf) { + match labelize::encode_pdf(&img, &parsed.drawer_options, &mut pdf_buf) { Ok(_) => ( StatusCode::OK, [(header::CONTENT_TYPE, "application/pdf")], @@ -382,3 +449,58 @@ async fn serve(host: String, port: u16) { .expect("Failed to bind"); axum::serve(listener, app).await.expect("Server failed"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detect_format_supports_tspl_extension_and_override() { + assert_eq!( + detect_format(Path::new("label.tspl"), None), + InputFormat::Tspl + ); + assert_eq!( + detect_format(Path::new("label.txt"), Some(InputFormat::Tspl)), + InputFormat::Tspl + ); + } + + #[test] + fn content_type_detection_supports_tspl() { + assert_eq!( + detect_format_from_content_type("application/tspl; charset=utf-8"), + InputFormat::Tspl + ); + assert_eq!( + detect_format_from_content_type("text/vnd.tspl"), + InputFormat::Tspl + ); + assert_eq!( + detect_format_from_content_type("APPLICATION/TSPL"), + InputFormat::Tspl + ); + assert_eq!( + detect_format_from_content_type("application/epl"), + InputFormat::Epl + ); + } + + #[test] + fn explicit_dimensions_override_tspl_size_per_axis() { + let options = DrawerOptions { + label_width_mm: 50.0, + label_height_mm: 25.0, + dpmm: 8, + enable_inverted_labels: true, + }; + + let resolved = apply_dimension_overrides(options, Some(100.0), None); + assert_eq!(resolved.label_width_mm, 100.0); + assert_eq!(resolved.label_height_mm, 25.0); + + let resolved = apply_dimension_overrides(resolved, None, Some(75.0)); + assert_eq!(resolved.label_width_mm, 100.0); + assert_eq!(resolved.label_height_mm, 75.0); + } +} diff --git a/src/parsers/epl_parser.rs b/src/parsers/epl_parser.rs index df472a3..16f48ef 100644 --- a/src/parsers/epl_parser.rs +++ b/src/parsers/epl_parser.rs @@ -2,6 +2,7 @@ use crate::elements::barcode_128::{Barcode128, Barcode128WithData, BarcodeMode}; use crate::elements::barcode_2of5::{Barcode2of5, Barcode2of5WithData}; use crate::elements::barcode_39::{Barcode39, Barcode39WithData}; use crate::elements::barcode_ean13::{BarcodeEan13, BarcodeEan13WithData}; +use crate::elements::field_alignment::FieldAlignment; use crate::elements::field_orientation::FieldOrientation; use crate::elements::font::FontInfo; use crate::elements::graphic_box::GraphicBox; @@ -254,6 +255,7 @@ fn parse_epl_barcode(line: &str, ref_x: i32, ref_y: i32) -> Result Result Result Result Result Self { + Self + } +} + +impl TsplParser { + pub fn new() -> Self { + Self + } + + pub fn parse(&self, tspl_data: &[u8]) -> Result, String> { + Ok(self + .parse_with_options(tspl_data, DrawerOptions::default())? + .into_iter() + .map(|parsed| parsed.label) + .collect()) + } + + pub fn parse_with_options( + &self, + tspl_data: &[u8], + base_options: DrawerOptions, + ) -> Result, String> { + let commands = split_tspl_commands(tspl_data)?; + let mut state = TsplState::new(base_options.with_defaults()); + let mut labels = Vec::new(); + + for command in commands { + match command { + TsplCommand::Line(line) => self.parse_line(&line, &mut state, &mut labels)?, + TsplCommand::Bitmap { header, data } => { + self.parse_bitmap(&header, data, &mut state)? + } + } + } + + if !state.elements.is_empty() { + labels.push(state.to_label()); + } + + Ok(labels) + } + + fn parse_line( + &self, + line: &str, + state: &mut TsplState, + labels: &mut Vec, + ) -> Result<(), String> { + let line = line.trim(); + if line.is_empty() { + return Ok(()); + } + + let name = command_name(line); + match name.as_str() { + "SIZE" => parse_size(line, state), + "DIRECTION" => parse_direction(line, state), + "REFERENCE" => parse_reference(line, state), + "SHIFT" => parse_shift(line, state), + "CLS" => { + state.elements.clear(); + Ok(()) + } + "PRINT" => { + if !state.elements.is_empty() { + labels.push(state.to_label()); + state.elements.clear(); + } + Ok(()) + } + "TEXT" => parse_text(line, state), + "BAR" => parse_bar(line, state), + "BOX" => parse_box(line, state), + "CIRCLE" => parse_circle(line, state), + "ELLIPSE" => parse_ellipse(line, state), + "ERASE" => parse_region_box(line, state, LineColor::White, false), + "REVERSE" => parse_region_box(line, state, LineColor::Black, true), + "BARCODE" => parse_barcode(line, state), + "QRCODE" => parse_qrcode(line, state), + "PDF417" => parse_pdf417(line, state), + "MPDF417" | "PUTPCX" | "PUTBMP" | "DMATRIX" | "AZTEC" => { + Err(format!("unsupported TSPL command {}", name)) + } + _ if is_device_noop(&name) => Ok(()), + _ => Ok(()), + } + } + + fn parse_bitmap( + &self, + header: &str, + data: Vec, + state: &mut TsplState, + ) -> Result<(), String> { + let args = command_args(header, "BITMAP"); + if args.len() < 5 { + return Err(format!( + "TSPL BITMAP command requires at least 5 parameters, got {}", + args.len() + )); + } + + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let row_bytes = parse_i32_arg(&args[2]).unwrap_or(1).max(1); + let height = parse_i32_arg(&args[3]).unwrap_or(1).max(1); + let mode = match parse_i32_arg(&args[4]).unwrap_or(0) { + 1 => GraphicFieldMode::Or, + 2 => GraphicFieldMode::Xor, + _ => GraphicFieldMode::Overwrite, + }; + let total_bytes = row_bytes * height; + + state + .elements + .push(LabelElement::GraphicField(GraphicField { + reverse_print: ReversePrint::default(), + position: state.position(x, y), + format: GraphicFieldFormat::Raw, + mode, + data_bytes: total_bytes, + total_bytes, + row_bytes, + data, + magnification_x: 1, + magnification_y: 1, + })); + Ok(()) + } +} + +struct TsplState { + elements: Vec, + options: DrawerOptions, + reference_x: i32, + reference_y: i32, + shift_x: i32, + shift_y: i32, + inverted: bool, +} + +impl TsplState { + fn new(options: DrawerOptions) -> Self { + Self { + elements: Vec::new(), + options, + reference_x: 0, + reference_y: 0, + shift_x: 0, + shift_y: 0, + inverted: false, + } + } + + fn position(&self, x: i32, y: i32) -> LabelPosition { + LabelPosition { + x: x + self.reference_x + self.shift_x, + y: y + self.reference_y + self.shift_y, + calculate_from_bottom: false, + automatic_position: false, + } + } + + fn to_label(&self) -> TsplParsedLabel { + TsplParsedLabel { + drawer_options: self.options.clone(), + label: LabelInfo { + print_width: 0, + inverted: self.inverted, + elements: self.elements.clone(), + }, + } + } +} + +enum TsplCommand { + Line(String), + Bitmap { header: String, data: Vec }, +} + +fn split_tspl_commands(data: &[u8]) -> Result, String> { + let mut commands = Vec::new(); + let mut idx = 0usize; + + while idx < data.len() { + while idx < data.len() && matches!(data[idx], b'\r' | b'\n') { + idx += 1; + } + if idx >= data.len() { + break; + } + + let line_start = idx; + let token_end = first_token_end(data, line_start); + let token = String::from_utf8_lossy(&data[line_start..token_end]).to_uppercase(); + + if token == "BITMAP" { + let payload_start = bitmap_payload_start(data, line_start) + .ok_or_else(|| "TSPL BITMAP command missing bitmap data separator".to_string())?; + let header = String::from_utf8_lossy(&data[line_start..payload_start]).to_string(); + let args = command_args(&header, "BITMAP"); + if args.len() < 4 { + return Err(format!( + "TSPL BITMAP command requires at least 4 size parameters, got {}", + args.len() + )); + } + let row_bytes = parse_i32_arg(&args[2]).unwrap_or(1).max(1) as usize; + let height = parse_i32_arg(&args[3]).unwrap_or(1).max(1) as usize; + let data_len = row_bytes * height; + let payload_end = payload_start.saturating_add(data_len).min(data.len()); + commands.push(TsplCommand::Bitmap { + header, + data: data[payload_start..payload_end].to_vec(), + }); + idx = payload_end; + while idx < data.len() && matches!(data[idx], b'\r' | b'\n') { + idx += 1; + } + continue; + } + + idx = command_line_end(data, line_start); + let line = String::from_utf8_lossy(&data[line_start..idx]).to_string(); + commands.push(TsplCommand::Line(line)); + } + + Ok(commands) +} + +fn first_token_end(data: &[u8], start: usize) -> usize { + let mut idx = start; + while idx < data.len() && !data[idx].is_ascii_whitespace() { + idx += 1; + } + idx +} + +fn bitmap_payload_start(data: &[u8], start: usize) -> Option { + let mut commas = 0usize; + let mut idx = start; + while idx < data.len() { + if data[idx] == b',' { + commas += 1; + if commas == 5 { + return Some(idx + 1); + } + } + if matches!(data[idx], b'\r' | b'\n') { + return None; + } + idx += 1; + } + None +} + +fn command_line_end(data: &[u8], start: usize) -> usize { + let mut idx = start; + let mut in_quotes = false; + + while idx < data.len() { + if data[idx] == b'"' && !is_escaped_quote_byte(data, idx) { + in_quotes = !in_quotes; + } else if matches!(data[idx], b'\r' | b'\n') && !in_quotes { + break; + } + idx += 1; + } + + idx +} + +fn is_escaped_quote_byte(data: &[u8], idx: usize) -> bool { + idx > 0 && data[idx - 1] == b'\\' + || idx >= 2 + && data[idx - 2] == b'\\' + && data[idx - 1] == b'[' + && data.get(idx + 1) == Some(&b']') +} + +fn command_name(line: &str) -> String { + line.split_whitespace() + .next() + .unwrap_or("") + .to_ascii_uppercase() +} + +fn command_args(line: &str, name: &str) -> Vec { + let rest = line.get(name.len()..).unwrap_or("").trim_start(); + split_tspl_args(rest) +} + +fn split_tspl_args(input: &str) -> Vec { + let chars: Vec = input.chars().collect(); + let mut args = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut i = 0usize; + + while i < chars.len() { + let ch = chars[i]; + let escaped_quote = ch == '"' + && (i > 0 && chars[i - 1] == '\\' + || i >= 2 + && chars[i - 2] == '\\' + && chars[i - 1] == '[' + && chars.get(i + 1) == Some(&']')); + + if ch == '"' && !escaped_quote { + in_quotes = !in_quotes; + current.push(ch); + } else if ch == ',' && !in_quotes { + args.push(clean_arg(¤t)); + current.clear(); + } else { + current.push(ch); + } + i += 1; + } + + if !current.is_empty() || input.ends_with(',') { + args.push(clean_arg(¤t)); + } + + args +} + +fn clean_arg(arg: &str) -> String { + let trimmed = arg.trim(); + let unquoted = if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { + &trimmed[1..trimmed.len() - 1] + } else { + trimmed + }; + unquoted.replace("\\[\"]", "\"").replace("\\\"", "\"") +} + +fn parse_i32_arg(arg: &str) -> Option { + arg.trim().parse::().ok().map(|v| v.round() as i32) +} + +fn parse_f64_arg(arg: &str) -> Option { + arg.trim().parse::().ok() +} + +fn parse_size(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "SIZE"); + if args.is_empty() { + return Err("TSPL SIZE command requires at least a width".to_string()); + } + if let Some(width) = parse_dimension_mm(&args[0], state.options.dpmm) { + state.options.label_width_mm = width; + } + if let Some(height_arg) = args.get(1) { + if let Some(height) = parse_dimension_mm(height_arg, state.options.dpmm) { + state.options.label_height_mm = height; + } + } + Ok(()) +} + +fn parse_dimension_mm(arg: &str, dpmm: i32) -> Option { + let value_text: String = arg + .chars() + .take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-') + .collect(); + let value = parse_f64_arg(&value_text)?; + let lower = arg.trim().to_ascii_lowercase(); + if lower.ends_with("mm") { + Some(value) + } else if lower.ends_with("dot") || lower.ends_with("dots") { + Some(value / dpmm.max(1) as f64) + } else { + Some(value * 25.4) + } +} + +fn parse_direction(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "DIRECTION"); + if let Some(v) = args.first().and_then(|arg| parse_i32_arg(arg)) { + state.inverted = v == 0; + } + Ok(()) +} + +fn parse_reference(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "REFERENCE"); + if let Some(v) = args.first().and_then(|arg| parse_i32_arg(arg)) { + state.reference_x = v; + } + if let Some(v) = args.get(1).and_then(|arg| parse_i32_arg(arg)) { + state.reference_y = v; + } + Ok(()) +} + +fn parse_shift(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "SHIFT"); + match args.as_slice() { + [y] => { + state.shift_y = parse_i32_arg(y).unwrap_or(0); + } + [x, y, ..] => { + state.shift_x = parse_i32_arg(x).unwrap_or(0); + state.shift_y = parse_i32_arg(y).unwrap_or(0); + } + _ => {} + } + Ok(()) +} + +fn parse_text(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "TEXT"); + if args.len() < 7 { + return Err(format!( + "TSPL TEXT command requires at least 7 parameters, got {}", + args.len() + )); + } + + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let (font_name, base_w, base_h) = tspl_font_info(&args[2]); + let orientation = tspl_rotation(&args[3]); + let x_mult = parse_i32_arg(&args[4]).unwrap_or(1).clamp(1, 10); + let y_mult = parse_i32_arg(&args[5]).unwrap_or(1).clamp(1, 10); + let (alignment, content_idx) = if args.len() >= 8 { + (tspl_alignment(&args[6]), 7usize) + } else { + (FieldAlignment::Left, 6usize) + }; + let text = args.get(content_idx).cloned().unwrap_or_default(); + if text.is_empty() { + return Ok(()); + } + + state.elements.push(LabelElement::Text(TextField { + reverse_print: ReversePrint::default(), + font: FontInfo { + name: font_name, + width: (base_w * x_mult) as f64, + height: (base_h * y_mult) as f64, + orientation, + } + .with_adjusted_sizes(), + position: state.position(x, y), + alignment, + text, + block: None, + })); + Ok(()) +} + +fn tspl_font_info(font: &str) -> (String, i32, i32) { + let font = font.trim_matches('"').to_ascii_uppercase(); + match font.as_str() { + "2" => ("TSPL_2".to_string(), 12, 20), + "3" => ("TSPL_3".to_string(), 16, 24), + "4" => ("TSPL_4".to_string(), 24, 32), + "5" => ("TSPL_5".to_string(), 32, 48), + "6" => ("TSPL_6".to_string(), 14, 19), + "7" => ("TSPL_7".to_string(), 21, 27), + "8" => ("TSPL_8".to_string(), 14, 25), + "TST16.BF2" | "TTT16.BF2" | "TSS16.BF2" => (font, 16, 16), + "TST24.BF2" | "TTT24.BF2" | "TSS24.BF2" => (font, 24, 24), + _ => ("TSPL_1".to_string(), 8, 12), + } +} + +fn tspl_alignment(arg: &str) -> FieldAlignment { + match parse_i32_arg(arg).unwrap_or(0) { + 2 => FieldAlignment::Center, + 3 => FieldAlignment::Right, + _ => FieldAlignment::Left, + } +} + +fn parse_bar(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "BAR"); + if args.len() < 4 { + return Err(format!( + "TSPL BAR command requires 4 parameters, got {}", + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let width = parse_i32_arg(&args[2]).unwrap_or(1).max(1); + let height = parse_i32_arg(&args[3]).unwrap_or(1).max(1); + state.elements.push(LabelElement::GraphicBox(filled_box( + state, + x, + y, + width, + height, + LineColor::Black, + false, + ))); + Ok(()) +} + +fn parse_box(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "BOX"); + if args.len() < 5 { + return Err(format!( + "TSPL BOX command requires at least 5 parameters, got {}", + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let x_end = parse_i32_arg(&args[2]).unwrap_or(x); + let y_end = parse_i32_arg(&args[3]).unwrap_or(y); + let thickness = parse_i32_arg(&args[4]).unwrap_or(1).max(1); + let radius = args + .get(5) + .and_then(|arg| parse_i32_arg(arg)) + .unwrap_or(0) + .max(0); + state.elements.push(LabelElement::GraphicBox(GraphicBox { + reverse_print: ReversePrint::default(), + position: state.position(x, y), + width: (x_end - x).abs().max(thickness), + height: (y_end - y).abs().max(thickness), + border_thickness: thickness, + corner_rounding: 0, + corner_radius_dots: (radius > 0).then_some(radius), + line_color: LineColor::Black, + })); + Ok(()) +} + +fn parse_circle(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "CIRCLE"); + if args.len() < 4 { + return Err(format!( + "TSPL CIRCLE command requires 4 parameters, got {}", + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let diameter = parse_i32_arg(&args[2]).unwrap_or(1).max(1); + let thickness = parse_i32_arg(&args[3]).unwrap_or(1).max(1); + state + .elements + .push(LabelElement::GraphicCircle(GraphicCircle { + reverse_print: ReversePrint::default(), + position: state.position(x, y), + circle_diameter: diameter, + border_thickness: thickness, + line_color: LineColor::Black, + })); + Ok(()) +} + +fn parse_ellipse(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "ELLIPSE"); + if args.len() < 5 { + return Err(format!( + "TSPL ELLIPSE command requires 5 parameters, got {}", + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let width = parse_i32_arg(&args[2]).unwrap_or(1).max(1); + let height = parse_i32_arg(&args[3]).unwrap_or(1).max(1); + let thickness = parse_i32_arg(&args[4]).unwrap_or(1).max(1); + state + .elements + .push(LabelElement::GraphicEllipse(GraphicEllipse { + reverse_print: ReversePrint::default(), + position: state.position(x, y), + width, + height, + border_thickness: thickness, + line_color: LineColor::Black, + })); + Ok(()) +} + +fn parse_region_box( + line: &str, + state: &mut TsplState, + color: LineColor, + reverse: bool, +) -> Result<(), String> { + let name = command_name(line); + let args = command_args(line, &name); + if args.len() < 4 { + return Err(format!( + "TSPL {} command requires 4 parameters, got {}", + name, + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let width = parse_i32_arg(&args[2]).unwrap_or(1).max(1); + let height = parse_i32_arg(&args[3]).unwrap_or(1).max(1); + state.elements.push(LabelElement::GraphicBox(filled_box( + state, x, y, width, height, color, reverse, + ))); + Ok(()) +} + +fn filled_box( + state: &TsplState, + x: i32, + y: i32, + width: i32, + height: i32, + color: LineColor, + reverse: bool, +) -> GraphicBox { + GraphicBox { + reverse_print: ReversePrint { value: reverse }, + position: state.position(x, y), + width, + height, + border_thickness: width.min(height).max(1), + corner_rounding: 0, + corner_radius_dots: None, + line_color: color, + } +} + +fn parse_barcode(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "BARCODE"); + if args.len() < 9 { + return Err(format!( + "TSPL BARCODE command requires at least 9 parameters, got {}", + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let code_type = args[2].trim().to_ascii_uppercase(); + let height = parse_i32_arg(&args[3]).unwrap_or(1).max(1); + let human_readable = parse_i32_arg(&args[4]).unwrap_or(0); + let line = human_readable > 0; + let line_alignment = tspl_alignment(&args[4]); + let orientation = tspl_rotation(&args[5]); + let narrow = parse_i32_arg(&args[6]).unwrap_or(1).max(1); + let wide = parse_i32_arg(&args[7]).unwrap_or(narrow).max(narrow); + let content_idx = if args.len() >= 10 { 9 } else { 8 }; + let data = args.get(content_idx).cloned().unwrap_or_default(); + let position = state.position(x, y); + let width_ratio = (wide as f64 / narrow as f64).max(1.0); + + let element = match code_type.as_str() { + "128" | "128M" => LabelElement::Barcode128(Barcode128WithData { + reverse_print: ReversePrint::default(), + barcode: Barcode128 { + orientation, + height, + line, + line_above: false, + line_alignment, + check_digit: false, + mode: BarcodeMode::Automatic, + }, + width: narrow, + position, + data, + }), + "EAN128" | "EAN128M" => LabelElement::Barcode128(Barcode128WithData { + reverse_print: ReversePrint::default(), + barcode: Barcode128 { + orientation, + height, + line, + line_above: false, + line_alignment, + check_digit: false, + mode: BarcodeMode::Ucc, + }, + width: narrow, + position, + data, + }), + "39" | "39C" => LabelElement::Barcode39(Barcode39WithData { + reverse_print: ReversePrint::default(), + barcode: Barcode39 { + orientation, + height, + line, + line_above: false, + line_alignment, + check_digit: code_type == "39C", + }, + width: narrow, + width_ratio, + position, + data, + }), + "EAN13" => LabelElement::BarcodeEan13(BarcodeEan13WithData { + reverse_print: ReversePrint::default(), + barcode: BarcodeEan13 { + orientation, + height, + line, + line_above: false, + line_alignment, + }, + width: narrow, + position, + data, + }), + _ => return Err(format!("unsupported TSPL barcode type {}", code_type)), + }; + + state.elements.push(element); + Ok(()) +} + +fn parse_qrcode(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "QRCODE"); + if args.len() < 7 { + return Err(format!( + "TSPL QRCODE command requires at least 7 parameters, got {}", + args.len() + )); + } + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let level = args[2].chars().next().unwrap_or('H').to_ascii_uppercase(); + let magnification = parse_i32_arg(&args[3]).unwrap_or(1).clamp(1, 10); + let mode = args[4].chars().next().unwrap_or('A').to_ascii_uppercase(); + let orientation = tspl_rotation(&args[5]); + let content = args.last().cloned().unwrap_or_default(); + + state + .elements + .push(LabelElement::BarcodeQr(BarcodeQrWithData { + reverse_print: ReversePrint::default(), + barcode: BarcodeQr { + magnification, + orientation, + }, + height: 0, + position: state.position(x, y), + data: format!("{}{},{}", level, mode, content), + })); + Ok(()) +} + +fn parse_pdf417(line: &str, state: &mut TsplState) -> Result<(), String> { + let args = command_args(line, "PDF417"); + if args.len() < 6 { + return Err(format!( + "TSPL PDF417 command requires at least 6 parameters, got {}", + args.len() + )); + } + + let x = parse_i32_arg(&args[0]).unwrap_or(0); + let y = parse_i32_arg(&args[1]).unwrap_or(0); + let expected_height = parse_i32_arg(&args[3]).unwrap_or(10).max(1); + let orientation = tspl_rotation(&args[4]); + let content = args.last().cloned().unwrap_or_default(); + let mut barcode = BarcodePdf417 { + orientation, + row_height: 0, + security: 0, + columns: 0, + rows: 0, + truncate: false, + module_width: 2, + by_height: expected_height, + }; + + for option in args.iter().skip(5).take(args.len().saturating_sub(6)) { + apply_pdf417_option(option, &mut barcode); + } + + state + .elements + .push(LabelElement::BarcodePdf417(BarcodePdf417WithData { + reverse_print: ReversePrint::default(), + barcode, + position: state.position(x, y), + data: content, + })); + Ok(()) +} + +fn apply_pdf417_option(option: &str, barcode: &mut BarcodePdf417) { + let upper = option.trim().to_ascii_uppercase(); + if upper.is_empty() { + return; + } + let value = upper + .get(1..) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + match upper.as_bytes()[0] { + b'E' => barcode.security = value.clamp(0, 8), + b'W' => barcode.module_width = value.max(1), + b'H' => barcode.row_height = value.max(1), + b'R' => barcode.rows = value, + b'C' => barcode.columns = value, + b'T' => barcode.truncate = value == 1, + _ => {} + } +} + +fn tspl_rotation(arg: &str) -> FieldOrientation { + match parse_i32_arg(arg).unwrap_or(0) { + 1 | 90 => FieldOrientation::Rotated90, + 2 | 180 => FieldOrientation::Rotated180, + 3 | 270 => FieldOrientation::Rotated270, + _ => FieldOrientation::Normal, + } +} + +fn is_device_noop(name: &str) -> bool { + matches!( + name, + "GAP" + | "BLINE" + | "GAPDETECT" + | "BLINEDETECT" + | "AUTODETECT" + | "OFFSET" + | "SPEED" + | "DENSITY" + | "CODEPAGE" + | "FEED" + | "BACKFEED" + | "FORMFEED" + | "HOME" + | "SELFTEST" + | "INITIALPRINTER" + | "DOWNLOAD" + | "EOP" + | "FILES" + | "KILL" + | "RUN" + | "OUT" + | "SET" + ) +} diff --git a/src/parsers/zpl_parser.rs b/src/parsers/zpl_parser.rs index 16a5a9d..c04a77e 100644 --- a/src/parsers/zpl_parser.rs +++ b/src/parsers/zpl_parser.rs @@ -6,11 +6,12 @@ use crate::elements::barcode_datamatrix::{BarcodeDatamatrix, DatamatrixRatio}; use crate::elements::barcode_ean13::BarcodeEan13; use crate::elements::barcode_pdf417::BarcodePdf417; use crate::elements::barcode_qr::BarcodeQr; +use crate::elements::field_alignment::FieldAlignment; use crate::elements::field_block::FieldBlock; use crate::elements::graphic_box::GraphicBox; use crate::elements::graphic_circle::GraphicCircle; use crate::elements::graphic_diagonal_line::GraphicDiagonalLine; -use crate::elements::graphic_field::{GraphicField, GraphicFieldFormat}; +use crate::elements::graphic_field::{GraphicField, GraphicFieldFormat, GraphicFieldMode}; use crate::elements::graphic_symbol::GraphicSymbol; use crate::elements::label_element::LabelElement; use crate::elements::label_info::LabelInfo; @@ -536,6 +537,7 @@ impl ZplParser { height: self.printer.default_barcode_dimensions.height, line: true, line_above: false, + line_alignment: FieldAlignment::Center, check_digit: false, mode: BarcodeMode::No, }; @@ -578,6 +580,7 @@ impl ZplParser { height: self.printer.default_barcode_dimensions.height, line: true, line_above: false, + line_alignment: FieldAlignment::Center, }; if let Some(s) = parts.first() { if !s.is_empty() { @@ -608,6 +611,7 @@ impl ZplParser { height: self.printer.default_barcode_dimensions.height, line: true, line_above: false, + line_alignment: FieldAlignment::Center, check_digit: false, }; if let Some(s) = parts.first() { @@ -644,6 +648,7 @@ impl ZplParser { height: self.printer.default_barcode_dimensions.height, line: true, line_above: false, + line_alignment: FieldAlignment::Center, check_digit: false, }; if let Some(s) = parts.first() { @@ -784,7 +789,15 @@ impl ZplParser { fn parse_barcode_qr(&mut self, command: &str) { let parts = split_command(command, "^BQ"); - let mut bc = BarcodeQr { magnification: 1 }; + let mut bc = BarcodeQr { + magnification: 1, + orientation: self.printer.default_orientation, + }; + if let Some(s) = parts.first() { + if !s.is_empty() { + bc.orientation = to_field_orientation(s.as_bytes()[0]); + } + } if let Some(v) = parts.get(2).and_then(|s| parse_int(s)) { bc.magnification = v.clamp(1, 100); } @@ -822,6 +835,7 @@ impl ZplParser { height: 1, border_thickness: 1, corner_rounding: 0, + corner_radius_dots: None, line_color: LineColor::Black, reverse_print: self.printer.get_reverse_print(), }; @@ -915,6 +929,7 @@ impl ZplParser { magnification_y: 1, reverse_print: self.printer.get_reverse_print(), format: GraphicFieldFormat::Hex, + mode: GraphicFieldMode::Or, data_bytes: 0, total_bytes: 0, row_bytes: 0, @@ -1025,6 +1040,7 @@ impl ZplParser { magnification_y: 1, reverse_print: ReversePrint::default(), format: GraphicFieldFormat::Hex, + mode: GraphicFieldMode::Or, data_bytes: 0, total_bytes: 0, row_bytes: 0, @@ -1057,6 +1073,7 @@ impl ZplParser { magnification_y: 1, reverse_print: self.printer.get_reverse_print(), format: GraphicFieldFormat::Hex, + mode: GraphicFieldMode::Or, data_bytes: 0, total_bytes: 0, row_bytes: 0, diff --git a/src/playground.rs b/src/playground.rs index 592da15..48d3817 100644 --- a/src/playground.rs +++ b/src/playground.rs @@ -302,6 +302,7 @@ pub const PLAYGROUND_HTML: &str = r##" @@ -330,14 +331,14 @@ pub const PLAYGROUND_HTML: &str = r##"
- +