📄 src/main.rs
mod proto;

use std::env::args;
use std::time::Duration;

use escpos::driver::NetworkDriver;
use escpos::printer::Printer;
use escpos::utils::{BitImageOption, BitImageSize, Protocol};
use tonic::{Request, Response, Status, transport::Server};

use proto::receipt_printer_server::{ReceiptPrinter, ReceiptPrinterServer};
use proto::{PrintRequest, PrintResponse};
use typst::foundations::{Dict, IntoValue};
use typst::layout::PagedDocument;
use typst_as_lib::TypstEngine;
use typst_as_lib::typst_kit_options::TypstKitFontOptions;

use crate::proto::TypstContent;
use crate::proto::print_request::Content;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<_> = args().skip(1).collect();
    if args.len() != 2 {
        println!("Please provide the printer connection string and .typ template as arguments");
        return Ok(());
    };
    let Some((printer_host, printer_port)) = args[0]
        .split_once(':')
        .map(|(a, b)| b.parse::<u16>().ok().map(|b| (a, b)))
        .flatten()
    else {
        println!("Printer connection string must be on format `host:port`");
        return Ok(());
    };
    let template_path = &args[1];

    let receipt_printer_service = ReceiptPrinterService {
        template_path: template_path.to_owned(),
        printer_host: printer_host.to_owned(),
        printer_port: printer_port,
    };

    Server::builder()
        .add_service(ReceiptPrinterServer::new(receipt_printer_service))
        .serve("[::1]:50051".parse()?)
        .await?;

    Ok(())
}

#[derive(Debug)]
pub struct ReceiptPrinterService {
    template_path: String,
    printer_host: String,
    printer_port: u16,
}

#[tonic::async_trait]
impl ReceiptPrinter for ReceiptPrinterService {
    async fn print(
        &self,
        request: Request<PrintRequest>,
    ) -> Result<Response<PrintResponse>, Status> {
        let Some(Content::Typst(TypstContent {
            content: Some(content),
        })) = &request.get_ref().content
        else {
            println!("No Typst content received");
            return Ok(Response::new(PrintResponse::default()));
        };

        let engine = TypstEngine::builder()
            .with_file_system_resolver(".")
            .search_fonts_with(TypstKitFontOptions::new().include_system_fonts(true))
            .build();

        println!("Compiling document...");
        let mut inputs = Dict::new();
        inputs.insert("content".into(), content.clone().into_value());
        let document: PagedDocument = match engine
            .compile_with_input(self.template_path.as_ref(), inputs)
            .output
        {
            Ok(document) => document,
            Err(error) => {
                println!("Failed to compile with message: {error}");
                return Ok(Response::new(PrintResponse::default()));
            }
        };

        let width_pixel = 72 * 8;
        let width_cm = 7.2;
        let width_pt = width_cm / 0.03527;
        let pixel_per_pt = width_pixel as f32 / width_pt;

        println!("Rendering document...");
        let pixmap = typst_render::render(&document.pages[0], pixel_per_pt);
        println!("Pixel size: {}x{}", pixmap.width(), pixmap.height());
        let png = match pixmap.encode_png() {
            Ok(png) => png,
            Err(error) => {
                println!("Could not encode image to PNG: {error}");
                return Ok(Response::new(PrintResponse::default()));
            }
        };

        println!("Connecting to printer...");
        let driver = match NetworkDriver::open(
            self.printer_host.as_ref(),
            self.printer_port,
            Some(Duration::from_secs(5)),
        ) {
            Ok(driver) => driver,
            Err(error) => {
                println!("Error connecting to printer: {error}");
                return Ok(Response::new(PrintResponse::default()));
            }
        };
        println!("Initializing printer...");
        let mut printer = Printer::new(driver, Protocol::default(), None);
        if let Err(error) = printer.init() {
            println!("Error initializing printer: {error}");
            return Ok(Response::new(PrintResponse::default()));
        }
        println!("Sending image to printer...");
        if let Err(error) = printer.bit_image_from_bytes_option(
            &png,
            BitImageOption::new(Some(width_pixel), None, BitImageSize::Normal).unwrap(),
        ) {
            println!("Error when printing document: {error}");
            return Ok(Response::new(PrintResponse::default()));
        }
        println!("Printing...");
        if let Err(error) = printer.print_cut() {
            println!("Error when printing: {error}");
            return Ok(Response::new(PrintResponse::default()));
        }
        println!("Done!");

        Ok(Response::new(PrintResponse::default()))
    }
}