kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use crate::devices::decode_oem_text;
use printpdf::{
    Mm, Op, ParsedFont, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions, Point, Pt, TextItem,
};
use std::path::Path;

const FONT_BYTES: &[u8] = include_bytes!("../../../assets/fonts/RobotoMono.ttf");
const PAGE_WIDTH: Mm = Mm(210.0);
const PAGE_HEIGHT: Mm = Mm(297.0);
const MARGIN: Mm = Mm(18.0);
const FONT_SIZE: Pt = Pt(10.0);
const LINE_HEIGHT: Pt = Pt(13.0);
const COLUMNS_PER_LINE: usize = 80;
const LINES_PER_PAGE: usize = 56;

pub(super) fn write(path: &Path, spool: &[u8]) -> Result<(), String> {
    let font = ParsedFont::from_bytes(FONT_BYTES, 0, &mut Vec::new())
        .ok_or_else(|| "failed to load printer font".to_owned())?;
    let mut document = PdfDocument::new("KR580 printer output");
    let font_id = document.add_font(&font);
    let lines = printer_lines(spool);
    let pages = lines
        .chunks(LINES_PER_PAGE)
        .map(|page_lines| {
            let mut operations = vec![
                Op::StartTextSection,
                Op::SetFont {
                    font: PdfFontHandle::External(font_id.clone()),
                    size: FONT_SIZE,
                },
                Op::SetLineHeight { lh: LINE_HEIGHT },
                Op::SetTextCursor {
                    pos: Point {
                        x: MARGIN.into(),
                        y: Mm(PAGE_HEIGHT.0 - MARGIN.0).into(),
                    },
                },
            ];
            for (index, line) in page_lines.iter().enumerate() {
                if index != 0 {
                    operations.push(Op::AddLineBreak);
                }
                operations.push(Op::ShowText {
                    items: vec![TextItem::Text(line.clone())],
                });
            }
            operations.push(Op::EndTextSection);
            PdfPage::new(PAGE_WIDTH, PAGE_HEIGHT, operations)
        })
        .collect();
    let bytes = document
        .with_pages(pages)
        .save(&PdfSaveOptions::default(), &mut Vec::new());
    std::fs::write(path, bytes).map_err(|error| error.to_string())
}

fn printer_lines(spool: &[u8]) -> Vec<String> {
    let text = decode_oem_text(spool).replace('\t', "    ");
    let mut lines = Vec::new();
    for logical_line in text.split('\n') {
        let characters = logical_line.chars().collect::<Vec<_>>();
        if characters.is_empty() {
            lines.push(String::new());
            continue;
        }
        lines.extend(
            characters
                .chunks(COLUMNS_PER_LINE)
                .map(|chunk| chunk.iter().collect()),
        );
    }
    if lines.is_empty() {
        lines.push(String::new());
    }
    lines
}

#[cfg(test)]
mod tests {
    use super::printer_lines;

    #[test]
    fn printer_lines_preserve_blank_lines_and_wrap_at_eighty_columns() {
        let source = format!("{}\r\n\r\nB", "A".repeat(81));
        let lines = printer_lines(source.as_bytes());

        assert_eq!(lines[0], "A".repeat(80));
        assert_eq!(lines[1], "A");
        assert_eq!(lines[2], "");
        assert_eq!(lines[3], "B");
    }
}