psfed.c in git
PSFED(1)                FreeBSD General Commands Manual               PSFED(1)

NAME
     psfed – PSF2 font editor

SYNOPSIS
     psfed [-H height] [-g glyphs] [-h height] [-w width] file

DESCRIPTION
     psfed is a PSF2 font editor for the Linux framebuffer.

     The arguments are as follows:

     -H height
             Modify the height of an existing font.  Only increasing the
             height is allowed.

     -g glyphs
             Set the number of glyphs in a new font.  The default number of
             glyphs is 256.

     -h height
             Set the height of a new font.  The default height is 16.

     -w width
             Set the width of a new font.  The default width is 8.

   Normal Mode
     In normal mode, each glyph is displayed in a grid.

     q       Quit.  psfed will ask for confirmation if the font has been
             modified since the last write.
     w       Write font to file.
     - +     Adjust display scale.
     h l     Select previous/next glyph.
     k j     Select glyph in previous/next row.
     f       Select glyph of next input character.
     '       Return to previously selected glyph.
     y       Copy selected glyph.
     e       Edit selected glyph in Edit Mode.
     i       Enter Preview Mode.

   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.

     ESC     Return to Normal Mode.
     - +     Adjust display scale.
     g G     Toggle guide on selected column/row.
     h l     Select previous/next bit in row.
     k j     Select previous/next bit in column.
     SPACE   Flip selected bit.
     r       Invert glyph.
     H L     Move glyph left/right.
     K J     Move glyph up/down.
     p       Paste the copied glyph.
     u       Revert glyph to initial state.

   Preview Mode
     In preview mode, arbitrary text may be entered for preview.  Press ESC to
     return to Normal Mode.

ENVIRONMENT
     FRAMEBUFFER  The framebuffer device path.  The default path is /dev/fb0.

SEE ALSO
     psfaddtable(1), psfgettable(1), psfstriptable(1), setfont(8)

CAVEATS
     psfed does not support Unicode tables.  Use psfaddtable(1) to add Unicode
     tables to fonts created by psfed.

BUGS
     psfed makes no attempt to convert header fields to and from little-endian
     format.

FreeBSD 12.0-RELEASE-p3        January 14, 2019        FreeBSD 12.0-RELEASE-p3
/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <locale.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sysexits.h>
#include <termios.h>
#include <unistd.h>
#include <wchar.h>

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 bitSet(uint32_t index, uint32_t x, uint32_t y, uint8_t bit) {
	*bitByte(index, x, y) &= ~(1 << (7 - x % 8));
	*bitByte(index, x, y) |= bit << (7 - x % 8);
}

static void drawGlyph(
	uint32_t destX, uint32_t destY, uint32_t scale, uint32_t index,
	uint32_t selectX, uint32_t selectY, uint32_t guideX, uint32_t guideY
) {
	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 (selectX & 1 << glyphX && selectY & 1 << glyphY) fill ^= 0x77;
			if (guideX & 1 << glyphX || guideY & 1 << glyphY) fill ^= 0x3300;

			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;
	bool to;
	uint32_t from;
} normal;

static struct {
	uint32_t scale;
	uint32_t index;
	uint32_t x;
	uint32_t y;
	uint32_t guideX;
	uint32_t guideY;
	uint8_t *undo;
	uint8_t *copy;
} 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), -(i == normal.index), 0, 0
		);
	}
}

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(const char *prefix) {
	if (normal.index <= 256) {
		printf("%s: %02X '%lc'\n", prefix, normal.index, CP437[normal.index]);
	} else {
		printf("%s: %02X\n", prefix, normal.index);
	}
}

static void inputNormal(char ch) {
	if (normal.to) {
		if (ch < header.glyph.len) normal.index = ch;
		normalPrint("index");
		normal.to = false;
		return;
	}

	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("index");
		break; case 'l': normalInc(1); normalPrint("index");
		break; case 'k': normalDec(NormalCols); normalPrint("index");
		break; case 'j': normalInc(NormalCols); normalPrint("index");
		break; case 'f': normal.from = normal.index; normal.to = true;
		break; case 047: normal.index = normal.from; normalPrint("index");
		break; case 'y': {
			if (!edit.copy) edit.copy = malloc(header.glyph.size);
			if (!edit.copy) err(EX_OSERR, "malloc");
			memcpy(edit.copy, glyph(normal.index), header.glyph.size);
			normalPrint("copy");
		}
		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,
		1 << edit.x, 1 << edit.y, edit.guideX, edit.guideY
	);
	drawBorder(header.glyph.width, header.glyph.height, edit.scale);
	drawGlyph(
		header.glyph.width << edit.scale,
		header.glyph.height << edit.scale,
		0, edit.index,
		0, 0, 0, 0
	);
}

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 'g': edit.guideY ^= 1 << edit.y;
		break; case 'G': edit.guideX ^= 1 << edit.x;

		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 'r': {
			for (uint32_t y = 0; y < header.glyph.height; ++y) {
				for (uint32_t x = 0; x < header.glyph.width; ++x) {
					bitFlip(edit.index, x, y);
				}
			}
		}

		break; case 'H': {
			for (uint32_t x = 0; x < header.glyph.width; ++x) {
				for (uint32_t y = 0; y < header.glyph.height; ++y) {
					if (x + 1 < header.glyph.width) {
						bitSet(edit.index, x, y, bitGet(edit.index, x + 1, y));
					} else {
						bitSet(edit.index, x, y, 0);
					}
				}
			}
		}

		break; case 'L': {
			uint32_t width = header.glyph.width;
			for (uint32_t x = width - 1; x < width; --x) {
				for (uint32_t y = 0; y < header.glyph.height; ++y) {
					if (x - 1 < width) {
						bitSet(edit.index, x, y, bitGet(edit.index, x - 1, y));
					} else {
						bitSet(edit.index, x, y, 0);
					}
				}
			}
		}

		break; case 'K': {
			for (uint32_t y = 0; y < header.glyph.height; ++y) {
				for (uint32_t x = 0; x < header.glyph.width; ++x) {
					if (y + 1 < header.glyph.height) {
						bitSet(edit.index, x, y, bitGet(edit.index, x, y + 1));
					} else {
						bitSet(edit.index, x, y, 0);
					}
				}
			}
		}

		break; case 'J': {
			uint32_t height = header.glyph.height;
			for (uint32_t y = height - 1; y < height; --y) {
				for (uint32_t x = 0; x < header.glyph.width; ++x) {
					if (y - 1 < height) {
						bitSet(edit.index, x, y, bitGet(edit.index, x, y - 1));
					} else {
						bitSet(edit.index, x, y, 0);
					}
				}
			}
		}

		break; case 'p': {
			if (!edit.copy) break;
			memcpy(glyph(edit.index), edit.copy, header.glyph.size);
		}
		break; case 'u': {
			if (!edit.undo) break;
			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), -(i == preview.index), 0, 0
		);
	}
}

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;
	uint32_t setHeight = 0;

	int opt;
	while (0 < (opt = getopt(argc, argv, "H:g:h:w:"))) {
		switch (opt) {
			break; case 'H': setHeight = strtoul(optarg, NULL, 0);
			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);

	if (setHeight) {
		if (setHeight < header.glyph.height) {
			errx(EX_CONFIG, "cannot decrease height");
		}

		uint32_t setSize = bytes(header.glyph.width) * setHeight;
		uint8_t *setGlyphs = calloc(header.glyph.len, setSize);
		for (uint32_t i = 0; i < header.glyph.len; ++i) {
			memcpy(&setGlyphs[setSize * i], glyph(i), header.glyph.size);
		}
		free(glyphs);
		glyphs = setGlyphs;

		header.glyph.height = setHeight;
		header.glyph.size = setSize;
		normal.modified = true;
	}

	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);
	}
}