ng-thermal-print icon indicating copy to clipboard operation
ng-thermal-print copied to clipboard

This library without printing images is useless

Open mohamed-mostafa10 opened this issue 1 year ago • 3 comments

mohamed-mostafa10 avatar Mar 16 '23 11:03 mohamed-mostafa10

I wrote this:

import { Inject, Injectable, NgModule } from '@angular/core';

export const WIDTH = 'WIDTH';
export const HEIGHT = 'HEIGHT';
export const INVERT_COLORS = 'INVERT_COLORS';
@Injectable({
  providedIn: 'root',
})
export class ImagePrintingService {
  private imageData: ImageData;
  private invertColors: boolean;
  
  private ESC_INIT = [0x1b, 0x40];
  private ESC_BIT_IMAGE = [0x1b, 0x2a]
  private DOTS_DENSITY = 24
  private LUMINANCE = {
      RED: 0.299,
      GREEN: 0.587,
      BLUE: 0.114
  }
  private LINE_FEED = 0x0a;
  // https://www.sparkag.com.br/wp-content/uploads/2016/06/ESC_POS-AK912-English-Command-Specifications-V1.4.pdf
  private LINE_FEED_AMOUNT = [0x1b, 0x33, 0x18]; // the third parameter is a numeric value where 1 = 0.125mm
  private LINE_FEED_DEFAULT = [0x1b, 0x32];

  // the best print quality is achieved when width and height are both multiples of 8
  constructor(image: HTMLImageElement, @Inject(INVERT_COLORS) invertColors: boolean = false, @Inject(WIDTH) width?: number, @Inject(HEIGHT) height?: number) {
    const canvas = document.createElement('canvas');
    canvas.width = width ?? image.width;
    canvas.height = height ?? image.height;
    const context = canvas.getContext('2d');

    if (!context) {
      throw new Error('Could not get 2D context for canvas');
    }

    context.drawImage(image, 0, 0, canvas.width, canvas.height);
    this.imageData = context.getImageData(0, 0, canvas.width, canvas.height);
    this.invertColors = invertColors;
  }

  private calculateLuminance(pixel: number[]): number {
    let lum = this.LUMINANCE.RED * pixel[0] + this.LUMINANCE.GREEN * pixel[1] + this.LUMINANCE.BLUE * pixel[2];

    return this.invertColors ? 255 - lum : lum;
  }

  private calculateSlice(x: number, y: number): number {
    const threshold = 127;
    let slice = 0;
    
    for (let bit = 0; bit < 8; bit++) {
        if ((y + bit) >= this.imageData.height)
            continue;

        const index = (this.imageData.width * (y + bit) + x) * 4;
        const pixel = [
          this.imageData.data[index],
          this.imageData.data[index + 1],
          this.imageData.data[index + 2]
        ];

        const luminance = this.calculateLuminance(pixel);

        slice |= (luminance < threshold ? 1 : 0) << 7 - bit;
    }

    return slice;
  }

  private collectStripe(x: number, y: number): number[] {
    let slices = [];
    let z = y + this.DOTS_DENSITY;

    let i = 0
    while (y < z && i < 3){
      slices.push(this.calculateSlice(x, y));

      y += 8
    }

    return slices;
  }

  private manipulateImage(): number[] {
    let data = [];
    const imageWidth = this.imageData.width;

    for (let y = 0; y < this.imageData.height; y += this.DOTS_DENSITY){
        data.push(...this.ESC_BIT_IMAGE, 33, (0x00ff & imageWidth), (0xff00 & imageWidth) >> 8);

        for (let x = 0; x < imageWidth; x++) {
            data.push(...this.collectStripe(x, y));
        }

        data.push(this.LINE_FEED);
    }

    return data;
  }

  public getUint8Array(): Uint8Array {
    let transformedImage = [];

  //  transformedImage.push(...this.ESC_INIT);
    transformedImage.push(...this.LINE_FEED_AMOUNT);
    transformedImage.push(...this.manipulateImage());
    transformedImage.push(...this.LINE_FEED_DEFAULT);

    return new Uint8Array(transformedImage);
  }
}

Use it like this:

let img = new Image();
img.src = "assets/logo.svg";
img.onload = () => {
  let imagePrintingService = new ImagePrintingService(img, true, 240, 56);
  this.escEncoder.raw(imagePrintingService.getUint8Array())
  }

PiegoIlTempo avatar Mar 23 '23 15:03 PiegoIlTempo

@PiegoIlTempo , thanks alot for ur Help, can i see (this.escEncoder)

mohamed-mostafa10 avatar Mar 27 '23 10:03 mohamed-mostafa10

Np man, I had the same problem trying to print images, actually let me know if it gets printed correctly since I tested it on few printers.

This is an example of a printing service in Angular 11 with usb and bluetooth:

import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import EscPosEncoder from 'esc-pos-encoder';
import { UsbDriver, BluetoothDriver, PrintService } from 'ngx-thermal-print';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { ImagePrintingService } from 'src/app/route-modules/web-app-root/services/image-printing-service';
import { formatToEuro } from 'src/app/shared/pipes/euro.pipe';
import { ConnectThermalPrinterDialogComponent } from '../components/connect-thermal-printer-dialog/connect-thermal-printer-dialog.component';

enum DeviceType {
  USB,
  BLUETOOTH
}
@Injectable({
    providedIn: 'root',
})
export class ThermalPrinterService {
    private readonly AUTO_PRINT_RECEIPT = "auto_print_receipt"
    private readonly PRINTER_DEVICE = "printer_device"

    private readonly productsTableFormat = [
        { width: 18, marginRight: 1, align: 'left' },
        { width: 4, align: 'right' },
        { width: 9, align: 'right' }
    ]

    private readonly escEncoder = new EscPosEncoder({
        imageMode: 'legacy',
        codepageMapping: 'legacy'
    });
    private usbPrintDriver = new UsbDriver();
    private bluetoothPrintDriver = new BluetoothDriver();
    readonly isConnected = new BehaviorSubject<boolean>(false)

    private codepage = "auto"

    constructor(private matDialog: MatDialog) {
      const device = this.getSavedPrinterDevice()
      if(device != null) {
        if(device.type == DeviceType.USB) {
          this.usbPrintDriver = new UsbDriver(device.vendorId, device.productId)
          this.usbPrintDriver.connect()
        }
      }
      
      const isConnectedObservable = combineLatest([this.usbPrintDriver.isConnected, this.bluetoothPrintDriver.isConnected]).pipe(
          map(([usb, bluetooth]) => {
              return usb || bluetooth
          })
      )

      isConnectedObservable.subscribe(this.isConnected)
    }

    private savePrinterDevice(type: DeviceType, vendorId: number, productId: number) {
      localStorage.setItem(this.PRINTER_DEVICE, JSON.stringify({type: type, vendorId: vendorId, productId: productId}));
    }

    private getSavedPrinterDevice(): any|null {
      const deviceData = localStorage.getItem(this.PRINTER_DEVICE)
      if(deviceData != null) {
        return JSON.parse(deviceData)
      }
      else return null;
    }

    private deleteSavedPrinterDevice() {
      localStorage.removeItem(this.PRINTER_DEVICE)
    }

    disconnect() {
      this.usbPrintDriver = new UsbDriver()
      this.bluetoothPrintDriver = new BluetoothDriver()
      this.deleteSavedPrinterDevice()
      window.location.reload()
    }

    connectUsbPrinter() {
        this.usbPrintDriver.requestUsb().subscribe(
          (result) => {
            // HACK to this fucking brand that doesn't support any codepage except chinese
            if (result.manufacturerName == "GEZHI") {
                this.codepage = "cp54936"
            } else {
                this.codepage = "auto"
            }
            this.usbPrintDriver.connect()
            this.savePrinterDevice(DeviceType.USB, result.vendorId, result.productId)
          },
          (error) => {
            console.log(error);
          }
        );
    }

    connectBluetoothPrinter() {
        this.bluetoothPrintDriver.requestBluetooth().subscribe(
            (result) => {
                // HACK to this fucking brand that doesn't support any codepage except chinese
                if (result.name == "MTP-II") {
                    this.codepage = "cp54936"
                } else {
                    this.codepage = "auto"
                }
                this.bluetoothPrintDriver.connect()
            },
            (error) => {
                console.log(error);
            }
        )
    }

    get hasPrinterSupport(): boolean {
        return this.usbPrintDriver.isSupported || this.bluetoothPrintDriver.isSupported
    }

    get isBluetoothSupported(): boolean {
        return this.bluetoothPrintDriver.isSupported   
    }

    get isUsbSupported(): boolean {
        return this.usbPrintDriver.isSupported   
    }

    set autoPrintReceipt(value: boolean) {
        localStorage.setItem(this.AUTO_PRINT_RECEIPT, value ? "1" : "0")
    }

    get autoPrintReceipt(): boolean {
        const value = localStorage.getItem(this.AUTO_PRINT_RECEIPT)
        if (value == null) {
            return true
        } else {
            return value == "1"
        }
    }

    connectPrinterDialog() {
        ConnectThermalPrinterDialogComponent.openDialog(this.matDialog, this)
    }

    private endOfPrint() {
        this.escEncoder.newline()
        this.escEncoder.newline()
        this.escEncoder.newline()
        this.escEncoder.newline()
        this.escEncoder.cut()
        this.writeBuffer(this.escEncoder.encode())
    }

    printCheckout(documentId: string, checkhoutDetails: CheckoutDetails) {
        if (this.isConnected.value) {
            this.internalPrintScontrino(documentId, scontrinoDetails)
        } else {
            ConnectThermalPrinterDialogComponent.openDialog(this.matDialog, this).subscribe(result => {
                if (result) {
                    this.internalPrintScontrino(documentId, scontrinoDetails)
                }
            })   
        }
    }

    private async internalPrintCheckout(documentId: string, checkhoutDetails: CheckoutDetails) {
        this.escEncoder.codepage(this.codepage as any)
        this.escEncoder.align("left")
        this.escEncoder.newline()

        this.escEncoder.bold(true)
        this.escEncoder.table(
            this.productsTableFormat,
            [
                ["", "VAT", "EUR"],
            ]
        )
        this.escEncoder.bold(false)
        this.escEncoder.table(
            this.productsTableFormat, [[/* your table */]]
        )

        this.escEncoder.line(documentId)

        this.escEncoder.newline()
        this.escEncoder.line("Thank you, come again!")
        this.escEncoder.newline()

        let logo = new Image();
        logo.src = "assets/logo.svg";
        logo.onload = () => {
          let imagePrintingService = new ImagePrintingService(logo, true, 240, 56);
          this.escEncoder.raw(imagePrintingService.getUint8Array())

          if(checkoutDetails.referral_link != null) {
            this.escEncoder.bold(true)        
            this.escEncoder.line("Download the app!") 
            this.escEncoder.qrcode(checkoutDetails.referral_link, 1, 8)
            this.escEncoder.bold(false)
          }

          this.escEncoder.align("left")
  
          this.endOfPrint()
        }
    }

    private writeBuffer(buffer: Uint8Array) {
        if (this.usbPrintDriver.isConnected.value) {
            this.usbPrintDriver.write(buffer)
        } else if (this.bluetoothPrintDriver.isConnected.value) {
            this.bluetoothPrintDriver.write(buffer)
        }
    }
}

NOTE: The EscPosEncoder has an image() printing method, but it never worked at all for me, so that's why I print images using raw(), it might work for you out of the box maybe.

PiegoIlTempo avatar Mar 27 '23 15:03 PiegoIlTempo