/* Copyright 2023 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "types.h" #include "stdclass.h" #include "printer.h" #include "serialize.h" #include "oslib/oslib.h" #include #include #include #ifdef STANDALONE_TEST #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION #undef INFO_LOG #define INFO_LOG(t, s, ...) printf(s "\n", __VA_ARGS__) #undef NOTICE_LOG #define NOTICE_LOG INFO_LOG #undef ERROR_LOG #define ERROR_LOG INFO_LOG #else #include "oslib/resources.h" #endif #include #include namespace printer { static u8 reverseBits(u8 b) { b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; b = (b & 0xCC) >> 2 | (b & 0x33) << 2; b = (b & 0xAA) >> 1 | (b & 0x55) << 1; return b; } class BitmapWriter { public: BitmapWriter(int printerWidth) : printerWidth(printerWidth) { #ifndef STANDALONE_TEST size_t size; ascii8x16 = resource::load("fonts/printer_ascii8x16.bin", size); ascii12x24 = resource::load("fonts/printer_ascii12x24.bin", size); kanji16x16 = resource::load("fonts/printer_kanji16x16.bin", size); kanji24x24 = resource::load("fonts/printer_kanji24x24.bin", size); #else ascii8x16 = loadFont("printer_ascii8x16.bin"); ascii12x24 = loadFont("printer_ascii12x24.bin"); kanji16x16 = loadFont("printer_kanji16x16.bin"); kanji24x24 = loadFont("printer_kanji24x24.bin"); #endif } template void print(T text) { if (text == '\n' || text == '\r') { linefeed(); return; } const int hScale = 1 + doubleHeight; const int wScale = 1 + doubleWidth; const u8 *glyph; int width; int height; bool msb = true; if (sizeof(T) == 1 && customCharsEnabled && (u8)text < customChars.size() && customChars[(u8)text].width > 0) { glyph = &customChars[text].data[0]; width = customChars[text].width; // / wScale; // FIXME tduno2 hack, breaks tduno height = customChars[text].height; msb = msbBitmap; } else { glyph = getGlyph(text); width = sizeof(T) == 1 ? bigFont ? 12 : 8 : bigFont ? 24 : 16; height = bigFont ? 24 : 16; } if (penx + width * wScale > printerWidth) linefeed(); maxLineHeight = std::max(maxLineHeight, height * hScale + maxUnderline); u8 *pen = getPenPosition(height * hScale); for (int y = 0; y < height; y++) { for (int dbh = 0; dbh < hScale; dbh++) { for (int x = 0; x < width && pen <= &page.back(); x++) { const u8 *src = glyph + x / 8; bool b = *src & (msb ? 0x80 >> (x % 8) : 1 << (x % 8)); b ^= reversed; if (b) { if (xorMode) *pen ^= 0xff; else *pen |= 0xff; } pen++; if (wScale > 1 && pen <= &page.back()) { if (b) { if (xorMode) *pen ^= 0xff; else *pen |= 0xff; } pen++; } } pen += printerWidth - width * wScale; } glyph += (width + 7) / 8; } if (!reversed) // reversed has priority over underline for (int y = 0; y < maxUnderline; y++) { u8 *pen = getPenPosition(height * hScale + y) + printerWidth * (height * hScale + y); const int ulwidth = width * wScale + hspace * (sizeof(T) == 1 ? 1 : 2); for (int x = 0; x < ulwidth && pen <= &page.back(); x++) { if (xorMode) *pen ^= 0xff; else *pen |= 0xff; pen++; } } penx += width * wScale; if (reversed) { // Fill inter-char space // FIXME only if there's another char on the same line u8 *pen = getPenPosition(height * hScale); for (int y = 0; y < height * hScale; y++) { for (int x = 0; x < hspace && pen <= &page.back(); x++) { if (xorMode) *pen++ ^= 0xff; else *pen++ |= 0xff; } pen += printerWidth - hspace; } } penx += hspace * (sizeof(T) == 1 ? 1 : 2); printBufferEmpty = false; } void printImage(int hpos, int width, int height, const u8 *data, bool msb) { //printf("printImage: hpos %d w %d h %d msb %d\n", hpos, width, height, msb); const int savePenx = penx; penx = hpos; u8 *pen = getPenPosition(height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { bool b = data[x / 8] & (msb ? 0x80 >> (x % 8) : 1 << (x % 8)); if (b) { if (xorMode) *pen ^= 0xff; else *pen |= 0xff; } pen++; } data += (width + 7) / 8; pen += printerWidth - width; } penx = savePenx; if (_advancePenOnBitmap) // F355 and tduno[2] seem to handle bitmap differently. tduno[2] needs the pen to advance when printing bitmaps, // and will do reverse feed to print over it. F355 doesn't reverse feed so the pen must not advance. peny += height; } void linefeed(int dots) { if (!printBufferEmpty) linefeed(); if (dots > 0) getPenPosition(dots); penx = 0; peny = std::max(0, peny + dots); } void selectFont(bool big) { bigFont = big; } void setReversed(bool enable) { reversed = enable; } void setHSpace(int dots) { hspace = dots; } void setVSpace(int dots) { vspace = dots; } void setDoubleWidth(bool enabled) { doubleWidth = enabled; } void setDoubleHeight(bool enabled) { doubleHeight = enabled; } bool getBitmapMSB() const { return msbBitmap; } void setBitmapMSB(bool enabled) { msbBitmap = enabled; } void setXorMode(bool enabled) { xorMode = enabled; } void setMaxCharWidth(int width) { maxCharWidth = width; } void setCustomChar(char code, int width, int height, const u8 *data) { if ((u8)code >= customChars.size()) customChars.resize(code + 1); CustomChar& cchar = customChars[code]; cchar.width = std::min(width, maxCharWidth); cchar.height = height; cchar.data.resize((cchar.width + 7) / 8 * height); if (msbBitmap) { int widthBytes = (cchar.width + 7) / 8; for (int y = 0; y < height; y++) for (int x = 0; x < widthBytes; x++) cchar.data[widthBytes * y + x] = reverseBits(data[(width + 7) / 8 * y + x]); } else if (cchar.width == width) { memcpy(cchar.data.data(), data, cchar.data.size()); } else { for (int y = 0; y < height; y++) memcpy(cchar.data.data() + cchar.width / 8 * y, data + (width + 7) / 8 * y, cchar.width / 8); } } void enableCustomChars(bool enable) { customCharsEnabled = enable; } void drawRuledLine(int from, int to) { if (from > to) std::swap(from, to); if (ruledLine.empty()) ruledLine.resize(printerWidth); for (int d = from; d <= to && d < (int)ruledLine.size(); d++) ruledLine[d] = 0xff; } void drawRuledPattern(u8 n1, u8 n2) { if (ruledLine.empty()) ruledLine.resize(printerWidth); u8 pattern[16]; for (int i = 0; i < 8; i++) { u8 mask = msbBitmap ? 0x80 >> i : 1 << i; pattern[i] = (n1 & mask) ? 0xff : 0; pattern[8 + i] = (n2 & mask) ? 0xff : 0; } for (int d = 0; d < (int)ruledLine.size(); d++) ruledLine[d] = pattern[d % std::size(pattern)]; } void clearRuledLine() { ruledLine.clear(); } void printRuledLine() { if (!ruledLineMode) { linefeed(1); return; } if (!printBufferEmpty) linefeed(); penx = 0; u8 *pen = getPenPosition(1); for (int x = 0; x < printerWidth && x < (int)ruledLine.size(); x++) { if (ruledLine[x] != 0) { if (xorMode) *pen ^= 0xff; else *pen |= 0xff; } pen++; } peny++; } void setRuledLineMode(bool enabled) { ruledLineMode = enabled; } void advancePenOnBitmap() { _advancePenOnBitmap = true; } void setUnderline(int dots) { underline = dots; maxUnderline = std::max(maxUnderline, underline); } bool isDirty() const { return lines > 0; } bool save(const std::string& filename) { if (page.empty()) return false; const auto& appendData = [](void *context, void *data, int size) { std::vector& v = *(std::vector *)context; v.insert(v.end(), (u8 *)data, (u8 *)data + size); }; stbi_flip_vertically_on_write(0); if (settings.content.gameId.substr(0, 4) == "F355") { u8 *data = nullptr; int x, y, comp; #ifndef STANDALONE_TEST size_t size; std::unique_ptr fileData = resource::load("picture/f355_print_template.png", size); data = stbi_load_from_memory(fileData.get(), size, &x, &y, &comp, STBI_rgb_alpha); #else FILE *f = fopen("f355_print_template.png", "rb"); if (f != nullptr) { data = stbi_load_from_file(f, &x, &y, &comp, STBI_rgb_alpha); fclose(f); } else fprintf(stderr, "Can't open template file %d\n", errno); #endif if (data != nullptr && (x != printerWidth || comp != STBI_rgb_alpha)) { ERROR_LOG(NAOMI, "Invalid printer template: width %d comp %d", x, comp); stbi_image_free(data); data = nullptr; } if (data != nullptr) { if (lines > y) { u8 *newData = (u8 *)malloc(printerWidth * 4 * lines); const u8 *end = newData + printerWidth * 4 * lines; for (u8 *p = newData; p < end; p += printerWidth * 4 * y) memcpy(p, data, std::min(printerWidth * 4 * y, (int)(end - p))); stbi_image_free(data); data = newData; } u32 *p = (u32 *)data; for (u8 b : page) { if (b == 0xff) *p = 0xff000000; p++; } std::vector pngData; stbi_write_png_to_func(appendData, &pngData, printerWidth, lines, 4, data, printerWidth * 4); stbi_image_free(data); try { hostfs::saveScreenshot(filename, pngData); } catch (const FlycastException& e) { ERROR_LOG(NAOMI, "Error saving print out: %s", e.what()); return false; } return true; } } for (u8& b : page) b = 0xff - b; std::vector pngData; stbi_write_png_to_func(appendData, &pngData, printerWidth, lines, 1, &page[0], printerWidth); try { hostfs::saveScreenshot(filename, pngData); } catch (const FlycastException& e) { ERROR_LOG(NAOMI, "Error saving print out: %s", e.what()); return false; } return true; } void serialize(Serializer& ser) { ser << printerWidth; ser << (u32)page.size(); ser.serialize(page.data(), page.size()); ser << lines; ser << penx; ser << peny; ser << vspace; ser << hspace; ser << bigFont; ser << reversed; ser << doubleWidth; ser << doubleHeight; ser << msbBitmap; ser << maxLineHeight; ser << xorMode; ser << _advancePenOnBitmap; ser << printBufferEmpty; ser << customCharsEnabled; ser << (u32)customChars.size(); for (const CustomChar& cc : customChars) { ser << cc.width; ser << cc.height; ser << (u32)cc.data.size(); ser.serialize(cc.data.data(), cc.data.size()); } ser << (u32)ruledLine.size(); ser.serialize(ruledLine.data(), ruledLine.size()); ser << ruledLineMode; ser << underline; ser << maxUnderline; } void deserialize(Deserializer& deser) { deser >> printerWidth; u32 size; deser >> size; page.resize(size); deser.deserialize(page.data(), page.size()); deser >> lines; deser >> penx; deser >> peny; deser >> vspace; deser >> hspace; deser >> bigFont; deser >> reversed; deser >> doubleWidth; deser >> doubleHeight; deser >> msbBitmap; deser >> maxLineHeight; deser >> xorMode; deser >> _advancePenOnBitmap; deser >> printBufferEmpty; deser >> customCharsEnabled; deser >> size; customChars.resize(size); for (CustomChar& cc : customChars) { deser >> cc.width; deser >> cc.height; deser >> size; cc.data.resize(size); deser.deserialize(cc.data.data(), cc.data.size()); } deser >> size; ruledLine.resize(size); deser.deserialize(ruledLine.data(), ruledLine.size()); deser >> ruledLineMode; deser >> underline; deser >> maxUnderline; } private: void linefeed() { if (maxLineHeight == 0) { const int height = bigFont ? 24 : 16; const int hScale = 1 + doubleHeight; maxLineHeight = height * hScale; } penx = 0; const int fromY = peny; peny += maxLineHeight + maxUnderline + vspace; maxUnderline = 0; maxLineHeight = 0; printBufferEmpty = true; if (ruledLineMode) { getPenPosition(0); for (int y = fromY; y < peny; y++) { u8 *pen = &page[y * printerWidth]; for (int x = 0; x < printerWidth && x < (int)ruledLine.size(); x++) { if (ruledLine[x] != 0) { if (xorMode) *pen ^= 0xff; else *pen |= 0xff; } pen++; } } } } const u8 *getGlyph(char c) { const u8 *glyph; int glyphSize; if (bigFont) { glyph = ascii12x24.get(); glyphSize = 2 * 24; } else { glyph = ascii8x16.get(); glyphSize = 16; } if ((uint8_t)c < ' ') return glyph; else return glyph + glyphSize * ((uint8_t)c - ' '); } const u8 *getGlyph(wchar_t c) { const u8 *glyph; int glyphSize; if (bigFont) { glyph = kanji24x24.get(); glyphSize = 3 * 24; } else { glyph = kanji16x16.get(); glyphSize = 2 * 16; } if (c == ' ') return glyph; uint8_t plane = c >> 8; c &= 0xff; if (plane < 0x21 || plane > 0x7e || c < 0x21 || c > 0x7e) return glyph; else return glyph + glyphSize * (1 + (plane - 0x21) * 94 + (c - 0x21)); } u8 *getPenPosition(int height) { if (peny + height > lines) addLines(peny + height - lines); return &page[peny * printerWidth + penx]; } void addLines(int count) { lines += count; page.insert(page.end(), printerWidth * count, 0); } std::unique_ptr loadFont(const char *fname) { FILE *f = fopen(fname, "rb"); if (!f) { perror(fname); return nullptr; } fseek(f, 0, SEEK_END); size_t sz = ftell(f); fseek(f, 0, SEEK_SET); std::unique_ptr data = std::make_unique(sz); fread(data.get(), sz, 1, f); fclose(f); return data; } int printerWidth = 920; std::vector page; int lines = 0; int penx = 0; int peny = 0; int vspace = 28; int hspace = 2; bool bigFont = false; bool reversed = false; bool doubleWidth = false; bool doubleHeight = false; bool msbBitmap = false; int maxLineHeight = 0; bool xorMode = false; bool _advancePenOnBitmap = false; bool printBufferEmpty = false; bool customCharsEnabled = false; struct CustomChar { int width; int height; std::vector data; }; std::vector customChars; int maxCharWidth = 48; // depends on the printer type? 48 for tduno, 64 for ntvmys std::vector ruledLine; bool ruledLineMode = false; int underline = 0; int maxUnderline = 0; std::unique_ptr ascii8x16; std::unique_ptr ascii12x24; std::unique_ptr kanji16x16; std::unique_ptr kanji24x24; }; // // Nichipri NP413-FA esc/pos printer emulation // Nichipri Industrial is now Nippon Primex // class ThermalPrinter { public: void setMaxCharWidth(int width) { getWriter().setMaxCharWidth(width); } void print(char c) { if (expectedDataBytes > 0) { dataBytes.push_back(c); if (expectedDataBytes == dataBytes.size()) { switch (state) { case ESC: executeEscCommand(); break; case DC2: executeDc2Command(); break; case DC3: executeDc3Command(); break; default: assert(false); break; } if (expectedDataBytes == dataBytes.size()) { expectedDataBytes = 0; if (!dc3Lock || state != DC3) state = Default; dataBytes.clear(); } } } else { switch (state) { case ESC: escCommand(c); break; case DC2: dc2Command(c); break; case DC3: dc3Command(c); break; default: switch (c) { case 0x1b: // ESC state = ESC; break; case 0x12: // DC2 state = DC2; break; case 0x13: // DC3 state = DC3; break; case '\r': case '\n': //printf("\\r\n"); getWriter().print('\r'); break; case 0x18: // CAN Erase print buffer case '\0': // ignore break; default: if (kanji) { if (kanjiByte0 == 0) { if (c <= ' ') getWriter().print(c); else kanjiByte0 = c; } else { wchar_t code = ((u8)kanjiByte0 << 8) | (u8)c; //printf("[%x]", code); fflush(stdout); getWriter().print(code); kanjiByte0 = 0; } } else { //if (c >= ' ') // printf("%c", c); //else // printf("[%02x]", (u8)c); getWriter().print(c); } break; } } } } void serialize(Serializer& ser) { ser << state; ser << dc3Lock; ser << curCommand; ser << expectedDataBytes; ser << (u32)dataBytes.size(); ser.serialize(dataBytes.data(), dataBytes.size()); ser << kanji; ser << kanjiByte0; ser << (u32)bitmaps.size(); for (const Bitmap& bm : bitmaps) { ser << bm.width; ser << bm.height; ser << bm.msb; ser << (u32)bm.data.size(); ser.serialize(bm.data.data(), bm.data.size()); } if (bitmapWriter == nullptr) ser << false; else { ser << true; bitmapWriter->serialize(ser); } } void deserialize(Deserializer& deser) { deser >> state; deser >> dc3Lock; deser >> curCommand; deser >> expectedDataBytes; u32 size; deser >> size; dataBytes.resize(size); deser.deserialize(dataBytes.data(), dataBytes.size()); deser >> kanji; deser >> kanjiByte0; deser >> size; bitmaps.resize(size); for (Bitmap& bm : bitmaps) { deser >> bm.width; deser >> bm.height; if (deser.version() >= Deserializer::V48) deser >> bm.msb; else bm.msb = false; deser >> size; bm.data.resize(size); deser.deserialize(bm.data.data(), bm.data.size()); } bool b; deser >> b; if (!b) bitmapWriter.reset(); else getWriter().deserialize(deser); } private: void escCommand(char c) { curCommand = c; switch (c) { case '3': // smallest LF pitch set (same as A) case 'A': // line space setting (n dots) case 'J': // smallest pitch line feed case 'j': // Reverse paper feed after printing (n dots) case ' ': // Character right space set (n dots) case 'I': // Reversed b/w character on/off case 'W': // set/reset double-width chars case 'w': // set/reset double-height chars case '=': // Image LSB/MSB selection case '#': // Overlay mode selection (0: OR, 1: XOR) case '-': // Set/reset underline expectedDataBytes = 1; break; case 'i': // Full cut //printf("<<>>\n"); state = Default; if (bitmapWriter && bitmapWriter->isDirty()) { std::string date = timeToISO8601(time(nullptr)); std::replace(date.begin(), date.end(), '/', '-'); std::replace(date.begin(), date.end(), ':', '-'); std::string s = settings.content.gameId + " - " + date + ".png"; const bool success = bitmapWriter->save(s); bitmapWriter.reset(); if (success) { os_notify("Print out saved", 5000, s.c_str()); NOTICE_LOG(NAOMI, "%s", s.c_str()); } } break; case 'K': // Set Kanji mode kanji = true; state = Default; break; case 'H': // Cancel Kanji mode kanji = false; state = Default; break; case '2': getWriter().setVSpace(16); state = Default; break; case 'E': // set/reset Emphasized Characters FIXME doesn't seem to take a param in tduno case 'F': // Undocumented?? state = Default; break; default: // unhandled but need to ignore data... INFO_LOG(NAOMI, "Unhandled ESC [%c]\n", c); state = Default; break; } } void executeEscCommand() { switch (curCommand) { case '3': case 'A': getWriter().setVSpace((u8)dataBytes[0]); break; case 'I': getWriter().setReversed(dataBytes[0] & 1); break; case 'J': getWriter().linefeed((u8)dataBytes[0]); break; case ' ': getWriter().setHSpace((u8)dataBytes[0] & 0x7f); break; case 'W': getWriter().setDoubleWidth(dataBytes[0] & 1); break; case 'w': getWriter().setDoubleHeight(dataBytes[0] & 1); break; case '-': getWriter().setUnderline(dataBytes[0] & 7); break; case '=': getWriter().setBitmapMSB((dataBytes[0] & 1) != 0); break; case '#': getWriter().setXorMode(dataBytes[0] & 1); break; case 'j': getWriter().linefeed(-(u8)dataBytes[0]); break; default: //printf("ESC %c", curCommand); //for (auto c : dataBytes) // printf(" %d", (u8)c); //printf("\n"); break; } } void dc2Command(char c) { curCommand = c; switch (c) { case 'R': // Read memory switch (should take a param but apparently doesn't ?) case 'V': // Raster bit image printing (w h [bytes ...]) FIXME tduno: w=0, h=0 and no data (?) or no param at all, f355: 1 param only (0) state = Default; break; case 'D': // Allocate/free download char area case 'F': // Font size selection. 0: Font B (8×16, 16×16), 1: Font A (12×24, 24×24) case 'G': // Allocate/free external char area case 'U': // Deletes the bitmap specified by n and releases the memory area used case 'p': // Select out-of-paper error ??? probably not case '~': // Set print density setting (n% with n in [50, 200]) case 'O': // set/reset optional font printing expectedDataBytes = 1; break; case 'S': // Select a bitmap (n1) and specify the print position in the horizontal direction (n2 * 8 dots) expectedDataBytes = 2; break; case 'T': // Create bitmap: n w yl yh [data...] where n is the bitmap id [0, 255], w is the width in bytes [1, 127], // yl + yh * 256 is the number of lines [1, 1023] expectedDataBytes = 4; break; case 'm': // Mark position detection: s nl nh where n is the max feed amount until detection [0, 65535] // s & 3 == 0: Feed the paper in the forward direction until it passes the marking position. // s & 3 == 1: Feed the paper in the forward direction to the marking position. // s & 3 == 2: Feed the paper in the opposite direction until it passes the marking position. // s & 3 == 3: Feed the paper in the opposite direction to the marking position. expectedDataBytes = 3; break; case 'P': // Register Optional Font? expectedDataBytes = 4; break; default: // unhandled but need to ignore data... INFO_LOG(NAOMI, "Unhandled DC2 [%c]\n", c); state = Default; break; } } void executeDc2Command() { switch (curCommand) { case 'F': getWriter().selectFont(dataBytes[0] & 1); break; case 'O': getWriter().enableCustomChars(dataBytes[0] & 1); break; case 'P': // Register optional font: s e y x [data...] k // s: start char code [20, 7e] // e: end char code [20, 7e] // y: vertical dots [1, 7f] // x: horizontal dots [1, 7f] // d: data bytes // k: data bytes count = INT((y + 7) / 8) × x × (e - s + 1) if (expectedDataBytes == 4) { expectedDataBytes += ((dataBytes[2] & 0x7f) + 7) / 8 * (dataBytes[3] & 0x7f) * ((u8)dataBytes[1] - (u8)dataBytes[0] + 1); } else { const char s = dataBytes[0] & 0x7f; const char e = dataBytes[1] & 0x7f; const int y = dataBytes[2] & 0x7f; const int x = dataBytes[3] & 0x7f; const u8 *p = (u8 *)&dataBytes[4]; const int charSize = (x + 7) / 8 * y; for (char c = s; c <= e; c++) { getWriter().setCustomChar(c, x, y, p); p += charSize; } //printf("Characters %c to %c: %d x %d\n", s, e, x, y); } break; case 'S': { const size_t idx = (u8)dataBytes[0]; const int hpos = (u8)dataBytes[1] * 8; if (idx < bitmaps.size()) getWriter().printImage(hpos, bitmaps[idx].width, bitmaps[idx].height, &bitmaps[idx].data[0], bitmaps[idx].msb); } break; case 'T': { // bitmap if (expectedDataBytes == 4) { expectedDataBytes += ((u8)dataBytes[1] & 0x7f) * std::min((u8)dataBytes[2] | ((u8)dataBytes[3] << 8), 1023); } else { const size_t idx = (u8)dataBytes[0]; const int w = (dataBytes[1] & 0x7f) * 8; const int h = std::min((u8)dataBytes[2] | ((u8)dataBytes[3] << 8), 1023); //printf("bitmap[%zd] %d x %d\n", idx, w, h); // B&W 1bit std::vector bitmap(w / 8 * h); memcpy(bitmap.data(), &dataBytes[4], bitmap.size()); if (bitmaps.size() <= idx) bitmaps.resize(idx + 1); bitmaps[idx].width = w; bitmaps[idx].height = h; bitmaps[idx].msb = getWriter().getBitmapMSB(); std::swap(bitmaps[idx].data, bitmap); } } break; case 'U': { const u32 idx = (u8)dataBytes[0]; if (idx < bitmaps.size()) { bitmaps[idx].width = bitmaps[idx].height = 0; bitmaps[idx].data.clear(); } } break; case 'p': // This very unlikely related to bitmaps but is used to distinguish between f355 and tduno[2] handling of bitmaps. if (dataBytes[0] != 0) getWriter().advancePenOnBitmap(); break; default: //printf("DC2 %c", curCommand); //for (auto c : dataBytes) // printf(" %d", (u8)c); //printf("\n"); break; } } void dc3Command(char c) { curCommand = c; switch (c) { case '(': dc3Lock = true; break; case ')': dc3Lock = false; state = Default; break; case '+': // Enable ruled line buffer printing if (!dc3Lock) state = Default; getWriter().setRuledLineMode(true); break; case '-': // Disable ruled line buffer printing if (!dc3Lock) state = Default; getWriter().setRuledLineMode(false); break; case 'A': // Select Ruled line buffer A case 'B': // Select Ruled line buffer B if (!dc3Lock) state = Default; break; case 'C': // Clear rule buffer if (!dc3Lock) state = Default; getWriter().clearRuledLine(); break; case 'D': // Dot fill. Writes "1" (black) at dot position specified by (nh * 256 + nl) * dotpitch in the selected ruled line buffer. // nl[0-255] nh[0-3] expectedDataBytes = 2; break; case 'F': // Pattern fill. Writes one line to the selected ruled line buffer using the data pattern specified by n1 and n2. // pattern is arranged horizontally from left to right. (Each byte obey image lsb/msb selection ESC =) expectedDataBytes = 2; break; case 'P': // Prints the data in the print buffer and prints 1 line of the ruled line buffer if (!dc3Lock) state = Default; getWriter().printRuledLine(); break; case 'L': // Writes "1" (black) in the range from nhnl to mhml in the selected ruled line buffer. // ml mh nl nh expectedDataBytes = 4; break; default: // unhandled but need to ignore data... INFO_LOG(NAOMI, "Unhandled DC3 [%c]\n", c); if (!dc3Lock) state = Default; break; } } void executeDc3Command() { switch (curCommand) { case 'D': getWriter().drawRuledLine((u8)dataBytes[0] + (u8)dataBytes[1] * 256, (u8)dataBytes[0] + (u8)dataBytes[1] * 256); break; case 'F': getWriter().drawRuledPattern((u8)dataBytes[0], (u8)dataBytes[1]); break; case 'L': getWriter().drawRuledLine((u8)dataBytes[0] + (u8)dataBytes[1] * 256, (u8)dataBytes[2] + (u8)dataBytes[3] * 256); break; default: //printf("DC3 %c", curCommand); //for (auto c : dataBytes) // printf(" %d", (u8)c); //printf("\n"); break; } } enum { Default, ESC, DC2, DC3 } state = Default; bool dc3Lock = false; char curCommand = 0; unsigned expectedDataBytes = 0; std::vector dataBytes; bool kanji = false; char kanjiByte0 = 0; struct Bitmap { int width; int height; bool msb; std::vector data; }; std::vector bitmaps; BitmapWriter& getWriter() { if (bitmapWriter == nullptr) bitmapWriter = std::make_unique(832); // tduno test print: min 894, but uses 832 for rules lines // tduno2 test print ok with 832 // f355 ok with 832 return *bitmapWriter; } std::unique_ptr bitmapWriter; }; static std::unique_ptr printer; void init() { printer = std::make_unique(); if (settings.content.gameId == "MIRAI YOSOU STUDIO") printer->setMaxCharWidth(64); } void term() { printer.reset(); } void print(char c) { if (printer != nullptr) printer->print(c); } #ifndef STANDALONE_TEST void serialize(Serializer& ser) { if (printer != nullptr) printer->serialize(ser); } void deserialize(Deserializer& deser) { if (printer != nullptr) { if (deser.version() >= Deserializer::V35) printer->deserialize(deser); else init(); } } #endif } #ifdef STANDALONE_TEST settings_t settings; std::string get_writable_data_path(const std::string& s) { return "./" + s; } void os_notify(char const*, int, char const*) { } int main(int argc, char *argv[]) { if (argc < 2) return 1; FILE *f = fopen(argv[1], "rb"); if (f == nullptr) { perror(argv[1]); return 1; } settings.content.gameId = "F355 CHALLENGE"; printer::ThermalPrinter printer; // printer.setMaxCharWidth(64); for (;;) { int c = fgetc(f); if (c == EOF) break; printer.print((char)c); } fclose(f); return 0; } #endif