flycast/core/hw/naomi/printer.cpp

1251 lines
29 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 <https://www.gnu.org/licenses/>.
*/
#include "types.h"
#include "stdclass.h"
#include "printer.h"
#include "serialize.h"
#include "oslib/oslib.h"
#include <cassert>
#include <memory>
#include <vector>
#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 <stb/stb_image.h>
#include <stb/stb_image_write.h>
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<typename T>
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<u8>& v = *(std::vector<u8> *)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<u8[]> 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<u8> 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<u8> 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<u8[]> 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<u8[]> data = std::make_unique<u8[]>(sz);
fread(data.get(), sz, 1, f);
fclose(f);
return data;
}
int printerWidth = 920;
std::vector<u8> 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<u8> data;
};
std::vector<CustomChar> customChars;
int maxCharWidth = 48; // depends on the printer type? 48 for tduno, 64 for ntvmys
std::vector<u8> ruledLine;
bool ruledLineMode = false;
int underline = 0;
int maxUnderline = 0;
std::unique_ptr<u8[]> ascii8x16;
std::unique_ptr<u8[]> ascii12x24;
std::unique_ptr<u8[]> kanji16x16;
std::unique_ptr<u8[]> 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("<<<full cut>>>\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<u8> 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<char> dataBytes;
bool kanji = false;
char kanjiByte0 = 0;
struct Bitmap
{
int width;
int height;
bool msb;
std::vector<u8> data;
};
std::vector<Bitmap> bitmaps;
BitmapWriter& getWriter()
{
if (bitmapWriter == nullptr)
bitmapWriter = std::make_unique<BitmapWriter>(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> bitmapWriter;
};
static std::unique_ptr<ThermalPrinter> printer;
void init()
{
printer = std::make_unique<ThermalPrinter>();
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