From 510d1de054e1a1cbf52fcd16da36ee37b4c22be1 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Mon, 24 Sep 2018 18:11:21 -0400 Subject: Add psfed, a PSF2 font editor --- bin/.gitignore | 4 +- bin/Makefile | 2 +- bin/README | 1 + bin/man/bin.7 | 3 + bin/man/psfed.1 | 137 +++++++++++++++++ bin/psfed.c | 459 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 bin/man/psfed.1 create mode 100644 bin/psfed.c diff --git a/bin/.gitignore b/bin/.gitignore index e3c78ed3..6f818aea 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -1,6 +1,7 @@ tags scheme.h -*.o +fbatt.o +fbclock.o atch dtch glitch @@ -18,5 +19,6 @@ watch bri fbatt fbclock +psfed klon scheme.png diff --git a/bin/Makefile b/bin/Makefile index fd3f1134..ef6a1099 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -2,7 +2,7 @@ PREFIX = ~/.local BINS = atch dtch glitch hnel modem open pbcopy pbd pbpaste pngo scheme wake xx BINS_BSD = watch -BINS_LINUX = bri fbatt fbclock +BINS_LINUX = bri fbatt fbclock psfed BINS_ALL = $(BINS) $(BINS_BSD) $(BINS_LINUX) GAMES_BSD = klon diff --git a/bin/README b/bin/README index d5a4f032..859819b0 100644 --- a/bin/README +++ b/bin/README @@ -17,6 +17,7 @@ DESCRIPTION modem(1) fixed baud rate wrapper pbd(1) macOS pasteboard daemon pngo(1) PNG optimizer + psfed(1) PSF2 font editor wake(1) wake-on-LAN watch(1) watch files xx(1) hexdump diff --git a/bin/man/bin.7 b/bin/man/bin.7 index 71c33579..383f4bf0 100644 --- a/bin/man/bin.7 +++ b/bin/man/bin.7 @@ -46,6 +46,9 @@ macOS pasteboard daemon .It Xr pngo 1 PNG optimizer . +.It Xr psfed 1 +PSF2 font editor +. .It Xr wake 1 wake-on-LAN . diff --git a/bin/man/psfed.1 b/bin/man/psfed.1 new file mode 100644 index 00000000..227bf6b5 --- /dev/null +++ b/bin/man/psfed.1 @@ -0,0 +1,137 @@ +.Dd September 24, 2018 +.Dt PSFED 1 +.Os "Causal Agency" +. +.Sh NAME +.Nm psfed +.Nd PSF2 font editor +. +.Sh SYNOPSIS +.Nm +.Op Fl g Ar glyphs +.Op Fl h Ar height +.Op Fl w Ar width +.Ar file +. +.Sh DESCRIPTION +.Nm +is a PSF2 font editor +for the Linux framebuffer. +. +.Pp +The arguments are as follows: +. +.Bl -tag -width Ds +.It Fl g Ar glyphs +Set the number of glyphs in a new font. +The default number of glyphs is 256. +. +.It Fl h Ar height +Set the height of a new font. +The default height is 16. +. +.It Fl w Ar width +Set the width of a new font. +The default width is 8. +.El +. +.Ss Normal Mode +In normal mode, +each glyph is displayed in a grid. +. +.Pp +.Bl -tag -width Ds -compact +.It Ic q +Quit. +.Nm +will ask for confirmation +if the font has been modified +since the last write. +. +.It Ic w +Write font to +.Ar file . +. +.It Ic - Ic + +Adjust display scale. +. +.It Ic h Ic l +Select previous/next glyph. +. +.It Ic k Ic j +Select glyph in previous/next row. +. +.It Ic e +Edit the selected glyph in +.Sx Edit Mode . +. +.It Ic i +Enter +.Sx Preview Mode . +.El +. +.Ss Edit Mode +In edit mode, +the selected glyph is displayed for editing +surrounded by a checked border. +The glyph is also displayed unscaled +in the bottom-right corner. +. +.Pp +.Bl -tag -width Ds -compact +.It Ic ESC +Return to +.Sx Normal Mode . +. +.It Ic - Ic + +Adjust display scale. +. +.It Ic h Ic l +Select previous/next bit in row. +. +.It Ic k Ic j +Select previous/next bit in column. +. +.It Ic SPACE +Flip selected bit. +. +.It Ic u +Revert glyph to initial state. +.El +. +.Ss Preview Mode +In preview mode, +arbitrary text may be entered +for preview. +Press +.Ic ESC +to return to +.Sx Normal Mode . +. +.Sh ENVIRONMENT +.Bl -tag -width FRAMEBUFFER +.It Ev FRAMEBUFFER +The framebuffer device path. +The default path is +.Pa /dev/fb0 . +.El +. +.Sh SEE ALSO +.Xr psfaddtable 1 , +.Xr psfgettable 1 , +.Xr psfstriptable 1 , +.Xr setfont 8 +. +.Sh CAVEATS +.Nm +does not support Unicode tables. +Use +.Xr psfaddtable 1 +to add Unicode tables +to fonts created by +.Nm . +. +.Sh BUGS +.Nm +makes no attempt to convert header fields +to and from little-endian format. diff --git a/bin/psfed.c b/bin/psfed.c new file mode 100644 index 00000000..9db35cf3 --- /dev/null +++ b/bin/psfed.c @@ -0,0 +1,459 @@ +/* Copyright (C) 2018 June McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const wchar_t CP437[256] = + L"\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼" + L"►◄↕‼¶§▬↨↑↓→←∟↔▲▼" + L" !\"#$%&'()*+,-./" + L"0123456789:;<=>?" + L"@ABCDEFGHIJKLMNO" + L"PQRSTUVWXYZ[\\]^_" + L"`abcdefghijklmno" + L"pqrstuvwxyz{|}~⌂" + L"ÇüéâäàåçêëèïîìÄÅ" + L"ÉæÆôöòûùÿÖÜ¢£¥₧ƒ" + L"áíóúñѪº¿⌐¬½¼¡«»" + L"░▒▓│┤╡╢╖╕╣║╗╝╜╛┐" + L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧" + L"╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀" + L"αßΓπΣσµτΦΘΩδ∞φε∩" + L"≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\0"; + +static struct { + uint32_t width; + uint32_t height; + uint32_t *buffer; + uint32_t background; +} frame; + +static void frameClear(void) { + for (uint32_t i = 0; i < frame.width * frame.height; ++i) { + frame.buffer[i] = frame.background; + } +} + +static void frameOpen(void) { + const char *dev = getenv("FRAMEBUFFER"); + if (!dev) dev = "/dev/fb0"; + + int fd = open(dev, O_RDWR); + if (fd < 0) err(EX_OSFILE, "%s", dev); + + struct fb_var_screeninfo info; + int error = ioctl(fd, FBIOGET_VSCREENINFO, &info); + if (error) err(EX_IOERR, "%s", dev); + + frame.width = info.xres; + frame.height = 3 * info.yres / 4; + frame.buffer = mmap( + NULL, sizeof(*frame.buffer) * frame.width * frame.height, + PROT_READ | PROT_WRITE, MAP_SHARED, + fd, 0 + ); + if (frame.buffer == MAP_FAILED) err(EX_IOERR, "%s", dev); + close(fd); + + frame.background = frame.buffer[0]; + atexit(frameClear); +} + +static const uint32_t Magic = 0x864AB572; +static const uint32_t Version = 0; +static const uint32_t FlagUnicode = 1 << 0; +static uint32_t bytes(uint32_t bits) { + return (bits + 7) / 8; +} + +static char *path; +static struct { + uint32_t magic; + uint32_t version; + uint32_t size; + uint32_t flags; + struct { + uint32_t len; + uint32_t size; + uint32_t height; + uint32_t width; + } glyph; +} header; +static uint8_t *glyphs; + +static void fileRead(uint32_t newLen, uint32_t newWidth, uint32_t newHeight) { + FILE *file = fopen(path, "r"); + if (file) { + size_t len = fread(&header, sizeof(header), 1, file); + if (ferror(file)) err(EX_IOERR, "%s", path); + if (len < 1) errx(EX_DATAERR, "%s: truncated header", path); + + } else { + if (errno != ENOENT) err(EX_NOINPUT, "%s", path); + header.magic = Magic; + header.version = Version; + header.size = sizeof(header); + header.flags = 0; + header.glyph.len = newLen; + header.glyph.size = bytes(newWidth) * newHeight; + header.glyph.height = newHeight; + header.glyph.width = newWidth; + } + + if (header.magic != Magic) { + errx(EX_DATAERR, "%s: invalid magic %08X", path, header.magic); + } + if (header.version != Version) { + errx(EX_DATAERR, "%s: unsupported version %u", path, header.version); + } + if (header.flags & FlagUnicode) { + errx(EX_DATAERR, "%s: unsupported unicode table", path); + } + if (header.flags) { + errx(EX_DATAERR, "%s: unsupported flags %08X", path, header.flags); + } + + if (file && header.size > sizeof(header)) { + int error = fseek(file, header.size, SEEK_SET); + if (error) err(EX_IOERR, "%s", path); + + warnx("%s: truncating long header", path); + header.size = sizeof(header); + } + + glyphs = calloc(header.glyph.len, header.glyph.size); + if (!glyphs) err(EX_OSERR, "calloc"); + + if (file) { + size_t len = fread(glyphs, header.glyph.size, header.glyph.len, file); + if (ferror(file)) err(EX_IOERR, "%s", path); + if (len < header.glyph.len) { + errx(EX_DATAERR, "%s: truncated glyphs", path); + } + fclose(file); + } +} + +static void fileWrite(void) { + FILE *file = fopen(path, "w"); + if (!file) err(EX_CANTCREAT, "%s", path); + + fwrite(&header, sizeof(header), 1, file); + if (ferror(file)) err(EX_IOERR, "%s", path); + + fwrite(glyphs, header.glyph.size, header.glyph.len, file); + if (ferror(file)) err(EX_IOERR, "%s", path); + + int error = fclose(file); + if (error) err(EX_IOERR, "%s", path); +} + +static uint8_t *glyph(uint32_t index) { + return &glyphs[header.glyph.size * index]; +} +static uint8_t *bitByte(uint32_t index, uint32_t x, uint32_t y) { + return &glyph(index)[bytes(header.glyph.width) * y + x / 8]; +} +static uint8_t bitGet(uint32_t index, uint32_t x, uint32_t y) { + return *bitByte(index, x, y) >> (7 - x % 8) & 1; +} +static void bitFlip(uint32_t index, uint32_t x, uint32_t y) { + *bitByte(index, x, y) ^= 1 << (7 - x % 8); +} + +static void drawGlyph( + uint32_t destX, uint32_t destY, uint32_t scale, + uint32_t index, bool select, uint32_t selectX, uint32_t selectY +) { + destX <<= scale; + destY <<= scale; + + for (uint32_t y = 0; y < (header.glyph.height << scale); ++y) { + if (destY + y >= frame.height) break; + for (uint32_t x = 0; x < (header.glyph.width << scale); ++x) { + if (destX + x >= frame.width) break; + + uint32_t glyphX = x >> scale; + uint32_t glyphY = y >> scale; + uint32_t fill = -bitGet(index, glyphX, glyphY); + if (select) fill ^= 0x77; + if (selectX == glyphX && selectY == glyphY) fill ^= 0x77; + + frame.buffer[frame.width * (destY + y) + destX + x] = fill; + } + } +} + +static void drawBorder(uint32_t destX, uint32_t destY, uint32_t scale) { + destX <<= scale; + destY <<= scale; + + for (uint32_t y = 0; y < destY; ++y) { + if (y >= frame.height) break; + uint32_t fill = -(y >> scale & 1) ^ 0x555555; + for (uint32_t x = 0; x < (uint32_t)(1 << scale); ++x) { + if (destX + x >= frame.width) break; + frame.buffer[frame.width * y + destX + x] = fill; + } + } + + for (uint32_t x = 0; x < destX; ++x) { + if (x >= frame.width) break; + uint32_t fill = -(x >> scale & 1) ^ 0x555555; + for (uint32_t y = 0; y < (uint32_t)(1 << scale); ++y) { + if (destY + y >= frame.height) break; + frame.buffer[frame.width * (destY + y) + x] = fill; + } + } +} + +enum { LF = '\n', ESC = '\33', DEL = '\177' }; + +static enum { + Normal, + Edit, + Preview, + Discard, +} mode; + +static struct { + uint32_t scale; + uint32_t index; + bool modified; +} normal; + +static struct { + uint32_t scale; + uint32_t index; + uint32_t x; + uint32_t y; + uint8_t *undo; +} edit = { + .scale = 4, +}; + +static const uint32_t NormalCols = 32; +static void drawNormal(void) { + for (uint32_t i = 0; i < header.glyph.len; ++i) { + drawGlyph( + header.glyph.width * (i % NormalCols), + header.glyph.height * (i / NormalCols), + normal.scale, + i, (i == normal.index), -1, -1 + ); + } +} + +static void normalDec(uint32_t n) { + if (normal.index >= n) normal.index -= n; +} +static void normalInc(uint32_t n) { + if (normal.index + n < header.glyph.len) normal.index += n; +} +static void normalPrint(void) { + if (normal.index <= 256) { + printf("index: %02X '%lc'\n", normal.index, CP437[normal.index]); + } else { + printf("index: %02X\n", normal.index); + } +} + +static void inputNormal(char ch) { + switch (ch) { + break; case 'q': { + if (!normal.modified) exit(EX_OK); + mode = Discard; + } + break; case 'w': { + fileWrite(); + printf("write: %s\n", path); + normal.modified = false; + } + break; case '-': if (normal.scale) normal.scale--; frameClear(); + break; case '+': normal.scale++; + break; case 'h': normalDec(1); normalPrint(); + break; case 'l': normalInc(1); normalPrint(); + break; case 'k': normalDec(NormalCols); normalPrint(); + break; case 'j': normalInc(NormalCols); normalPrint(); + break; case 'e': { + normal.modified = true; + edit.index = normal.index; + if (!edit.undo) edit.undo = malloc(header.glyph.size); + if (!edit.undo) err(EX_OSERR, "malloc"); + memcpy(edit.undo, glyph(edit.index), header.glyph.size); + mode = Edit; + frameClear(); + } + break; case 'i': mode = Preview; frameClear(); + } +} + +static void drawEdit(void) { + drawGlyph(0, 0, edit.scale, edit.index, false, edit.x, edit.y); + drawBorder(header.glyph.width, header.glyph.height, edit.scale); + drawGlyph( + header.glyph.width << edit.scale, + header.glyph.height << edit.scale, + 0, + edit.index, false, -1, -1 + ); +} + +static void inputEdit(char ch) { + switch (ch) { + break; case ESC: mode = Normal; frameClear(); + break; case '-': if (edit.scale) edit.scale--; frameClear(); + break; case '+': edit.scale++; + break; case 'h': if (edit.x) edit.x--; + break; case 'l': if (edit.x + 1 < header.glyph.width) edit.x++; + break; case 'k': if (edit.y) edit.y--; + break; case 'j': if (edit.y + 1 < header.glyph.height) edit.y++; + break; case ' ': bitFlip(edit.index, edit.x, edit.y); + break; case 'u': { + memcpy(glyph(edit.index), edit.undo, header.glyph.size); + } + } +} + +enum { PreviewRows = 8, PreviewCols = 64 }; +static struct { + uint32_t glyphs[PreviewRows * PreviewCols]; + uint32_t index; +} preview; + +static void drawPreview(void) { + for (uint32_t i = 0; i < PreviewRows * PreviewCols; ++i) { + drawGlyph( + header.glyph.width * (i % PreviewCols), + header.glyph.height * (i / PreviewCols), + 0, + preview.glyphs[i], (i == preview.index), -1, -1 + ); + } +} + +static void inputPreview(char ch) { + switch (ch) { + break; case ESC: mode = Normal; frameClear(); + break; case DEL: { + if (preview.index) preview.index--; + preview.glyphs[preview.index] = 0; + } + break; case LF: { + uint32_t tail = PreviewCols - (preview.index % PreviewCols); + memset( + &preview.glyphs[preview.index], + 0, sizeof(preview.glyphs[0]) * tail + ); + preview.index += tail; + } + break; default: preview.glyphs[preview.index++] = ch; + } + preview.index %= PreviewRows * PreviewCols; +} + +static void drawDiscard(void) { + printf("discard modifications? "); + fflush(stdout); +} + +static void inputDiscard(char ch) { + printf("%c\n", ch); + if (ch == 'Y' || ch == 'y') exit(EX_OK); + mode = Normal; +} + +static void draw(void) { + switch (mode) { + break; case Normal: drawNormal(); + break; case Edit: drawEdit(); + break; case Preview: drawPreview(); + break; case Discard: drawDiscard(); + } +} + +static void input(char ch) { + switch (mode) { + break; case Normal: inputNormal(ch); + break; case Edit: inputEdit(ch); + break; case Preview: inputPreview(ch); + break; case Discard: inputDiscard(ch); + } +} + +static struct termios saveTerm; +static void restoreTerm(void) { + tcsetattr(STDIN_FILENO, TCSADRAIN, &saveTerm); +} + +int main(int argc, char *argv[]) { + setlocale(LC_CTYPE, ""); + + uint32_t newLen = 256; + uint32_t newWidth = 8; + uint32_t newHeight = 16; + + int opt; + while (0 < (opt = getopt(argc, argv, "g:h:w:"))) { + switch (opt) { + break; case 'g': newLen = strtoul(optarg, NULL, 0); + break; case 'h': newHeight = strtoul(optarg, NULL, 0); + break; case 'w': newWidth = strtoul(optarg, NULL, 0); + break; default: return EX_USAGE; + } + } + if (!newLen || !newWidth || !newHeight) return EX_USAGE; + if (optind == argc) return EX_USAGE; + + path = strdup(argv[optind]); + fileRead(newLen, newWidth, newHeight); + + frameOpen(); + frameClear(); + + int error = tcgetattr(STDIN_FILENO, &saveTerm); + if (error) err(EX_IOERR, "tcgetattr"); + atexit(restoreTerm); + + struct termios term = saveTerm; + term.c_lflag &= ~(ICANON | ECHO); + error = tcsetattr(STDIN_FILENO, TCSADRAIN, &term); + if (error) err(EX_IOERR, "tcsetattr"); + + for (;;) { + draw(); + char ch; + ssize_t size = read(STDIN_FILENO, &ch, 1); + if (size < 0) err(EX_IOERR, "read"); + if (!size) return EX_SOFTWARE; + input(ch); + } +} -- cgit 1.4.1