├── .cargo └── config ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── example.toml └── src ├── bitmap.rs ├── commands.rs ├── device.rs ├── lib.rs ├── render ├── display.rs ├── mod.rs └── ops.rs ├── tiff.rs └── util.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.armv7-unknown-linux-gnueabihf] 2 | linker = "arm-linux-gnueabihf-gcc" 3 | 4 | [target.aarch64-unknown-linux-gnu] 5 | linker = "aarch64-linux-gnu-gcc" 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot dependency version checks / updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "cargo" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build-util: 15 | 16 | runs-on: ${{ matrix.os }} 17 | continue-on-error: true 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | include: 23 | - target: x86_64-unknown-linux-gnu 24 | os: ubuntu-latest 25 | output: ptouch-util 26 | args: --no-default-features --features=util 27 | - target: armv7-unknown-linux-gnueabihf 28 | os: ubuntu-latest 29 | output: ptouch-util 30 | apt-arch: armhf 31 | - target: x86_64-apple-darwin 32 | os: macos-latest 33 | output: ptouch-util 34 | - target: x86_64-pc-windows-msvc 35 | os: windows-latest 36 | output: ptouch-util.exe 37 | args: --no-default-features --features=util 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: FranzDiebold/github-env-vars-action@v1.2.1 42 | 43 | - name: Configure toolchain 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: nightly 47 | target: ${{ matrix.target }} 48 | override: true 49 | 50 | - name: Configure caching 51 | uses: actions/cache@v2 52 | # Caching disabled on macos due to https://github.com/actions/cache/issues/403 53 | if: ${{ matrix.os != 'macos-latest' }} 54 | with: 55 | key: ${{ matrix.os }}-${{ matrix.target }} 56 | path: | 57 | ${{ env.HOME }}/.cargo 58 | target 59 | 60 | - name: Install deps (brew) 61 | if: ${{ matrix.os == 'macos-latest' }} 62 | run: brew install libusb sdl2 63 | 64 | - name: Install deps (apt native) 65 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.target == 'x86_64-unknown-linux-gnu' }} 66 | run: sudo apt install -y libusb-dev libusb-1.0-0-dev 67 | 68 | - name: Install deps (foreign architecture) 69 | if: ${{ matrix.apt-arch }} 70 | uses: ryankurte/action-apt@v0.3.0 71 | with: 72 | arch: ${{ matrix.apt-arch }} 73 | packages: libusb-dev:${{ matrix.apt-arch }} libusb-1.0-0-dev:${{ matrix.apt-arch }} libsdl2-dev:${{ matrix.apt-arch }} 74 | 75 | - name: Install cross toolchain (armv7) 76 | if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} 77 | run: sudo apt install gcc-arm-linux-gnueabihf 78 | 79 | - name: Enable cross compilation (armv7) 80 | if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} 81 | run: | 82 | echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV 83 | 84 | - name: Install libusb (vcpkg) 85 | if: ${{ matrix.os == 'windows-latest' }} 86 | run: | 87 | vcpkg integrate install 88 | vcpkg install libusb:x64-windows-static 89 | echo "LIBUSB_DIR=C:/vcpkg/installed/x64-windows-static/" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 90 | 91 | - name: Cache vcpkg 92 | if: ${{ matrix.os == 'windows-latest' }} 93 | uses: actions/cache@v2 94 | with: 95 | key: ${{ matrix.os }}-${{ matrix.target }} 96 | path: $VCPKG_DIRECTORY 97 | 98 | - name: Build release 99 | uses: actions-rs/cargo@v1 100 | with: 101 | use-cross: ${{ matrix.use_cross }} 102 | command: build 103 | args: --target ${{ matrix.target }} --release ${{ matrix.args }} 104 | 105 | - name: Copy / Rename utility 106 | run: | 107 | cp target/${{ matrix.target }}/release/${{ matrix.output }} ${{ matrix.output }}-${{ matrix.target }} 108 | tar -czvf ptouch-util-${{ matrix.target }}.tgz ${{ matrix.output }}-${{ matrix.target }} 109 | 110 | - name: Upload utility artifacts 111 | uses: actions/upload-artifact@v4 112 | with: 113 | name: ${{ matrix.output }}-${{ matrix.target }} 114 | path: ${{ matrix.output }}-${{ matrix.target }} 115 | 116 | - name: Upload utility binary to release 117 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 118 | uses: svenstaro/upload-release-action@v2 119 | with: 120 | repo_token: ${{ secrets.GITHUB_TOKEN }} 121 | file: ptouch-util-${{ matrix.target }}.tgz 122 | asset_name: ptouch-util-${{ matrix.target }}.tgz 123 | tag: ${{ github.ref }} 124 | overwrite: true 125 | 126 | release: 127 | name: Create release 128 | runs-on: ubuntu-latest 129 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 130 | steps: 131 | 132 | - name: Create Release 133 | uses: actions/create-release@v1 134 | id: create_release 135 | env: 136 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | with: 138 | tag_name: ${{ github.ref }} 139 | release_name: Release ${{ github.ref }} 140 | body: Release ${{ github.ref }} 141 | 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ptouch" 3 | repository = "https://github.com/ryankurte/rust-ptouch" 4 | description = "Brother PTouch label maker driver and utility" 5 | keywords = [ "ptouch", "label", "print", "driver" ] 6 | readme = "README.md" 7 | version = "0.2.2" 8 | authors = ["ryan "] 9 | edition = "2018" 10 | license = "MPL-2.0" 11 | 12 | [features] 13 | util = [ "toml", "structopt", "strum", "serde" ] 14 | preview = [ "embedded-graphics-simulator" ] 15 | default = [ "util", "preview" ] 16 | 17 | [dependencies] 18 | structopt = { version = "0.3.21", optional = true } 19 | rusb = "0.9.1" 20 | lazy_static = "1.4.0" 21 | log = "0.4.13" 22 | bitfield = "0.14.0" 23 | bitflags = "1.2.1" 24 | strum = { version = "0.24.0", optional = true } 25 | strum_macros = "0.24.3" 26 | anyhow = "1.0.38" 27 | 28 | simplelog = "0.12.0" 29 | qrcode = "0.12.0" 30 | datamatrix = "0.3.1" 31 | image = "0.23.14" 32 | barcoders = "1.0.2" 33 | 34 | thiserror = "1.0.23" 35 | tempdir = "0.3.7" 36 | 37 | embedded-graphics = "0.6.2" 38 | embedded-text = "0.4.0" 39 | # TODO: make preview optional 40 | embedded-graphics-simulator = { version = "0.2.0", optional = true } 41 | 42 | serde = { version = "1.0.123", features = [ "derive" ], optional = true } 43 | bitvec = "1.0.1" 44 | toml = { version = "0.5.8", optional = true } 45 | 46 | [[bin]] 47 | name = "ptouch-util" 48 | path = "src/util.rs" 49 | required-features = [ "util" ] 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brother P-Touch Raster Driver (and utility) 2 | 3 | Brother P-Touch Label-Maker Raster Driver for `PT-E550W/P750W/P710BT` devices. 4 | 5 | 6 | ## Status 7 | 8 | ***Extremely alpha, tested only on the `PT-P710BT`, API subject to change*** 9 | 10 | [![GitHub tag](https://img.shields.io/github/tag/ryankurte/rust-ptouch.svg)](https://github.com/ryankurte/rust-ptouch) 11 | ![Build Status](https://github.com/ryankurte/rust-ptouch/workflows/Rust/badge.svg) 12 | [![Crates.io](https://img.shields.io/crates/v/ptouch.svg)](https://crates.io/crates/ptouch) 13 | [![Docs.rs](https://docs.rs/ptouch/badge.svg)](https://docs.rs/ptouch) 14 | 15 | 16 | ## Usage 17 | 18 | ### Utility 19 | 20 | Install with `cargo install ptouch` or grab the latest release [here](https://github.com/ryankurte/rust-ptouch/releases/latest). 21 | 22 | The utility supports a set of basic subcommands: 23 | 24 | - `ptouch-util [SUBCOMMAND] --help` to show help options 25 | - `ptouch-util [--media MEDIA] render --file=[OUTPUT] [OPTIONS]` to render to an `OUTPUT` image file 26 | - `ptouch-util [--media MEDIA] preview [OPTIONS]` to render to a preview window (not available on all platforms) 27 | - `ptouch-util print [OPTIONS]` to print 28 | 29 | The `--media` argument sets the default media type when the printer is unavailable, otherwise this is loaded from the printer. 30 | 31 | Each of `render`, `preview`, and `print` take a set of `[OPTIONS]` to configure the output, these options are: 32 | 33 | - `text VALUE [--font=FONT]` to render text in the specified font, use `\n` for newlines 34 | - `qr CODE` to render a QRCode with the provided value 35 | - `qr-text CODE VALUE [--font=FONT]` to render a QRCode followed by text 36 | - `image FILE` to render an image directly 37 | - `template FILE` to load a `.toml` render template (see [example.toml](example.toml)) 38 | - `barcode CODE` to render a barcode (experimental, missing config options) 39 | 40 | These CLI options are a subset of those available using the library intended to provide the basics. If you think there's something missing, feel free to open an issue / PR! 41 | 42 | 43 | ### API 44 | 45 | This needs cleaning up before it's _reasonable_ to use... for usage see [src/util.rs](src/util.rs). 46 | 47 | ### Examples 48 | 49 | ``` 50 | ptouch-util --media tze24mm preview qr-text \ 51 | 'https://github.com/ryankurte/rust-ptouch' \ 52 | 'Rust PTouch Driver\n@ryankurte' --font=24x32` 53 | ``` 54 | 55 | ![image](https://user-images.githubusercontent.com/860620/111896515-0c7e1000-8a7f-11eb-95e6-af5f7b18a1ae.png) 56 | 57 | ``` 58 | ptouch-util print qr-text \ 59 | "https://github.com/ryankurte/rust-ptouch" \ 60 | "Rust PTouch Driver\n@ryankurte" --font=24x32 61 | ``` 62 | ![IMG_1840](https://user-images.githubusercontent.com/860620/111896577-9201c000-8a7f-11eb-9c5f-a5041dba9236.jpg) 63 | 64 | 65 | ## Resources 66 | 67 | - [PT-P710BT Manual](https://support.brother.com/g/b/manualtop.aspx?c=eu_ot&lang=en&prod=p710bteuk) 68 | - [Brother Raster Command Reference](https://download.brother.com/welcome/docp100064/cv_pte550wp750wp710bt_eng_raster_101.pdf) 69 | - [Pytouch Cube python driver](https://github.com/piksel/pytouch-cube) 70 | -------------------------------------------------------------------------------- /example.toml: -------------------------------------------------------------------------------- 1 | 2 | [[ops]] 3 | kind = "pad" 4 | count = 16 5 | 6 | [[ops]] 7 | kind = "qr" 8 | code = "https://github.com/ryankurte/rust-ptouch" 9 | 10 | [[ops]] 11 | kind = "text" 12 | text = "Rust PTouch Driver Library\n@ryankurte" 13 | font = "font12x16" 14 | 15 | [[ops]] 16 | kind = "pad" 17 | count = 16 -------------------------------------------------------------------------------- /src/bitmap.rs: -------------------------------------------------------------------------------- 1 | //! Bitmap helper for PTouch raster encoding 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | /// Bitmap helper for encoding raster data. 8 | /// This internally manages offsets and byte ordering for printing. 9 | pub struct Bitmap { 10 | offset: usize, 11 | width: usize, 12 | data: Vec<[u8; 16]>, 13 | } 14 | 15 | impl Bitmap { 16 | /// Create a new bitmap object with the provided raster line offset / width 17 | pub fn new(offset: usize, width: usize) -> Self { 18 | Self { 19 | offset, 20 | width, 21 | data: vec![] 22 | } 23 | } 24 | 25 | /// Add a raster line 26 | pub fn raster_line(&mut self, line: &[bool]) { 27 | let mut e = [0u8; 16]; 28 | 29 | if line.len() > self.width as usize { 30 | panic!("Line width exceeds renderable width"); 31 | } 32 | 33 | for i in 0..line.len() { 34 | // Skip unset pixels 35 | if !line[i] { 36 | continue; 37 | } 38 | 39 | let offset_index = self.offset + i; 40 | 41 | // Set pixels, not the reverse bit-order within the byte 42 | e[offset_index / 8] |= 1 << (7 - (offset_index % 8)); 43 | } 44 | 45 | self.data.push(e); 46 | } 47 | 48 | // Fetch encoded lines for printing 49 | pub fn data(&self) -> Vec<[u8; 16]> { 50 | self.data.clone() 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | //! PTouch low-level command API 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | use std::time::Duration; 8 | 9 | use log::{trace, debug}; 10 | 11 | use crate::{Error, PTouch, device::Status}; 12 | use crate::device::{AdvancedMode, Mode, PrintInfo, VariousMode, CompressionMode}; 13 | 14 | /// Raw command API for the PTouch device. 15 | /// This provides low-level access to the device (if desired) 16 | pub trait Commands { 17 | /// Null command 18 | fn null(&mut self) -> Result<(), Error>; 19 | 20 | /// Init command, sets up the device for printing 21 | fn init(&mut self) -> Result<(), Error>; 22 | 23 | /// Invalidate command, resets the device 24 | fn invalidate(&mut self) -> Result<(), Error>; 25 | 26 | /// Issue a status request 27 | fn status_req(&mut self) -> Result<(), Error>; 28 | 29 | /// Read a status response with the provided timeout 30 | fn read_status(&mut self, timeout: Duration) -> Result; 31 | 32 | /// Switch mode, required for raster printing 33 | fn switch_mode(&mut self, mode: Mode) -> Result<(), Error>; 34 | 35 | /// Set status notify (printer automatically sends status on change) 36 | fn set_status_notify(&mut self, enabled: bool) -> Result<(), Error>; 37 | 38 | /// Set print information 39 | fn set_print_info(&mut self, info: &PrintInfo) -> Result<(), Error>; 40 | 41 | /// Set various mode flags 42 | fn set_various_mode(&mut self, mode: VariousMode) -> Result<(), Error>; 43 | 44 | /// Set advanced mode flags 45 | fn set_advanced_mode(&mut self, mode: AdvancedMode) -> Result<(), Error>; 46 | 47 | /// Set pre/post print margin 48 | fn set_margin(&mut self, dots: u16) -> Result<(), Error>; 49 | 50 | /// Set print page number 51 | fn set_page_no(&mut self, no: u8) -> Result<(), Error>; 52 | 53 | /// Set compression mode (None or Tiff). 54 | /// Note TIFF mode is currently... broken 55 | fn set_compression_mode(&mut self, mode: CompressionMode) -> Result<(), Error>; 56 | 57 | /// Transfer raster data 58 | fn raster_transfer(&mut self, data: &[u8]) -> Result<(), Error>; 59 | 60 | /// Send a zero raster line 61 | fn raster_zero(&mut self) -> Result<(), Error>; 62 | 63 | /// Start a print 64 | fn print(&mut self) -> Result<(), Error>; 65 | 66 | /// Start a print and feed 67 | fn print_and_feed(&mut self) -> Result<(), Error>; 68 | } 69 | 70 | /// Low-level command API implementation 71 | impl Commands for PTouch { 72 | fn null(&mut self) -> Result<(), Error> { 73 | self.write(&[0x00], self.timeout) 74 | } 75 | 76 | fn init(&mut self) -> Result<(), Error> { 77 | self.write(&[0x1b, 0x40], self.timeout) 78 | } 79 | 80 | fn invalidate(&mut self) -> Result<(), Error> { 81 | self.write(&[0u8; 100], self.timeout) 82 | } 83 | 84 | fn status_req(&mut self) -> Result<(), Error> { 85 | self.write(&[0x1b, 0x69, 0x53], self.timeout) 86 | } 87 | 88 | fn read_status(&mut self, timeout: Duration) -> Result { 89 | let status_raw = self.read(timeout)?; 90 | 91 | let status = Status::from(status_raw); 92 | 93 | debug!("Status: {:?}", status); 94 | trace!("Raw status: {:?}", &status_raw); 95 | 96 | Ok(status) 97 | } 98 | 99 | fn switch_mode(&mut self, mode: Mode) -> Result<(), Error> { 100 | self.write(&[0x1b, 0x69, 0x61, mode as u8], self.timeout) 101 | } 102 | 103 | fn set_status_notify(&mut self, enabled: bool) -> Result<(), Error> { 104 | let en = match enabled { 105 | true => 0, 106 | false => 1, 107 | }; 108 | 109 | self.write(&[0x1b, 0x69, 0x21, en], self.timeout) 110 | } 111 | 112 | fn set_print_info(&mut self, info: &PrintInfo) -> Result<(), Error> { 113 | let mut buff = [0u8; 13]; 114 | 115 | debug!("Set print info: {:?}", info); 116 | 117 | // Command header 118 | buff[0] = 0x1b; 119 | buff[1] = 0x69; 120 | buff[2] = 0x7a; 121 | 122 | if let Some(i) = &info.kind { 123 | buff[3] |= 0x02; 124 | buff[4] = *i as u8; 125 | } 126 | 127 | if let Some(w) = &info.width { 128 | buff[3] |= 0x04; 129 | buff[5] = *w as u8; 130 | } 131 | 132 | if let Some(l) = &info.length { 133 | buff[3] |= 0x08; 134 | buff[6] = *l as u8; 135 | } 136 | 137 | let raster_bytes = info.raster_no.to_le_bytes(); 138 | buff[7..11].copy_from_slice(&raster_bytes); 139 | 140 | if info.recover { 141 | buff[3] |= 0x80; 142 | } 143 | 144 | self.write(&buff, self.timeout) 145 | } 146 | 147 | fn set_various_mode(&mut self, mode: VariousMode) -> Result<(), Error> { 148 | debug!("Set various mode: {:?}", mode); 149 | 150 | self.write(&[0x1b, 0x69, 0x4d, mode.bits()], self.timeout) 151 | } 152 | 153 | fn set_advanced_mode(&mut self, mode: AdvancedMode) -> Result<(), Error> { 154 | debug!("Set advanced mode: {:?}", mode); 155 | 156 | self.write(&[0x1b, 0x69, 0x4b, mode.bits()], self.timeout) 157 | } 158 | 159 | fn set_margin(&mut self, dots: u16) -> Result<(), Error> { 160 | debug!("Set margin: {:?}", dots); 161 | 162 | self.write( 163 | &[0x1b, 0x69, 0x64, dots as u8, (dots >> 8) as u8], 164 | self.timeout, 165 | ) 166 | } 167 | 168 | fn set_page_no(&mut self, no: u8) -> Result<(), Error> { 169 | debug!("Set page no: {:?}", no); 170 | 171 | self.write(&[0x1b, 0x69, 0x41, no], self.timeout) 172 | } 173 | 174 | fn set_compression_mode(&mut self, mode: CompressionMode) -> Result<(), Error> { 175 | debug!("Set compression mode: {:?}", mode); 176 | 177 | self.write(&[0x4D, mode as u8], self.timeout) 178 | } 179 | 180 | fn raster_transfer(&mut self, data: &[u8]) -> Result<(), Error> { 181 | let mut buff = vec![0u8; data.len() + 3]; 182 | 183 | buff[0] = 0x47; 184 | buff[1] = (data.len() & 0xFF) as u8; 185 | buff[2] = (data.len() >> 8) as u8; 186 | 187 | (&mut buff[3..3+data.len()]).copy_from_slice(data); 188 | 189 | trace!("Raster transfer: {:02x?}", &buff[..3+data.len()]); 190 | 191 | self.write(&buff[..3+data.len()], self.timeout) 192 | } 193 | 194 | fn raster_zero(&mut self) -> Result<(), Error> { 195 | debug!("Raster zero line"); 196 | 197 | self.write(&[0x5a], self.timeout) 198 | } 199 | 200 | fn print(&mut self) -> Result<(), Error> { 201 | debug!("Print command"); 202 | self.write(&[0x0c], self.timeout) 203 | } 204 | 205 | fn print_and_feed(&mut self) -> Result<(), Error> { 206 | debug!("Print feed command"); 207 | self.write(&[0x1a], self.timeout) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/device.rs: -------------------------------------------------------------------------------- 1 | //! PTouch printer device definitions 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | use crate::Error; 8 | 9 | use bitflags::bitflags; 10 | 11 | #[cfg(feature = "strum")] 12 | use strum_macros::{Display, EnumString, EnumVariantNames}; 13 | 14 | bitflags::bitflags! { 15 | /// First error byte 16 | pub struct Error1: u8 { 17 | const NO_MEDIA = 0x01; 18 | const CUTTER_JAM = 0x04; 19 | const WEAK_BATT = 0x08; 20 | const HIGH_VOLT = 0x40; 21 | } 22 | } 23 | 24 | bitflags::bitflags! { 25 | /// Second device error type 26 | pub struct Error2: u8 { 27 | const WRONG_MEDIA = 0x01; 28 | const COVER_OPEN = 0x10; 29 | const OVERHEAT = 0x20; 30 | } 31 | } 32 | 33 | /// PTouch device type. 34 | /// Note that only the p710bt has been tested 35 | #[derive(Copy, Clone, PartialEq, Debug)] 36 | #[cfg_attr(feature = "strum", derive(Display, EnumString, EnumVariantNames))] 37 | #[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] 38 | pub enum PTouchDevice { 39 | #[cfg_attr(feature = "strum", strum(serialize = "pt-e550w"))] 40 | PtE550W = 0x2060, 41 | #[cfg_attr(feature = "strum", strum(serialize = "pt-e560bt"))] 42 | PtE560BT = 0x2203, 43 | #[cfg_attr(feature = "strum", strum(serialize = "pt-p750w"))] 44 | PtP750W = 0x2062, 45 | #[cfg_attr(feature = "strum", strum(serialize = "pt-p710bt"))] 46 | PtP710Bt = 0x20af, 47 | #[cfg_attr(feature = "strum", strum(serialize = "pt-d600"))] 48 | PtD600 = 0x2074, 49 | } 50 | 51 | 52 | /// Media width encoding for Status message 53 | #[derive(Copy, Clone, PartialEq, Debug)] 54 | #[cfg_attr(feature = "strum", derive(Display, EnumString, EnumVariantNames))] 55 | #[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] 56 | pub enum Media { 57 | /// 6mm TZe Tape 58 | Tze6mm = 257, 59 | /// 9mm TZe Tape 60 | Tze9mm = 258, 61 | /// 12mm TZe Tape 62 | Tze12mm = 259, 63 | /// 18mm TZe Tape 64 | Tze18mm = 260, 65 | /// 24mm TZe Tape 66 | Tze24mm = 261, 67 | 68 | /// 6mm HeatShrink Tube 69 | Hs6mm = 415, 70 | /// 9mm HeatShrink Tube 71 | Hs9mm = 416, 72 | /// 12mm HeatShrink Tube 73 | Hs12mm = 417, 74 | /// 18mm HeatShrink Tube 75 | Hs18mm = 418, 76 | /// 24mm HeatShrink Tube 77 | Hs24mm = 419, 78 | 79 | /// Unknown media width 80 | Unknown = 0xFFFF, 81 | } 82 | 83 | /// Generate a MediaWidth from provided MediaKind and u8 width 84 | impl From<(MediaKind, u8)> for Media { 85 | fn from(v: (MediaKind, u8)) -> Self { 86 | use MediaKind::*; 87 | use Media::*; 88 | 89 | match v { 90 | (LaminatedTape, 6) | (NonLaminatedTape, 6) | (FlexibleTape, 6) => Tze6mm, 91 | (LaminatedTape, 9) | (NonLaminatedTape, 9) | (FlexibleTape, 9) => Tze9mm, 92 | (LaminatedTape, 12) | (NonLaminatedTape, 12) | (FlexibleTape, 12) => Tze12mm, 93 | (LaminatedTape, 18) | (NonLaminatedTape, 18) | (FlexibleTape, 18) => Tze18mm, 94 | (LaminatedTape, 24) | (NonLaminatedTape, 24) | (FlexibleTape, 24) => Tze24mm, 95 | (HeatShrinkTube, 6) => Hs6mm, 96 | (HeatShrinkTube, 9) => Hs9mm, 97 | (HeatShrinkTube, 12) => Hs12mm, 98 | (HeatShrinkTube, 18) => Hs18mm, 99 | (HeatShrinkTube, 24) => Hs24mm, 100 | _ => Unknown, 101 | } 102 | } 103 | } 104 | 105 | impl Media { 106 | /// Fetch media print area (left margin, print area, right margin) 107 | pub fn area(&self) -> (usize, usize, usize) { 108 | use Media::*; 109 | 110 | match self { 111 | Tze6mm => (52, 32, 52), 112 | Tze9mm => (39, 50, 39), 113 | Tze12mm => (29, 70, 29), 114 | Tze18mm => (8, 112, 8), 115 | Tze24mm => (0, 128, 0), 116 | 117 | Hs6mm => (50, 28, 50), 118 | Hs9mm => (40, 48, 40), 119 | Hs12mm => (31, 66, 31), 120 | Hs18mm => (11, 106, 11), 121 | Hs24mm => (0, 128, 0), 122 | 123 | Unknown => (0, 0, 0) 124 | } 125 | } 126 | 127 | /// Check if a media type is _tape_ 128 | pub fn is_tape(&self) -> bool { 129 | use Media::*; 130 | 131 | match self { 132 | Tze6mm | Tze9mm | Tze12mm | Tze18mm | Tze24mm => true, 133 | _ => false, 134 | } 135 | } 136 | 137 | /// Fetch the (approximate) media width in mm 138 | pub fn width(&self) -> usize { 139 | use Media::*; 140 | 141 | match self { 142 | Tze6mm => 6, 143 | Tze9mm => 9, 144 | Tze12mm => 12, 145 | Tze18mm => 18, 146 | Tze24mm => 24, 147 | Hs6mm => 6, 148 | Hs9mm => 9, 149 | Hs12mm => 12, 150 | Hs18mm => 18, 151 | Hs24mm => 24, 152 | _ => panic!("Unknown media width"), 153 | } 154 | } 155 | } 156 | 157 | /// Kind of media loaded in printer 158 | #[derive(Copy, Clone, PartialEq, Debug)] 159 | pub enum MediaKind { 160 | None = 0x00, 161 | LaminatedTape = 0x01, 162 | NonLaminatedTape = 0x03, 163 | HeatShrinkTube = 0x11, 164 | FlexibleTape = 0x14, 165 | IncompatibleTape = 0xFF, 166 | } 167 | 168 | /// Device operating phase 169 | #[derive(Copy, Clone, PartialEq, Debug)] 170 | pub enum Phase { 171 | Editing, 172 | Printing, 173 | Unknown, 174 | } 175 | 176 | impl From for Phase { 177 | fn from(v: u8) -> Self { 178 | use Phase::*; 179 | 180 | match v { 181 | 0 => Editing, 182 | 1 => Printing, 183 | _ => Unknown 184 | } 185 | } 186 | } 187 | 188 | /// Create media kind from status values 189 | impl From for MediaKind { 190 | fn from(v: u8) -> Self { 191 | match v { 192 | 0x00 => MediaKind::None, 193 | 0x01 => MediaKind::LaminatedTape, 194 | 0x03 => MediaKind::NonLaminatedTape, 195 | 0x11 => MediaKind::HeatShrinkTube, 196 | 0x14 => MediaKind::FlexibleTape, 197 | 0xFF => MediaKind::IncompatibleTape, 198 | _ => MediaKind::IncompatibleTape, 199 | } 200 | } 201 | } 202 | 203 | /// Device state enumeration 204 | #[derive(Copy, Clone, PartialEq, Debug)] 205 | pub enum DeviceStatus { 206 | Reply = 0x00, 207 | Completed = 0x01, 208 | Error = 0x02, 209 | ExitIF = 0x03, 210 | TurnedOff = 0x04, 211 | Notification = 0x05, 212 | PhaseChange = 0x06, 213 | 214 | Unknown = 0xFF, 215 | } 216 | 217 | impl From for DeviceStatus { 218 | fn from(v: u8) -> Self { 219 | use DeviceStatus::*; 220 | 221 | match v { 222 | 0x00 => Reply, 223 | 0x01 => Completed, 224 | 0x02 => Error, 225 | 0x03 => ExitIF, 226 | 0x04 => TurnedOff, 227 | 0x05 => Notification, 228 | 0x06 => PhaseChange, 229 | _ => Unknown, 230 | } 231 | } 232 | } 233 | 234 | /// Device mode for set_mode command 235 | #[derive(Copy, Clone, PartialEq, Debug)] 236 | pub enum Mode { 237 | /// Not sure tbqh? 238 | EscP = 0x00, 239 | /// Raster mode, what this driver uses 240 | Raster = 0x01, 241 | /// Note PTouchTemplate is not available on most devices 242 | PTouchTemplate = 0x03, 243 | } 244 | 245 | bitflags! { 246 | /// Various mode flags 247 | pub struct VariousMode: u8 { 248 | const AUTO_CUT = (1 << 6); 249 | const MIRROR = (1 << 7); 250 | } 251 | } 252 | 253 | bitflags! { 254 | /// Advanced mode flags 255 | pub struct AdvancedMode: u8 { 256 | const HALF_CUT = (1 << 2); 257 | const NO_CHAIN = (1 << 3); 258 | const SPECIAL_TAPE = (1 << 4); 259 | const HIGH_RES = (1 << 6); 260 | const NO_BUFF_CLEAR = (1 << 7); 261 | } 262 | } 263 | 264 | /// Notification enumerations 265 | #[derive(Copy, Clone, PartialEq, Debug)] 266 | pub enum Notification { 267 | NotAvailable = 0x00, 268 | CoverOpen = 0x01, 269 | CoverClosed = 0x02, 270 | } 271 | 272 | /// Tape colour enumerations 273 | #[derive(Copy, Clone, PartialEq, Debug)] 274 | pub enum TapeColour { 275 | White = 0x01, 276 | Other = 0x02, 277 | ClearBlack = 0x03, 278 | Red = 0x04, 279 | Blue = 0x05, 280 | Black = 0x08, 281 | ClearWhite = 0x09, 282 | MatteWhite = 0x20, 283 | MatteClear = 0x21, 284 | MatteSilver = 0x22, 285 | SatinGold = 0x23, 286 | SatinSilver = 0x24, 287 | BlueD = 0x30, 288 | RedD = 0x31, 289 | FluroOrange=0x40, 290 | FluroYellow=0x41, 291 | BerryPinkS = 0x50, 292 | LightGrayS = 0x51, 293 | LimeGreenS = 0x52, 294 | YellowF = 0x60, 295 | PinkF = 0x61, 296 | BlueF = 0x62, 297 | WhiteHst = 0x70, 298 | WhiteFlexId = 0x90, 299 | YellowFlexId = 0x91, 300 | Cleaning = 0xF0, 301 | Stencil = 0xF1, 302 | Incompatible = 0xFF, 303 | } 304 | 305 | impl From for TapeColour { 306 | fn from(v: u8) -> TapeColour { 307 | use TapeColour::*; 308 | 309 | match v { 310 | 0x01 => White, 311 | 0x02 => Other, 312 | 0x03 => ClearBlack, 313 | 0x04 => Red, 314 | 0x05 => Blue, 315 | 0x08 => Black, 316 | 0x09 => ClearWhite, 317 | 0x20 => MatteWhite, 318 | 0x21 => MatteClear, 319 | 0x22 => MatteSilver, 320 | 0x23 => SatinGold, 321 | 0x24 => SatinSilver, 322 | 0x30 => BlueD, 323 | 0x31 => RedD, 324 | 0x40 => FluroOrange, 325 | 0x41 => FluroYellow, 326 | 0x50 => BerryPinkS, 327 | 0x51 => LightGrayS, 328 | 0x52 => LimeGreenS, 329 | 0x60 => YellowF, 330 | 0x61 => PinkF, 331 | 0x62 => BlueF, 332 | 0x70 => WhiteHst, 333 | 0x90 => WhiteFlexId, 334 | 0x91 => YellowFlexId, 335 | 0xF0 => Cleaning, 336 | 0xF1 => Stencil, 337 | 0xFF | _ => Incompatible, 338 | } 339 | } 340 | } 341 | 342 | /// Text colour enumerations 343 | #[derive(Copy, Clone, PartialEq, Debug)] 344 | pub enum TextColour { 345 | White = 0x01, 346 | Red = 0x04, 347 | Blue = 0x05, 348 | Black = 0x08, 349 | Gold = 0x0A, 350 | BlueF = 0x62, 351 | Cleaning = 0xf0, 352 | Stencil = 0xF1, 353 | Other = 0x02, 354 | Incompatible = 0xFF, 355 | } 356 | 357 | impl From for TextColour { 358 | fn from(v: u8) -> TextColour { 359 | use TextColour::*; 360 | 361 | match v { 362 | 0x01 => White, 363 | 0x04 => Red, 364 | 0x05 => Blue, 365 | 0x08 => Black, 366 | 0x0A => Gold, 367 | 0x62 => BlueF, 368 | 0xf0 => Cleaning, 369 | 0xF1 => Stencil, 370 | 0x02 => Other, 371 | 0xFF | _=> Incompatible, 372 | } 373 | } 374 | } 375 | 376 | /// Device status message 377 | #[derive(Clone, PartialEq, Debug)] 378 | pub struct Status { 379 | pub model: u8, 380 | 381 | pub error1: Error1, 382 | pub error2: Error2, 383 | 384 | pub media_width: u8, 385 | pub media_kind: MediaKind, 386 | 387 | pub status_type: DeviceStatus, 388 | pub phase: Phase, 389 | 390 | pub tape_colour: TapeColour, 391 | pub text_colour: TextColour, 392 | } 393 | 394 | impl Status { 395 | // This function is gonna be called if the --no-status-fetch flag is enabled. 396 | // It returns a default status, which is assumed to be correct to then print. 397 | pub fn new(media: &Media) -> Result { 398 | Ok(Status { 399 | model: 0, // The model is not that important, and also the manual only shows the model ID of E550W and E750W 400 | error1: Error1::empty(), // Assuming there's no error 401 | error2: Error2::empty(), // Assuming there's no error 402 | media_width: media.width() as u8, // Width given by user in command 403 | media_kind: match media.is_tape() { // Not sure if this is really important, but this is an easy way to detect if it is tape (can't know if laminated or not) or not 404 | true => MediaKind::LaminatedTape, 405 | false => MediaKind::HeatShrinkTube 406 | }, 407 | status_type: DeviceStatus::Completed, // Assuming the printer is ready to print 408 | phase: Phase::Editing, // Assuming the printer is not printing 409 | tape_colour: TapeColour::White, // By default, assuming the tape is white... 410 | text_colour: TextColour::Black, // ...and the text colour is black. Would maybe be good to let the user change it in the command 411 | }) 412 | } 413 | } 414 | 415 | impl From<[u8; 32]> for Status { 416 | 417 | fn from(r: [u8; 32]) -> Self { 418 | Self { 419 | model: r[0], 420 | error1: Error1::from_bits_truncate(r[8]), 421 | error2: Error2::from_bits_truncate(r[9]), 422 | media_width: r[10], 423 | media_kind: MediaKind::from(r[11]), 424 | 425 | status_type: DeviceStatus::from(r[18]), 426 | phase: Phase::from(r[20]), 427 | tape_colour: TapeColour::from(r[24]), 428 | text_colour: TextColour::from(r[25]), 429 | } 430 | } 431 | } 432 | 433 | /// Print information command 434 | #[derive(Clone, PartialEq, Debug)] 435 | pub struct PrintInfo { 436 | /// Media kind 437 | pub kind: Option, 438 | /// Tape width in mm 439 | pub width: Option, 440 | /// Tape length, always set to 0 441 | pub length: Option, 442 | /// Raster number (??) 443 | pub raster_no: u32, 444 | /// Enable print recovery 445 | pub recover: bool, 446 | } 447 | 448 | impl Default for PrintInfo { 449 | fn default() -> Self { 450 | Self { 451 | kind: None, 452 | width: None, 453 | length: Some(0), 454 | raster_no: 0, 455 | recover: true, 456 | } 457 | } 458 | } 459 | 460 | /// Compression mode enumeration 461 | #[derive(Copy, Clone, PartialEq, Debug)] 462 | pub enum CompressionMode { 463 | None = 0x00, 464 | Tiff = 0x02, 465 | } 466 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Rust PTouch Driver / Utility 2 | // 3 | // https://github.com/ryankurte/rust-ptouch 4 | // Copyright 2021 Ryan Kurte 5 | 6 | use std::time::Duration; 7 | 8 | use commands::Commands; 9 | use device::Status; 10 | use image::ImageError; 11 | use log::{trace, debug, error}; 12 | 13 | #[cfg(feature = "structopt")] 14 | use structopt::StructOpt; 15 | 16 | #[cfg(feature = "strum")] 17 | use strum::VariantNames; 18 | 19 | use rusb::{Context, Device, DeviceDescriptor, DeviceHandle, Direction, TransferType, UsbContext}; 20 | 21 | pub mod device; 22 | use device::*; 23 | 24 | pub mod commands; 25 | 26 | pub mod bitmap; 27 | 28 | pub mod tiff; 29 | 30 | pub mod render; 31 | 32 | /// PTouch device instance 33 | pub struct PTouch { 34 | _device: Device, 35 | handle: DeviceHandle, 36 | descriptor: DeviceDescriptor, 37 | //endpoints: Endpoints, 38 | timeout: Duration, 39 | 40 | cmd_ep: u8, 41 | stat_ep: u8, 42 | } 43 | 44 | /// Brother USB Vendor ID 45 | pub const BROTHER_VID: u16 = 0x04F9; 46 | 47 | /// Options for connecting to a PTouch device 48 | #[derive(Clone, PartialEq, Debug)] 49 | #[cfg_attr(feature = "structopt", derive(StructOpt))] 50 | pub struct Options { 51 | #[cfg_attr(feature = "structopt", structopt(long, possible_values = &device::PTouchDevice::VARIANTS, default_value = "pt-p710bt"))] 52 | /// Label maker device kind 53 | pub device: device::PTouchDevice, 54 | 55 | #[cfg_attr(feature = "structopt", structopt(long, default_value = "0"))] 56 | /// Index (if multiple devices are connected) 57 | pub index: usize, 58 | 59 | #[cfg_attr(feature = "structopt", structopt(long, default_value = "500"))] 60 | /// Timeout to pass to the read_bulk and write_bulk methods 61 | pub timeout_milliseconds: u64, 62 | 63 | #[cfg_attr(feature = "structopt", structopt(long, hidden = true))] 64 | /// Do not reset the device on connect 65 | pub no_reset: bool, 66 | 67 | #[cfg_attr(feature = "structopt", structopt(long, hidden = true))] 68 | /// (DEBUG) Do not claim USB interface on connect 69 | pub usb_no_claim: bool, 70 | 71 | #[cfg_attr(feature = "structopt", structopt(long, hidden = true))] 72 | /// (DEBUG) Do not detach from kernel drivers on connect 73 | pub usb_no_detach: bool, 74 | 75 | #[cfg_attr(feature = "structopt", structopt(long))] 76 | /// If true, the program will not perform a status request 77 | pub no_status_fetch: bool, 78 | } 79 | 80 | // Lazy initialised libusb context 81 | lazy_static::lazy_static! { 82 | static ref CONTEXT: Context = { 83 | Context::new().unwrap() 84 | }; 85 | } 86 | 87 | /// PTouch API errors 88 | #[derive(thiserror::Error, Debug)] 89 | pub enum Error { 90 | #[error("USB error: {:?}", 0)] 91 | Usb(rusb::Error), 92 | 93 | #[error("IO error: {:?}", 0)] 94 | Io(std::io::Error), 95 | 96 | #[error("Image error: {:?}", 0)] 97 | Image(ImageError), 98 | 99 | #[error("Invalid device index")] 100 | InvalidIndex, 101 | 102 | #[error("No supported languages")] 103 | NoLanguages, 104 | 105 | #[error("Unable to locate expected endpoints")] 106 | InvalidEndpoints, 107 | 108 | #[error("Renderer error")] 109 | Render, 110 | 111 | #[error("Operation timeout")] 112 | Timeout, 113 | 114 | #[error("PTouch Error ({:?} {:?})", 0, 1)] 115 | PTouch(Error1, Error2), 116 | } 117 | 118 | impl From for Error { 119 | fn from(e: std::io::Error) -> Self { 120 | Error::Io(e) 121 | } 122 | } 123 | 124 | impl From for Error { 125 | fn from(e: rusb::Error) -> Self { 126 | Error::Usb(e) 127 | } 128 | } 129 | 130 | impl From for Error { 131 | fn from(e: ImageError) -> Self { 132 | Error::Image(e) 133 | } 134 | } 135 | 136 | /// PTouch device information 137 | #[derive(Clone, Debug, PartialEq)] 138 | pub struct Info { 139 | pub manufacturer: String, 140 | pub product: String, 141 | pub serial: String, 142 | } 143 | 144 | impl PTouch { 145 | /// Create a new PTouch driver with the provided USB options 146 | pub fn new(o: &Options) -> Result { 147 | Self::new_with_context(o, &CONTEXT) 148 | } 149 | 150 | /// Create a new PTouch driver with the provided USB options and an existing rusb::Context 151 | pub fn new_with_context(o: &Options, context: &Context) -> Result { 152 | // List available devices 153 | let devices = context.devices()?; 154 | 155 | // Find matching VID/PIDs 156 | let mut matches: Vec<_> = devices 157 | .iter() 158 | .filter_map(|d| { 159 | // Fetch device descriptor 160 | let desc = match d.device_descriptor() { 161 | Ok(d) => d, 162 | Err(e) => { 163 | debug!("Could not fetch descriptor for device {:?}: {:?}", d, e); 164 | return None; 165 | } 166 | }; 167 | 168 | // Return devices matching vid/pid filters 169 | if desc.vendor_id() == BROTHER_VID && desc.product_id() == o.device as u16 { 170 | Some((d, desc)) 171 | } else { 172 | None 173 | } 174 | }) 175 | .collect(); 176 | 177 | // Check index is valid 178 | if matches.len() < o.index || matches.len() == 0 { 179 | debug!( 180 | "Device index ({}) exceeds number of discovered devices ({})", 181 | o.index, 182 | matches.len() 183 | ); 184 | return Err(Error::InvalidIndex); 185 | } 186 | 187 | debug!("Found matching devices: {:?}", matches); 188 | 189 | // Fetch matching device 190 | let (device, descriptor) = matches.remove(o.index); 191 | 192 | // Open device handle 193 | let mut handle = match device.open() { 194 | Ok(v) => v, 195 | Err(e) => { 196 | debug!("Error opening device"); 197 | return Err(e.into()); 198 | } 199 | }; 200 | 201 | // Reset device 202 | if let Err(e) = handle.reset() { 203 | debug!("Error resetting device handle"); 204 | return Err(e.into()) 205 | } 206 | 207 | // Locate endpoints 208 | let config_desc = match device.config_descriptor(0) { 209 | Ok(v) => v, 210 | Err(e) => { 211 | debug!("Failed to fetch config descriptor"); 212 | return Err(e.into()); 213 | } 214 | }; 215 | 216 | let interface = match config_desc.interfaces().next() { 217 | Some(i) => i, 218 | None => { 219 | debug!("No interfaces found"); 220 | return Err(Error::InvalidEndpoints); 221 | } 222 | }; 223 | 224 | // EP1 is a bulk IN (printer -> PC) endpoint for status messages 225 | // EP2 is a bulk OUT (PC -> printer) endpoint for print commands 226 | // TODO: is this worth it, could we just, hard-code the endpoints? 227 | let (mut cmd_ep, mut stat_ep) = (None, None); 228 | 229 | for interface_desc in interface.descriptors() { 230 | for endpoint_desc in interface_desc.endpoint_descriptors() { 231 | // Find the relevant endpoints 232 | match (endpoint_desc.transfer_type(), endpoint_desc.direction()) { 233 | (TransferType::Bulk, Direction::In) => stat_ep = Some(endpoint_desc.address()), 234 | (TransferType::Bulk, Direction::Out) => cmd_ep = Some(endpoint_desc.address()), 235 | (_, _) => continue, 236 | } 237 | } 238 | } 239 | 240 | let (cmd_ep, stat_ep) = match (cmd_ep, stat_ep) { 241 | (Some(cmd), Some(stat)) => (cmd, stat), 242 | _ => { 243 | debug!("Failed to locate command and status endpoints"); 244 | return Err(Error::InvalidEndpoints); 245 | } 246 | }; 247 | 248 | // Detach kernel driver 249 | // TODO: this is usually not supported on all libusb platforms 250 | // for now this is enabled through hidden config options... 251 | // needs testing and a cfg guard as appropriate 252 | debug!("Checking for active kernel driver"); 253 | match handle.kernel_driver_active(interface.number())? { 254 | true => { 255 | if !o.usb_no_detach { 256 | debug!("Detaching kernel driver"); 257 | handle.detach_kernel_driver(interface.number())?; 258 | } else { 259 | debug!("Kernel driver detach disabled"); 260 | } 261 | }, 262 | false => { 263 | debug!("Kernel driver inactive"); 264 | }, 265 | } 266 | 267 | // Claim interface for driver 268 | // TODO: this is usually not supported on all libusb platforms 269 | // for now this is enabled through hidden config options... 270 | // needs testing and a cfg guard as appropriate 271 | if !o.usb_no_claim { 272 | debug!("Claiming interface"); 273 | handle.claim_interface(interface.number())?; 274 | } else { 275 | debug!("Claim interface disabled"); 276 | } 277 | 278 | // Create device object 279 | let mut s = Self { 280 | _device: device, 281 | handle, 282 | descriptor, 283 | cmd_ep, 284 | stat_ep, 285 | timeout: Duration::from_millis(o.timeout_milliseconds), 286 | }; 287 | 288 | // Unless we're skipping reset 289 | if !o.no_reset { 290 | // Send invalidate to reset device 291 | s.invalidate()?; 292 | // Initialise device 293 | s.init()?; 294 | } else { 295 | debug!("Skipping device reset"); 296 | } 297 | 298 | Ok(s) 299 | } 300 | 301 | /// Fetch device information 302 | pub fn info(&mut self) -> Result { 303 | let timeout = Duration::from_millis(200); 304 | 305 | // Fetch base configuration 306 | let languages = self.handle.read_languages(timeout)?; 307 | let active_config = self.handle.active_configuration()?; 308 | 309 | trace!("Active configuration: {}", active_config); 310 | trace!("Languages: {:?}", languages); 311 | 312 | // Check a language is available 313 | if languages.len() == 0 { 314 | return Err(Error::NoLanguages); 315 | } 316 | 317 | // Fetch information 318 | let language = languages[0]; 319 | let manufacturer = 320 | self.handle 321 | .read_manufacturer_string(language, &self.descriptor, timeout)?; 322 | let product = self 323 | .handle 324 | .read_product_string(language, &self.descriptor, timeout)?; 325 | let serial = self 326 | .handle 327 | .read_serial_number_string(language, &self.descriptor, timeout)?; 328 | 329 | Ok(Info { 330 | manufacturer, 331 | product, 332 | serial, 333 | }) 334 | } 335 | 336 | /// Fetch the device status 337 | pub fn status(&mut self) -> Result { 338 | // Issue status request 339 | self.status_req()?; 340 | 341 | // Read status response 342 | let d = self.read(self.timeout)?; 343 | 344 | // Convert to status object 345 | let s = Status::from(d); 346 | 347 | debug!("Status: {:02x?}", s); 348 | 349 | Ok(s) 350 | } 351 | 352 | /// Setup the printer and print using raw raster data. 353 | /// Print output must be shifted and in the correct bit-order for this function. 354 | /// 355 | /// TODO: this is too low level of an interface, should be replaced with higher-level apis 356 | pub fn print_raw(&mut self, data: Vec<[u8; 16]>, info: &PrintInfo) -> Result<(), Error> { 357 | // TODO: should we check info (and size) match status here? 358 | 359 | 360 | // Print sequence from raster guide Section 2.1 361 | // 1. Set to raster mode 362 | self.switch_mode(Mode::Raster)?; 363 | 364 | // 2. Enable status notification 365 | self.set_status_notify(true)?; 366 | 367 | // 3. Set print information (media type etc.) 368 | self.set_print_info(info)?; 369 | 370 | // 4. Set various mode settings 371 | self.set_various_mode(VariousMode::AUTO_CUT)?; 372 | 373 | // 5. Specify page number in "cut each * labels" 374 | // Note this is not supported on the PT-P710BT 375 | // TODO: add this for other printers 376 | 377 | // 6. Set advanced mode settings 378 | self.set_advanced_mode(AdvancedMode::NO_CHAIN)?; 379 | 380 | // 7. Specify margin amount 381 | // TODO: based on what? 382 | self.set_margin(0)?; 383 | 384 | // 8. Set compression mode 385 | // TODO: fix broken TIFF mode and add compression flag 386 | self.set_compression_mode(CompressionMode::None)?; 387 | 388 | // Send raster data 389 | for line in data { 390 | // TODO: re-add when TIFF mode issues resolved 391 | //let l = tiff::compress(&line); 392 | 393 | self.raster_transfer(&line)?; 394 | } 395 | 396 | // Execute print operation 397 | self.print_and_feed()?; 398 | 399 | 400 | // Poll on print completion 401 | let mut i = 0; 402 | loop { 403 | if let Ok(s) = self.read_status(self.timeout) { 404 | if !s.error1.is_empty() || !s.error2.is_empty() { 405 | debug!("Print error: {:?} {:?}", s.error1, s.error2); 406 | return Err(Error::PTouch(s.error1, s.error2)); 407 | } 408 | 409 | if s.status_type == DeviceStatus::PhaseChange { 410 | debug!("Started printing"); 411 | } 412 | 413 | if s.status_type == DeviceStatus::Completed { 414 | debug!("Print completed"); 415 | break; 416 | } 417 | } 418 | 419 | if i > 10 { 420 | debug!("Print timeout"); 421 | return Err(Error::Timeout); 422 | } 423 | 424 | i += 1; 425 | 426 | std::thread::sleep(Duration::from_secs(1)); 427 | } 428 | 429 | 430 | Ok(()) 431 | } 432 | 433 | /// Read from status EP (with specified timeout) 434 | fn read(&mut self, timeout: Duration) -> Result<[u8; 32], Error> { 435 | let mut buff = [0u8; 32]; 436 | let mut attempts = 10; 437 | // retry this 10 times 438 | loop { 439 | attempts -= 1; 440 | if attempts == 0 { 441 | return Err(Error::Timeout); 442 | } 443 | // Execute read 444 | let n = self.handle.read_bulk(self.stat_ep, &mut buff, timeout)?; 445 | if n == 0 { 446 | continue; 447 | } else { 448 | break; 449 | } 450 | } 451 | 452 | // TODO: parse out status? 453 | 454 | Ok(buff) 455 | } 456 | 457 | /// Write to command EP (with specified timeout) 458 | fn write(&mut self, data: &[u8], timeout: Duration) -> Result<(), Error> { 459 | debug!("WRITE: {:02x?}", data); 460 | 461 | // Execute write 462 | let n = self.handle.write_bulk(self.cmd_ep, &data, timeout)?; 463 | 464 | // Check write length for timeouts 465 | if n != data.len() { 466 | return Err(Error::Timeout) 467 | } 468 | 469 | Ok(()) 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/render/display.rs: -------------------------------------------------------------------------------- 1 | //! Virtual display for render support 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | use embedded_graphics::{ 8 | prelude::*, 9 | pixelcolor::BinaryColor, 10 | }; 11 | 12 | use crate::Error; 13 | 14 | /// In memory display for drawing / rendering data 15 | pub struct Display { 16 | y: usize, 17 | y_max: usize, 18 | data: Vec>, 19 | } 20 | 21 | impl Display { 22 | /// Create a new display with the provided height and minimum width 23 | pub fn new(y: usize, min_x: usize) -> Self { 24 | let mut y_max = y; 25 | while y_max % 8 != 0 { 26 | y_max += 1; 27 | } 28 | 29 | Self { 30 | y, 31 | y_max, 32 | data: vec![vec![0u8; y_max / 8]; min_x], 33 | } 34 | } 35 | 36 | /// Fetch a flipped + compressed vector image for output to printer 37 | pub fn image(&self) -> Result, Error> { 38 | // Generate new buffer 39 | let mut x_len = self.data.len(); 40 | while x_len % 8 != 0 { 41 | x_len += 1; 42 | } 43 | 44 | println!( 45 | "Using {} rows {}({}) columns", 46 | self.y, 47 | x_len, 48 | self.data.len() 49 | ); 50 | 51 | let mut buff = vec![0u8; x_len * self.y / 8]; 52 | 53 | // Reshape from row first to column first 54 | for x in 0..self.data.len() { 55 | for y in 0..self.y { 56 | let i = x / 8 + y * x_len / 8; 57 | let m = 1 << (x % 8) as u8; 58 | let v = self.get(x, y)?; 59 | let p = &mut buff[i]; 60 | 61 | println!("x: {} y: {} p: {:5?} i: {} m: 0b{:08b}", x, y, v, i, m); 62 | 63 | match v { 64 | true => *p |= m, 65 | false => *p &= !m, 66 | } 67 | } 68 | } 69 | 70 | Ok(buff) 71 | } 72 | 73 | 74 | pub fn raster(&self, margins: (usize, usize, usize)) -> Result, anyhow::Error> { 75 | let s = self.size(); 76 | 77 | println!("Raster display size: {:?} output area: {:?}", s, margins); 78 | if s.height != margins.1 as u32 { 79 | return Err(anyhow::anyhow!("Raster display and output size differ ({:?}, {:?})", s, margins)); 80 | } 81 | 82 | let mut buff = vec![[0u8; 16]; s.width as usize]; 83 | 84 | for x in 0..(s.width as usize) { 85 | for y in 0..(s.height as usize) { 86 | let p = self.get(x, y)?; 87 | 88 | let y_offset = y + margins.0 as usize; 89 | 90 | if p { 91 | buff[x][y_offset / 8] |= 1 << 7 - (y_offset % 8); 92 | } 93 | } 94 | } 95 | 96 | Ok(buff) 97 | } 98 | 99 | /// Set a pixel value by X/Y location 100 | pub fn set(&mut self, x: usize, y: usize, v: bool) -> Result<(), Error> { 101 | // Check Y bounds 102 | if y > self.y { 103 | return Err(Error::Render); 104 | } 105 | 106 | // Extend buffer in X direction 107 | while x >= self.data.len() { 108 | self.data.push(vec![0u8; self.y_max / 8]) 109 | } 110 | 111 | // Fetch pixel storage 112 | let c = &mut self.data[x][y / 8]; 113 | 114 | // Update pixel 115 | match v { 116 | true => *c |= 1 << ((y % 8) as u8), 117 | false => *c &= !(1 << ((y % 8) as u8)), 118 | } 119 | 120 | Ok(()) 121 | } 122 | 123 | /// Fetch a pixel value by X/Y location 124 | pub fn get(&self, x: usize, y: usize) -> Result { 125 | // Check Y bounds 126 | if y > self.y { 127 | return Err(Error::Render); 128 | } 129 | 130 | // Fetch pixel storage 131 | let c = self.data[x][y / 8]; 132 | 133 | // Check bits 134 | Ok(c & (1 << (y % 8) as u8) != 0) 135 | } 136 | 137 | /// Fetch a pixel value by X/Y location 138 | pub fn get_pixel(&self, x: usize, y: usize) -> Result, Error> { 139 | let v = match self.get(x, y)? { 140 | true => BinaryColor::On, 141 | false => BinaryColor::Off, 142 | }; 143 | 144 | Ok(Pixel(Point::new(x as i32, y as i32), v)) 145 | } 146 | } 147 | 148 | /// DrawTarget impl for in-memory Display type 149 | impl DrawTarget for Display { 150 | type Error = Error; 151 | 152 | fn draw_pixel(&mut self, pixel: Pixel) -> Result<(), Self::Error> { 153 | let Pixel(coord, color) = pixel; 154 | self.set(coord.x as usize, coord.y as usize, color.is_on()) 155 | } 156 | 157 | fn size(&self) -> Size { 158 | Size::new(self.data.len() as u32, self.y as u32) 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod test { 164 | 165 | use super::*; 166 | 167 | #[test] 168 | fn test_display() { 169 | let mut d = Display::new(8, 1); 170 | d.set(0, 0, true).unwrap(); 171 | assert_eq!(d.data, vec![vec![0b0000_0001]]); 172 | assert_eq!( 173 | d.image().unwrap(), 174 | vec![ 175 | 0b0000_0001, 176 | 0b0000_0000, 177 | 0b0000_0000, 178 | 0b0000_0000, 179 | 0b0000_0000, 180 | 0b0000_0000, 181 | 0b0000_0000, 182 | 0b0000_0000, 183 | ] 184 | ); 185 | 186 | let mut d = Display::new(8, 1); 187 | d.set(1, 0, true).unwrap(); 188 | assert_eq!(d.data, vec![vec![0b0000_0000], vec![0b0000_0001]]); 189 | assert_eq!( 190 | d.image().unwrap(), 191 | vec![ 192 | 0b0000_0010, 193 | 0b0000_0000, 194 | 0b0000_0000, 195 | 0b0000_0000, 196 | 0b0000_0000, 197 | 0b0000_0000, 198 | 0b0000_0000, 199 | 0b0000_0000, 200 | ] 201 | ); 202 | 203 | let mut d = Display::new(8, 1); 204 | d.set(0, 1, true).unwrap(); 205 | assert_eq!(d.data, vec![vec![0b0000_0010]]); 206 | assert_eq!( 207 | d.image().unwrap(), 208 | vec![ 209 | 0b0000_0000, 210 | 0b0000_0001, 211 | 0b0000_0000, 212 | 0b0000_0000, 213 | 0b0000_0000, 214 | 0b0000_0000, 215 | 0b0000_0000, 216 | 0b0000_0000, 217 | ] 218 | ); 219 | } 220 | 221 | #[cfg(disabled)] 222 | #[test] 223 | fn test_raster() { 224 | let mut d = Display::new(112, 1); 225 | d.set(0, 0, true).unwrap(); 226 | d.set(1, 1, true).unwrap(); 227 | d.set(2, 2, true).unwrap(); 228 | 229 | 230 | assert_eq!( 231 | &d.raster((8, 112, 8)).unwrap(), 232 | &[ 233 | [0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], 234 | [0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], 235 | [0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], 236 | ] 237 | ); 238 | } 239 | } -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | //! Basic label rendering engine 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | use std::path::Path; 8 | use log::debug; 9 | use image::Luma; 10 | use barcoders::sym::code39::Code39; 11 | use qrcode::QrCode; 12 | use datamatrix::{DataMatrix, SymbolList}; 13 | 14 | #[cfg(feature = "structopt")] 15 | use structopt::StructOpt; 16 | 17 | use embedded_graphics::prelude::*; 18 | use embedded_text::prelude::*; 19 | 20 | use embedded_graphics::{ 21 | pixelcolor::BinaryColor, 22 | style::PrimitiveStyle, 23 | }; 24 | 25 | #[cfg(feature = "preview")] 26 | use embedded_graphics_simulator::{ 27 | BinaryColorTheme, OutputSettingsBuilder, SimulatorDisplay, Window, 28 | }; 29 | 30 | use crate::Error; 31 | 32 | pub mod display; 33 | pub use display::*; 34 | pub mod ops; 35 | pub use ops::*; 36 | 37 | #[derive(Clone, PartialEq, Debug)] 38 | #[cfg_attr(feature = "structopt", derive(StructOpt))] 39 | pub struct RenderConfig { 40 | /// Image minimum X size 41 | pub min_x: usize, 42 | /// Image maximum X size 43 | pub max_x: usize, 44 | /// Image Y size 45 | pub y: usize, 46 | } 47 | 48 | impl Default for RenderConfig { 49 | fn default() -> Self { 50 | Self { 51 | min_x: 32, 52 | max_x: 10 * 1024, 53 | y: 64, 54 | } 55 | } 56 | } 57 | 58 | pub struct Render { 59 | cfg: RenderConfig, 60 | display: Display, 61 | } 62 | 63 | impl Render { 64 | /// Create a new render instance 65 | pub fn new(cfg: RenderConfig) -> Self { 66 | // Setup virtual display for render data 67 | let display = Display::new(cfg.y as usize, cfg.min_x as usize); 68 | 69 | // Return new renderer 70 | Self { cfg, display } 71 | } 72 | 73 | /// Save the render buffer as an image 74 | pub fn save>(&self, path: P) -> Result<(), anyhow::Error> { 75 | // Fetch current display size 76 | let size = self.display.size(); 77 | 78 | // Create image 79 | let i = image::DynamicImage::new_luma8(size.width, size.height); 80 | let mut i = i.into_luma8(); 81 | 82 | // Copy data into image 83 | for x in 0..size.width { 84 | for y in 0..size.height { 85 | let p = self.display.get(x as usize, y as usize)?; 86 | if !p { 87 | i.put_pixel(x, y, Luma([0xff])); 88 | } 89 | } 90 | } 91 | 92 | // Save image to file 93 | i.save(path)?; 94 | 95 | Ok(()) 96 | } 97 | 98 | 99 | /// Execute render operations 100 | pub fn render(&mut self, ops: &[Op]) -> Result<&Self, Error> { 101 | let mut x = 0; 102 | for operation in ops { 103 | x += match operation { 104 | Op::Text { text, opts } => self.render_text(x, text, opts)?, 105 | Op::Pad{ count } => self.pad(x, *count)?, 106 | Op::Qr{ code } => self.render_qrcode(x, code)?, 107 | Op::DataMatrix{ code } => self.render_datamatrix(x, code)?, 108 | Op::Barcode{ code, opts } => self.render_barcode(x, code, opts)?, 109 | Op::Image{ file, opts } => self.render_image(x, file, opts)?, 110 | } 111 | } 112 | 113 | // TODO: store data? idk 114 | 115 | Ok(self) 116 | } 117 | 118 | fn render_text(&mut self, start_x: usize, value: &str, opts: &TextOptions) -> Result { 119 | use embedded_graphics::fonts::*; 120 | use embedded_text::style::vertical_overdraw::Hidden; 121 | 122 | // TODO: customise styles 123 | 124 | // TODO: custom alignment 125 | 126 | // TODO: clean this up when updated embedded-graphics font API lands 127 | // https://github.com/embedded-graphics/embedded-graphics/issues/511 128 | 129 | // Fix for escaped newlines from shell 130 | // Otherwise "\n" becomes "\\n" and nothing works quite right 131 | let value = value.replace("\\n", "\n"); 132 | 133 | // Compute maximum line width 134 | let max_line_x = value 135 | .split("\n") 136 | .map(|line| opts.font.char_width() * line.len() + 1) 137 | .max() 138 | .unwrap(); 139 | let max_x = self.cfg.max_x.min(start_x + max_line_x); 140 | 141 | // Create textbox instance 142 | let tb = TextBox::new( 143 | &value, 144 | Rectangle::new( 145 | Point::new(start_x as i32, 0 as i32), 146 | Point::new(max_x as i32, self.cfg.y as i32), 147 | ), 148 | ); 149 | 150 | debug!("Textbox: {:?}", tb); 151 | 152 | #[cfg(nope)] 153 | let a = match opts.h_align { 154 | HAlign::Centre => CenterAligned, 155 | HAlign::Left => LeftAligned, 156 | HAlign::Right => RightAligned, 157 | HAlign::Justify => Justified, 158 | }; 159 | #[cfg(nope)] 160 | let v = match opts.v_align { 161 | VAlign::Centre => CenterAligned, 162 | VAlign::Top => TopAligned, 163 | VAlign::Bottom => BottomAligned, 164 | }; 165 | 166 | let a = CenterAligned; 167 | let v = CenterAligned; 168 | let h = Exact(Hidden); 169 | let l = 4; 170 | 171 | // Render with loaded style 172 | let res = match opts.font { 173 | FontKind::Font6x6 => { 174 | let ts = TextBoxStyleBuilder::new(Font6x6) 175 | .text_color(BinaryColor::On) 176 | .height_mode(h) 177 | .alignment(a) 178 | .line_spacing(l) 179 | .vertical_alignment(v) 180 | .build(); 181 | 182 | let tb = tb.into_styled(ts); 183 | 184 | tb.draw(&mut self.display).unwrap(); 185 | 186 | tb.size() 187 | } 188 | FontKind::Font6x8 => { 189 | let ts = TextBoxStyleBuilder::new(Font6x8) 190 | .text_color(BinaryColor::On) 191 | .height_mode(h) 192 | .alignment(a) 193 | .line_spacing(l) 194 | .vertical_alignment(v) 195 | .build(); 196 | 197 | let tb = tb.into_styled(ts); 198 | 199 | tb.draw(&mut self.display).unwrap(); 200 | 201 | tb.size() 202 | } 203 | FontKind::Font6x12 => { 204 | let ts = TextBoxStyleBuilder::new(Font6x12) 205 | .text_color(BinaryColor::On) 206 | .height_mode(h) 207 | .alignment(a) 208 | .line_spacing(l) 209 | .vertical_alignment(v) 210 | .build(); 211 | 212 | let tb = tb.into_styled(ts); 213 | 214 | tb.draw(&mut self.display).unwrap(); 215 | 216 | tb.size() 217 | } 218 | FontKind::Font8x16 => { 219 | let ts = TextBoxStyleBuilder::new(Font8x16) 220 | .text_color(BinaryColor::On) 221 | .height_mode(h) 222 | .alignment(a) 223 | .line_spacing(l) 224 | .vertical_alignment(v) 225 | .build(); 226 | 227 | let tb = tb.into_styled(ts); 228 | 229 | tb.draw(&mut self.display).unwrap(); 230 | 231 | tb.size() 232 | } 233 | FontKind::Font12x16 => { 234 | let ts = TextBoxStyleBuilder::new(Font12x16) 235 | .text_color(BinaryColor::On) 236 | .height_mode(h) 237 | .alignment(a) 238 | .line_spacing(l) 239 | .vertical_alignment(v) 240 | .build(); 241 | 242 | let tb = tb.into_styled(ts); 243 | 244 | tb.draw(&mut self.display).unwrap(); 245 | 246 | tb.size() 247 | } 248 | FontKind::Font24x32 => { 249 | let ts = TextBoxStyleBuilder::new(Font24x32) 250 | .text_color(BinaryColor::On) 251 | .height_mode(h) 252 | .alignment(a) 253 | .line_spacing(l) 254 | .vertical_alignment(v) 255 | .build(); 256 | 257 | let tb = tb.into_styled(ts); 258 | 259 | tb.draw(&mut self.display).unwrap(); 260 | 261 | tb.size() 262 | } 263 | }; 264 | 265 | Ok(res.width as usize) 266 | } 267 | 268 | fn pad(&mut self, x: usize, columns: usize) -> Result { 269 | self.display 270 | .draw_pixel(Pixel(Point::new((x + columns) as i32, 0), BinaryColor::Off))?; 271 | Ok(columns) 272 | } 273 | 274 | fn render_qrcode(&mut self, x_start: usize, value: &str) -> Result { 275 | // Generate QR 276 | let qr = QrCode::new(value).unwrap(); 277 | let img = qr.render() 278 | .dark_color(image::Rgb([0, 0, 0])) 279 | .light_color(image::Rgb([255, 255, 255])) 280 | .quiet_zone(false) 281 | .max_dimensions(self.cfg.y as u32, self.cfg.y as u32) 282 | .build(); 283 | 284 | // Generate offsets 285 | let y_offset = (self.cfg.y as i32 - img.height() as i32) / 2; 286 | let x_offset = x_start as i32 + y_offset; 287 | 288 | // Write to display 289 | for (x, y, v) in img.enumerate_pixels() { 290 | let c = match v { 291 | image::Rgb([0, 0, 0]) => BinaryColor::On, 292 | _ => BinaryColor::Off, 293 | }; 294 | let p = Pixel(Point::new(x_offset + x as i32, y_offset + y as i32), c); 295 | self.display.draw_pixel(p)? 296 | } 297 | 298 | Ok(img.width() as usize + x_offset as usize) 299 | } 300 | 301 | fn render_datamatrix(&mut self, x_start: usize, value: &str) -> Result { 302 | // Only allow up to max height of tape 303 | let sl = SymbolList::default().enforce_height_in(..self.cfg.y); 304 | let dm = DataMatrix::encode(value.as_bytes(), sl).unwrap(); 305 | let bitmap = dm.bitmap(); 306 | 307 | // We want to make the datamatrix as large as possible for scanning 308 | let scale = self.cfg.y / bitmap.height(); 309 | 310 | let x_offset = x_start; 311 | let y_offset = ((self.cfg.y as i32 - (bitmap.height() * scale) as i32) / 2) as usize; 312 | 313 | for (x,y) in bitmap.pixels() { 314 | let xs = x_offset + x * scale; 315 | let ys = y_offset + y * scale; 316 | let r = Rectangle::new(Point::new(xs as i32, ys as i32), 317 | Point::new((xs+scale-1) as i32, (ys+scale-1) as i32)); 318 | self.display.draw_rectangle(&r.into_styled(PrimitiveStyle::with_fill(BinaryColor::On)))?; 319 | } 320 | Ok(bitmap.width()*scale + x_offset) 321 | } 322 | 323 | fn render_barcode(&mut self, x_start: usize, value: &str, opts: &BarcodeOptions) -> Result { 324 | let barcode = Code39::new(value).unwrap(); 325 | let encoded: Vec = barcode.encode(); 326 | 327 | let x_offset = x_start as i32; 328 | 329 | // TODO: something is not quite right here... 330 | for i in 0..encoded.len() { 331 | //let v = (encoded[i / 8] & ( 1 << (i % 8) ) ) == 0; 332 | 333 | for y in opts.y_offset..self.cfg.y-opts.y_offset { 334 | let c = match encoded[i] != 0 { 335 | true => BinaryColor::On, 336 | false => BinaryColor::Off, 337 | }; 338 | 339 | let p = Pixel(Point::new(x_offset + i as i32, y as i32), c); 340 | self.display.draw_pixel(p)? 341 | } 342 | } 343 | 344 | Ok(encoded.len() + x_offset as usize) 345 | } 346 | 347 | fn render_image(&mut self, x_start: usize, file: &str, _opts: &ImageOptions) -> Result { 348 | // Load image and convert to greyscale 349 | let img = image::io::Reader::open(file)?.decode()?; 350 | let i = img.clone().into_luma8(); 351 | let d = i.dimensions(); 352 | 353 | // TODO: Rescale based on image options 354 | 355 | let x_offset = x_start as i32; 356 | let y_offset = (self.cfg.y / 2) as i32 - (d.1 as usize / 2) as i32; 357 | 358 | // Copy image data into display 359 | for x in 0..d.0 as i32 { 360 | for y in 0..d.1 as i32 { 361 | let p = i.get_pixel(x as u32, y as u32); 362 | 363 | let c = match p.0[0] == 0 { 364 | true => BinaryColor::On, 365 | false => BinaryColor::Off, 366 | }; 367 | 368 | let p = Pixel(Point::new(x_offset + x as i32, y_offset + y as i32), c); 369 | self.display.draw_pixel(p)? 370 | } 371 | } 372 | 373 | Ok(d.0 as usize + x_offset as usize) 374 | } 375 | 376 | /// Raster data to a ptouch compatible buffer for printing 377 | pub fn raster(&self, margins: (usize, usize, usize)) -> Result, anyhow::Error> { 378 | self.display.raster(margins) 379 | } 380 | 381 | /// Show the rendered image (note that this blocks until the window is closed) 382 | #[cfg(feature = "preview")] 383 | pub fn show(&self) -> Result<(), anyhow::Error> { 384 | // Fetch rendered size 385 | let s = self.display.size(); 386 | 387 | debug!("Render display size: {:?}", s); 388 | 389 | // Create simulated display 390 | let mut sim_display: SimulatorDisplay = SimulatorDisplay::new(s); 391 | 392 | // Copy buffer into simulated display 393 | for y in 0..s.height as usize { 394 | for x in 0..s.width as usize { 395 | let p = self.display.get_pixel(x, y)?; 396 | sim_display.draw_pixel(p)?; 397 | } 398 | } 399 | 400 | let output_settings = OutputSettingsBuilder::new() 401 | // TODO: set theme based on tape? 402 | .theme(BinaryColorTheme::LcdWhite) 403 | .build(); 404 | 405 | let name = format!("Label preview ({}, {})", s.width, s.height); 406 | Window::new(&name, &output_settings).show_static(&sim_display); 407 | 408 | Ok(()) 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/render/ops.rs: -------------------------------------------------------------------------------- 1 | //! Render operations 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | #[cfg(feature = "strum")] 8 | use strum_macros::{Display, EnumString, EnumVariantNames}; 9 | 10 | #[cfg(feature = "serde")] 11 | use serde::{Serialize, Deserialize}; 12 | 13 | #[cfg(feature = "structopt")] 14 | use structopt::StructOpt; 15 | 16 | #[derive(Clone, Debug, PartialEq)] 17 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 18 | pub struct RenderTemplate { 19 | pub ops: Vec, 20 | } 21 | 22 | 23 | /// Render operations, used to construct an image 24 | #[derive(Clone, PartialEq, Debug)] 25 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 26 | #[cfg_attr(feature = "serde", serde(tag="kind", rename_all="snake_case"))] 27 | pub enum Op { 28 | Text { 29 | text: String, 30 | #[cfg_attr(feature = "serde", serde(flatten))] 31 | opts: TextOptions 32 | }, 33 | Pad{ 34 | count: usize 35 | }, 36 | Qr{ 37 | code: String 38 | }, 39 | DataMatrix{ 40 | code: String 41 | }, 42 | Barcode{ 43 | code: String, 44 | #[cfg_attr(feature = "serde", serde(flatten, default))] 45 | opts: BarcodeOptions 46 | }, 47 | Image{ 48 | file: String, 49 | #[cfg_attr(feature = "serde", serde(flatten, default))] 50 | opts: ImageOptions 51 | }, 52 | } 53 | 54 | impl Op { 55 | pub fn text(s: &str) -> Self { 56 | Self::Text { 57 | text: s.to_string(), 58 | opts: TextOptions::default(), 59 | } 60 | } 61 | 62 | pub fn text_with_font(s: &str, f: FontKind) -> Self { 63 | Self::Text { 64 | text: s.to_string(), 65 | opts: TextOptions{ 66 | font: f, 67 | ..Default::default() 68 | }, 69 | } 70 | } 71 | 72 | pub fn pad(columns: usize) -> Self { 73 | Self::Pad{ count: columns } 74 | } 75 | 76 | pub fn qr(code: &str) -> Self { 77 | Self::Qr{ code: code.to_string() } 78 | } 79 | 80 | pub fn datamatrix(code: &str) -> Self { 81 | Self::DataMatrix{ code: code.to_string() } 82 | } 83 | 84 | pub fn barcode(code: &str) -> Self { 85 | Self::Barcode{ 86 | code: code.to_string(), 87 | opts: BarcodeOptions::default(), 88 | } 89 | } 90 | 91 | pub fn image(file: &str) -> Self { 92 | Self::Image { 93 | file: file.to_string(), 94 | opts: ImageOptions::default(), 95 | } 96 | } 97 | } 98 | 99 | 100 | #[derive(Copy, Clone, PartialEq, Debug)] 101 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 102 | #[cfg_attr(feature = "strum", derive(Display, EnumString, EnumVariantNames))] 103 | #[cfg_attr(feature = "serde", serde(rename_all="snake_case"))] 104 | pub enum FontKind { 105 | #[cfg_attr(feature = "strum", strum(serialize = "6x6"))] 106 | Font6x6, 107 | #[cfg_attr(feature = "strum", strum(serialize = "6x8"))] 108 | Font6x8, 109 | #[cfg_attr(feature = "strum", strum(serialize = "6x12"))] 110 | Font6x12, 111 | #[cfg_attr(feature = "strum", strum(serialize = "8x16"))] 112 | Font8x16, 113 | #[cfg_attr(feature = "strum", strum(serialize = "12x16"))] 114 | Font12x16, 115 | #[cfg_attr(feature = "strum", strum(serialize = "24x32"))] 116 | Font24x32, 117 | } 118 | 119 | impl FontKind { 120 | pub fn char_width(&self) -> usize { 121 | use embedded_graphics::fonts::*; 122 | 123 | match self { 124 | FontKind::Font6x6 => Font6x6::CHARACTER_SIZE.width as usize, 125 | FontKind::Font6x8 => Font6x8::CHARACTER_SIZE.width as usize, 126 | FontKind::Font6x12 => Font6x12::CHARACTER_SIZE.width as usize, 127 | FontKind::Font8x16 => Font8x16::CHARACTER_SIZE.width as usize, 128 | FontKind::Font12x16 => Font12x16::CHARACTER_SIZE.width as usize, 129 | FontKind::Font24x32 => Font24x32::CHARACTER_SIZE.width as usize, 130 | } 131 | } 132 | 133 | pub fn char_height(&self) -> usize { 134 | use embedded_graphics::fonts::*; 135 | 136 | match self { 137 | FontKind::Font6x6 => Font6x6::CHARACTER_SIZE.height as usize, 138 | FontKind::Font6x8 => Font6x8::CHARACTER_SIZE.height as usize, 139 | FontKind::Font6x12 => Font6x12::CHARACTER_SIZE.height as usize, 140 | FontKind::Font8x16 => Font8x16::CHARACTER_SIZE.height as usize, 141 | FontKind::Font12x16 => Font12x16::CHARACTER_SIZE.height as usize, 142 | FontKind::Font24x32 => Font24x32::CHARACTER_SIZE.height as usize, 143 | } 144 | } 145 | } 146 | 147 | #[derive(Clone, PartialEq, Debug)] 148 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 149 | #[cfg_attr(feature = "serde", serde(default))] 150 | pub struct TextOptions { 151 | pub font: FontKind, 152 | pub v_align: VAlign, 153 | pub h_align: HAlign, 154 | } 155 | 156 | #[derive(Clone, PartialEq, Debug)] 157 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 158 | #[cfg_attr(feature = "strum", derive(EnumString, EnumVariantNames))] 159 | pub enum HAlign { 160 | Left, 161 | Centre, 162 | Right, 163 | } 164 | 165 | #[derive(Clone, PartialEq, Debug)] 166 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 167 | #[cfg_attr(feature = "strum", derive(EnumString, EnumVariantNames))] 168 | pub enum VAlign { 169 | Top, 170 | Centre, 171 | Bottom, 172 | } 173 | 174 | impl Default for TextOptions { 175 | fn default() -> Self { 176 | Self { 177 | font: FontKind::Font12x16, 178 | h_align: HAlign::Centre, 179 | v_align: VAlign::Centre, 180 | } 181 | } 182 | } 183 | 184 | #[derive(Clone, PartialEq, Debug)] 185 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 186 | #[cfg_attr(feature = "structopt", derive(StructOpt))] 187 | pub struct BarcodeOptions { 188 | #[cfg_attr(feature = "structopt", structopt(default_value="4"))] 189 | /// Y offset from top and bottom of label 190 | pub y_offset: usize, 191 | 192 | #[cfg_attr(feature = "structopt", structopt(long))] 193 | /// Double barcode width 194 | pub double: bool, 195 | } 196 | 197 | impl Default for BarcodeOptions { 198 | fn default() -> Self { 199 | Self { 200 | y_offset: 4, 201 | double: false, 202 | } 203 | } 204 | } 205 | 206 | #[derive(Clone, PartialEq, Debug)] 207 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 208 | #[cfg_attr(feature = "structopt", derive(StructOpt))] 209 | pub struct ImageOptions { 210 | // TODO: scaling, invert, etc... 211 | } 212 | 213 | impl Default for ImageOptions { 214 | fn default() -> Self { 215 | Self {} 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/tiff.rs: -------------------------------------------------------------------------------- 1 | //! TIFF compression functions 2 | // Rust PTouch Driver / Utility 3 | // 4 | // https://github.com/ryankurte/rust-ptouch 5 | // Copyright 2021 Ryan Kurte 6 | 7 | #[derive(Clone, Debug, PartialEq)] 8 | enum CompressMode { 9 | None(u8), 10 | Repeated(u8, usize), 11 | Unique(Vec), 12 | } 13 | 14 | // TODO: incomplete implementation, does not consider > 16 case from docs 15 | pub fn compress(data: &[u8]) -> Vec { 16 | let mut c = Vec::::new(); 17 | 18 | let mut state = CompressMode::None(data[0]); 19 | 20 | // Perform byte-wise compression 21 | for i in 1..data.len() { 22 | state = match state { 23 | CompressMode::None(v) if data[i] == v => CompressMode::Repeated(v, 1), 24 | CompressMode::None(v) => CompressMode::Unique(vec![v, data[i]]), 25 | CompressMode::Repeated(v, n) if data[i] == v => CompressMode::Repeated(v, n + 1), 26 | CompressMode::Repeated(v, n) => { 27 | let count = 0xFF - (n as u8 - 1); 28 | 29 | c.push(count as u8); 30 | c.push(v); 31 | 32 | CompressMode::None(data[i]) 33 | } 34 | CompressMode::Unique(mut v) if data[i] != v[v.len() - 1] => { 35 | v.push(data[i]); 36 | 37 | CompressMode::Unique(v) 38 | } 39 | CompressMode::Unique(v) => { 40 | let count = v.len() - 1; 41 | 42 | c.push(count as u8); 43 | c.extend_from_slice(&v[..count]); 44 | 45 | CompressMode::Repeated(data[i], 2) 46 | } 47 | }; 48 | } 49 | 50 | // Finalize any pending data 51 | match state { 52 | CompressMode::None(v) => { 53 | c.push(0x00); 54 | c.push(v); 55 | } 56 | CompressMode::Repeated(v, n) => { 57 | let count = 0xFF - (n as u8 - 1); 58 | 59 | c.push(count as u8); 60 | c.push(v); 61 | } 62 | CompressMode::Unique(v) => { 63 | let count = v.len() - 1; 64 | 65 | c.push(count as u8); 66 | c.extend_from_slice(&v); 67 | } 68 | } 69 | 70 | // If the encoded length > 16, just use this in simple mode. 71 | if c.len() > 16 { 72 | c = vec![]; 73 | c.push(data.len() as u8); 74 | c.extend_from_slice(data); 75 | } 76 | 77 | c 78 | } 79 | 80 | pub fn uncompress(data: &[u8]) -> Vec { 81 | let mut u = vec![]; 82 | let mut i: usize = 0; 83 | 84 | loop { 85 | let d = data[i] as i8; 86 | 87 | if d < 0 { 88 | // -ve indicates repeated chars 89 | let mut r = vec![data[i+1]; (-d+1) as usize]; 90 | u.append(&mut r); 91 | i += 2; 92 | } else { 93 | // +ve indicates literal sequence 94 | let c = d as usize; 95 | u.extend_from_slice(&data[i+1..i+c+2]); 96 | i += c + 2; 97 | } 98 | 99 | if i >= data.len() { 100 | break; 101 | } 102 | } 103 | 104 | return u 105 | } 106 | 107 | #[cfg(test)] 108 | mod test { 109 | #[test] 110 | fn test_raster_compression() { 111 | let uncompressed = [ 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x22, 0x23, 0xBA, 0xBF, 0xA2, 0x22, 0x2B, 114 | ]; 115 | let compressed = [ 116 | 0xED, 0x00, 0xFF, 0x22, 0x05, 0x23, 0xBA, 0xBF, 0xA2, 0x22, 0x2B, 117 | ]; 118 | 119 | let c = super::compress(&uncompressed); 120 | 121 | assert_eq!( 122 | c, compressed, 123 | "Compressed: {:02x?} Expected: {:02x?}", 124 | &c, &compressed 125 | ); 126 | 127 | let d = super::uncompress(&compressed); 128 | 129 | assert_eq!( 130 | d, uncompressed, 131 | "Uncompressed: {:02x?} Expected: {:02x?}", 132 | &d, &uncompressed 133 | ); 134 | } 135 | 136 | // TODO: test compress / decompress as something is definitely not -right- 137 | } 138 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Rust PTouch Command Line Utility 2 | // 3 | // https://github.com/ryankurte/rust-ptouch 4 | // Copyright 2021 Ryan Kurte 5 | 6 | use log::{debug, info, warn}; 7 | use simplelog::{LevelFilter, TermLogger, TerminalMode, ColorChoice}; 8 | use structopt::StructOpt; 9 | use strum::VariantNames; 10 | 11 | use ptouch::{Options, PTouch, render::RenderTemplate}; 12 | use ptouch::device::{Media, PrintInfo, Status}; 13 | use ptouch::render::{FontKind, Op, Render, RenderConfig}; 14 | 15 | 16 | #[derive(Clone, Debug, PartialEq, StructOpt)] 17 | pub struct Flags { 18 | #[structopt(flatten)] 19 | options: Options, 20 | 21 | #[structopt(subcommand)] 22 | command: Command, 23 | 24 | #[structopt(long, default_value="16")] 25 | /// Padding for start and end of renders 26 | pad: usize, 27 | 28 | #[structopt(long, possible_values = &Media::VARIANTS, default_value="tze12mm")] 29 | /// Default media kind when unable to query this from printer 30 | media: Media, 31 | 32 | #[structopt(long, default_value = "info")] 33 | log_level: LevelFilter, 34 | } 35 | 36 | #[derive(Clone, Debug, PartialEq, StructOpt)] 37 | pub enum RenderCommand { 38 | /// Basic text rendering 39 | Text { 40 | /// Text value 41 | text: String, 42 | #[structopt(long, possible_values = &FontKind::VARIANTS, default_value="12x16")] 43 | /// Text font 44 | font: FontKind, 45 | }, 46 | /// QR Code with text 47 | QrText { 48 | /// QR value 49 | qr: String, 50 | 51 | /// Text value 52 | text: String, 53 | 54 | #[structopt(long, possible_values = &FontKind::VARIANTS, default_value="12x16")] 55 | /// Text font 56 | font: FontKind, 57 | }, 58 | /// QR Code 59 | Qr { 60 | /// QR value 61 | qr: String, 62 | }, 63 | /// Datamatrix 64 | Datamatrix { 65 | /// Datamatrix value 66 | dm: String, 67 | }, 68 | /// Barcode (EXPERIMENTAL) 69 | Barcode { 70 | /// Barcode value 71 | code: String, 72 | }, 73 | /// Render from template 74 | Template{ 75 | /// Template file 76 | file: String, 77 | }, 78 | /// Render from image 79 | Image{ 80 | /// Image file 81 | file: String, 82 | }, 83 | /// Render example 84 | Example, 85 | } 86 | 87 | #[derive(Clone, Debug, PartialEq, StructOpt)] 88 | pub enum Command { 89 | // Fetch printer info 90 | Info, 91 | 92 | // Fetch printer status 93 | Status, 94 | 95 | // Render and display a preview 96 | Preview(RenderCommand), 97 | 98 | // Render to an image file 99 | Render{ 100 | #[structopt(long)] 101 | /// Image file to save render output 102 | file: String, 103 | 104 | #[structopt(subcommand)] 105 | cmd: RenderCommand, 106 | }, 107 | 108 | // Print data! 109 | Print(RenderCommand), 110 | } 111 | 112 | fn main() -> anyhow::Result<()> { 113 | // Parse CLI options 114 | let opts = Flags::from_args(); 115 | 116 | // Setup logging 117 | TermLogger::init( 118 | opts.log_level, 119 | simplelog::Config::default(), 120 | TerminalMode::Mixed, 121 | ColorChoice::Auto, 122 | ) 123 | .unwrap(); 124 | 125 | // Create default render configuration 126 | let mut rc = RenderConfig{ 127 | y: opts.media.area().1 as usize, 128 | ..Default::default() 129 | }; 130 | 131 | debug!("Connecting to PTouch device: {:?}", opts.options); 132 | 133 | // Attempt to connect to ptouch device to inform configuration 134 | let connect = match PTouch::new(&opts.options) { 135 | Ok(mut pt) => { 136 | let status; 137 | if opts.options.no_status_fetch { 138 | info!("Connected! status request disabled, using default status..."); 139 | // Getting default status 140 | status = Status::new(&opts.media)?; 141 | info!("Device status (default one used): {:?}", status); 142 | } else { 143 | info!("Connected! fetching status..."); 144 | // Fetch device status 145 | status = pt.status()?; 146 | info!("Device status (fetched from device): {:?}", status); 147 | } 148 | 149 | // Build MediaWidth from status message to retrieve offsets 150 | let media = Media::from((status.media_kind, status.media_width)); 151 | 152 | // Update render config to reflect tape 153 | rc.y = media.area().1 as usize; 154 | // TODO: update colours too? 155 | 156 | // Return device and mediat width 157 | Ok((pt, status, media)) 158 | }, 159 | Err(e) => Err(e), 160 | }; 161 | 162 | 163 | // TODO: allow RenderConfig override from CLI 164 | 165 | 166 | // Run commands that do not _require_ the printer 167 | match &opts.command { 168 | #[cfg(feature = "preview")] 169 | Command::Preview(cmd) => { 170 | // Inform user if print boundaries are unset 171 | if connect.is_err() { 172 | warn!("Using default media: {}, override with `--media` argument", opts.media); 173 | } 174 | 175 | // Load render operations from command 176 | let ops = cmd.load(opts.pad)?; 177 | 178 | // Create renderer 179 | let mut r = Render::new(rc); 180 | 181 | // Apply render operations 182 | r.render(&ops)?; 183 | 184 | // Display render output 185 | r.show()?; 186 | 187 | return Ok(()); 188 | }, 189 | #[cfg(not(feature = "preview"))] 190 | Command::Preview(_cmd) => { 191 | warn!("Preview not enabled (or not supported on this platform"); 192 | warn!("Try `render` command to render to image files"); 193 | return Ok(()) 194 | } 195 | Command::Render{ file, cmd } => { 196 | // Inform user if print boundaries are unset 197 | if connect.is_err() { 198 | warn!("Using default media: {}, override with `--media` argument", opts.media); 199 | } 200 | 201 | // Load render operations from command 202 | let ops = cmd.load(opts.pad)?; 203 | 204 | // Create renderer 205 | let mut r = Render::new(rc); 206 | 207 | // Apply render operations 208 | r.render(&ops)?; 209 | 210 | // Display render output 211 | r.save(file)?; 212 | 213 | return Ok(()); 214 | }, 215 | _ => (), 216 | } 217 | 218 | // Check PTouch connection was successful 219 | let (mut ptouch, status, media) = match connect { 220 | Ok(d) => d, 221 | Err(e) => { 222 | return Err(anyhow::anyhow!("Error connecting to PTouch: {:?}", e)); 223 | } 224 | }; 225 | 226 | // Run commands that -do- require the printer 227 | match &opts.command { 228 | Command::Info => { 229 | let i = ptouch.info()?; 230 | println!("Info: {:?}", i); 231 | }, 232 | Command::Status => { 233 | println!("Status: {:?}", status); 234 | }, 235 | Command::Print(cmd) => { 236 | 237 | // Load render operations from command 238 | let ops = cmd.load(opts.pad)?; 239 | 240 | // Create renderer 241 | let mut r = Render::new(rc); 242 | 243 | // Apply render operations 244 | r.render(&ops)?; 245 | 246 | // Generate raster data for printing 247 | let data = r.raster(media.area())?; 248 | 249 | // Setup print info based on media and rastered data 250 | let info = PrintInfo { 251 | width: Some(status.media_width), 252 | length: Some(0), 253 | raster_no: data.len() as u32, 254 | ..Default::default() 255 | }; 256 | 257 | // Print the thing! 258 | ptouch.print_raw(data, &info)?; 259 | 260 | }, 261 | _ => (), 262 | } 263 | 264 | // TODO: close the printer? 265 | 266 | Ok(()) 267 | } 268 | 269 | 270 | impl RenderCommand { 271 | pub fn load(&self, pad: usize) -> Result, anyhow::Error> { 272 | match self { 273 | RenderCommand::Text { text, font } => { 274 | let ops = vec![ 275 | Op::pad(pad), 276 | Op::text_with_font(text, *font), 277 | Op::pad(pad), 278 | ]; 279 | Ok(ops) 280 | }, 281 | RenderCommand::QrText { qr, text, font } => { 282 | let ops = vec![ 283 | Op::pad(pad), 284 | Op::qr(qr), 285 | Op::text_with_font(text, *font), 286 | Op::pad(pad) 287 | ]; 288 | Ok(ops) 289 | }, 290 | RenderCommand::Qr { qr } => { 291 | let ops = vec![ 292 | Op::pad(pad), 293 | Op::qr(qr), 294 | Op::pad(pad) 295 | ]; 296 | Ok(ops) 297 | }, 298 | RenderCommand::Datamatrix { dm } => { 299 | let ops = vec![ 300 | Op::pad(pad), 301 | Op::datamatrix(dm), 302 | Op::pad(pad) 303 | ]; 304 | Ok(ops) 305 | }, 306 | RenderCommand::Barcode { code } => { 307 | let ops = vec![ 308 | Op::pad(pad), 309 | Op::barcode(code), 310 | Op::pad(pad) 311 | ]; 312 | Ok(ops) 313 | }, 314 | RenderCommand::Template { file } => { 315 | // Read template file 316 | let t = std::fs::read_to_string(file)?; 317 | // Parse to render ops 318 | let c: RenderTemplate = toml::from_str(&t)?; 319 | // Return render operations 320 | Ok(c.ops) 321 | }, 322 | RenderCommand::Image { file } => { 323 | let ops = vec![ 324 | Op::pad(pad), 325 | Op::image(file), 326 | Op::pad(pad) 327 | ]; 328 | Ok(ops) 329 | } 330 | RenderCommand::Example => { 331 | let ops = vec![ 332 | Op::pad(pad), 333 | Op::qr("https://hello.world"), 334 | Op::text("hello world,,\nhow's it going?"), 335 | Op::pad(pad) 336 | ]; 337 | 338 | Ok(ops) 339 | } 340 | } 341 | } 342 | } 343 | --------------------------------------------------------------------------------