Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/cli/THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Third-Party Licenses

## Cozette

The Sixel dashboard includes a generated subset of Cozette bitmap glyph data,
version 1.30.0. Source: <https://github.com/the-moonwitch/Cozette>.

MIT License

Copyright (c) 2020, Ines <ines@moonwit.ch>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dist/node-sqlite3-wasm.wasm",
"dist/vendor/symbolic_bg.wasm",
"LICENSE.md",
"THIRD_PARTY_LICENSES.md",
"README.md"
],
"license": "FSL-1.1-Apache-2.0",
Expand Down
83 changes: 83 additions & 0 deletions packages/cli/src/lib/formatters/cozette-font.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*!
* Cozette bitmap font subset, version 1.30.0.
* Copyright (c) 2020, Ines <ines@moonwit.ch>
* https://github.com/the-moonwitch/Cozette
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import { Buffer } from "node:buffer";

/** Cozette's native monospaced cell width. */
export const COZETTE_CELL_WIDTH = 6;

/** Cozette's native cell height, including descenders. */
export const COZETTE_CELL_HEIGHT = 13;

const FIRST_ASCII_CODE_POINT = 32;
const LAST_ASCII_CODE_POINT = 126;
const FULL_BLOCK_CODE_POINT = 0x25_88;
const BLACK_SQUARE_CODE_POINT = 0x25_a0;
const FULL_BLOCK_INDEX = LAST_ASCII_CODE_POINT - FIRST_ASCII_CODE_POINT + 1;
const BLACK_SQUARE_INDEX = FULL_BLOCK_INDEX + 1;

/** Visible fallback for a Unicode glyph outside the embedded Cozette subset. */
const UNKNOWN_GLYPH = new Uint8Array([
63, 33, 45, 45, 45, 45, 45, 45, 45, 45, 33, 63, 0,
]);

/**
* Printable ASCII, full block, and black square raster rows generated from
* Cozette's v1.30.0 BDF release. Each glyph has 13 rows packed into the low
* six bits of a byte, with bit 5 as the leftmost pixel. Source SHA-256:
* 8d740166af3a14053773ac7a8846bf288c8818a44f73ab28d4725d91b03a5639.
*/
const GLYPH_ROWS = Uint8Array.from(
Buffer.from(
"AAAAAAAAAAAAAAAAAAAABAQEBAQEAAQAAAAACgoKAAAAAAAAAAAAAAAKCh8KCh8KCgAAAAAEDhUUDgUFFQ4EAAAACBQJAgQIEgUCAAAAAAQKCgQNEhISDQAAAAAEBAQAAAAAAAAAAAAAAgQECAgICAgEBAIAAAgEBAICAgICBAQIAAAAAAAKBB8ECgAAAAAAAAAABAQfBAQAAAAAAAAAAAAAAAAMDAQIAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAwMAAAAAAEBAgIEBAgIEBAAAAAADhERFRUREQ4AAAAAAAQMFAQEBAQfAAAAAAAOEQECBAgQHwAAAAAADhEBBgEBEQ4AAAAAAAEDBQkRHwEBAAAAAAAfEBAeAQERDgAAAAAABggQHhEREQ4AAAAAAB8BAgIEBAgIAAAAAAAOEREOERERDgAAAAAADhEREQ8BAgwAAAAAAAAADAwAAAwMAAAAAAAAAAwMAAAMDAQIAAAAAAECBAgEAgEAAAAAAAAAAB8AHwAAAAAAAAAAEAgEAgQIEAAAAAAADhEBAgQEAAQAAAAAAA4RERcVFxAPAAAAAAAOERERHxEREQAAAAAAHhERHhERER4AAAAAAA4REBAQEBEOAAAAAAAcEhERERESHAAAAAAAHxAQHhAQEB8AAAAAAB8QEB4QEBAQAAAAAAAOERAQExERDgAAAAAAERERHxEREREAAAAAAA4EBAQEBAQOAAAAAAAHAQEBARERDgAAAAAAERIUHBISEREAAAAAABAQEBAQEBAfAAAAAAARGxUVEREREQAAAAAAERkZFRUTExEAAAAAAA4REREREREOAAAAAAAeERERHhAQEAAAAAAADhEREREREg0BAAAAAB4RER4SERERAAAAAAAOERAOAQERDgAAAAAAHwQEBAQEBAQAAAAAABEREREREREOAAAAAAAREREKCgoEBAAAAAAAERERFRUOCgoAAAAAABERCgQEChERAAAAAAAREREKBAQEBAAAAAAAHwIEBAgIEB8AAAAADggICAgICAgICA4AABAQCAgEBAICAQEAAAAOAgICAgICAgICDgAEChEAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAIBAAAAAAAAAAAAAAAAAAADxERERMNAAAAABAQEB4RERERHgAAAAAAAAAOERAQEQ4AAAAAAQEBDxEREREPAAAAAAAAAA4RHxARDgAAAAAHCAgeCAgICAgAAAAAAAAADxEREREPAQEOABAQEB4REREREQAAAAAABAAMBAQEBAMAAAAAAAIABgICAgICCgQAABAQEBESFBwSEQAAAAAMBAQEBAQEBAYAAAAAAAAAGhUVFRUVAAAAAAAAAB4REREREQAAAAAAAAAOEREREQ4AAAAAAAAAHhEREREeEBAQAAAAAA8RERERDwEBAQAAAAAeERAQEBAAAAAAAAAADxAOAQEeAAAAAAAICB4ICAgIBwAAAAAAAAAREREREQ8AAAAAAAAAEREKCgQEAAAAAAAAABERFRUKCgAAAAAAAAARCgQEChEAAAAAAAAAEREREREPAQEOAAAAAB8CBAgQHwAAAAADBAQEBBgEBAQEAwAABAQEBAQEBAQEBAQAABgEBAQEAwQEBAQYAAAAAAAACRUSAAAAAAA/Pz8/Pz8/Pz8/Pz8/AAAAPz8/Pz8/PwAAAA==",
"base64"
)
);

/** Look up a Cozette glyph by character without parsing or rasterizing a font. */
export function getCozetteGlyph(character: string): Uint8Array {
const codePoint = character.codePointAt(0);
const index = getGlyphIndex(codePoint);
if (index === undefined) {
return UNKNOWN_GLYPH;
}
const start = index * COZETTE_CELL_HEIGHT;
return GLYPH_ROWS.subarray(start, start + COZETTE_CELL_HEIGHT);
}

function getGlyphIndex(codePoint: number | undefined): number | undefined {
if (
codePoint !== undefined &&
codePoint >= FIRST_ASCII_CODE_POINT &&
codePoint <= LAST_ASCII_CODE_POINT
) {
return codePoint - FIRST_ASCII_CODE_POINT;
}
if (codePoint === FULL_BLOCK_CODE_POINT) {
return FULL_BLOCK_INDEX;
}
if (codePoint === BLACK_SQUARE_CODE_POINT) {
return BLACK_SQUARE_INDEX;
}
}
192 changes: 81 additions & 111 deletions packages/cli/src/lib/formatters/pixel-canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*/

import type { DecodedImage } from "../sixel-image.js";
import {
COZETTE_CELL_HEIGHT,
COZETTE_CELL_WIDTH,
getCozetteGlyph,
} from "./cozette-font.js";

/** An RGB color tuple with one 0-255 value per channel. */
export type Rgb = [number, number, number];
Expand Down Expand Up @@ -147,130 +152,95 @@ function copyOpaquePixel(
destination.data[targetOffset + 3] = source.data[sourceOffset + 3] ?? 255;
}

/** 5x7 glyphs for dashboard labels and table content. */
const FONT: Record<string, string[]> = {
A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
B: ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
C: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"],
D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
G: ["01111", "10000", "10000", "10111", "10001", "10001", "01111"],
H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
I: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"],
J: ["00111", "00010", "00010", "00010", "10010", "10010", "01100"],
K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
Q: ["01110", "10001", "10001", "10001", "10101", "10010", "01101"],
R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
V: ["10001", "10001", "10001", "10001", "10001", "01010", "00100"],
W: ["10001", "10001", "10001", "10101", "10101", "10101", "01010"],
X: ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
"0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
"1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
"2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
"3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
"4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
"5": ["11111", "10000", "10000", "11110", "00001", "00001", "11110"],
"6": ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
"7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
"8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
"9": ["01110", "10001", "10001", "01111", "00001", "00001", "01110"],
" ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"],
".": ["00000", "00000", "00000", "00000", "00000", "00110", "00110"],
",": ["00000", "00000", "00000", "00000", "00110", "00110", "00100"],
":": ["00000", "00110", "00110", "00000", "00110", "00110", "00000"],
";": ["00000", "00110", "00110", "00000", "00110", "00110", "00100"],
"-": ["00000", "00000", "00000", "01110", "00000", "00000", "00000"],
_: ["00000", "00000", "00000", "00000", "00000", "00000", "11111"],
"/": ["00001", "00010", "00100", "01000", "10000", "00000", "00000"],
"\\": ["10000", "01000", "00100", "00010", "00001", "00000", "00000"],
"(": ["00010", "00100", "01000", "01000", "01000", "00100", "00010"],
")": ["01000", "00100", "00010", "00010", "00010", "00100", "01000"],
"[": ["01110", "01000", "01000", "01000", "01000", "01000", "01110"],
"]": ["01110", "00010", "00010", "00010", "00010", "00010", "01110"],
"=": ["00000", "11111", "00000", "11111", "00000", "00000", "00000"],
"+": ["00000", "00100", "00100", "11111", "00100", "00100", "00000"],
"?": ["01110", "10001", "00001", "00010", "00100", "00000", "00100"],
"!": ["00100", "00100", "00100", "00100", "00100", "00000", "00100"],
"#": ["01010", "11111", "01010", "01010", "11111", "01010", "00000"],
"%": ["11001", "11010", "00100", "01000", "10110", "00110", "00000"],
"@": ["01110", "10001", "10111", "10101", "10111", "10000", "01111"],
"*": ["00000", "10101", "01110", "11111", "01110", "10101", "00000"],
"|": ["00100", "00100", "00100", "00100", "00100", "00100", "00100"],
"<": ["00010", "00100", "01000", "10000", "01000", "00100", "00010"],
">": ["01000", "00100", "00010", "00001", "00010", "00100", "01000"],
"&": ["01100", "10010", "10100", "01000", "10101", "10010", "01101"],
"'": ["00100", "00100", "00010", "00000", "00000", "00000", "00000"],
'"': ["01010", "01010", "00100", "00000", "00000", "00000", "00000"],
"█": ["11111", "11111", "11111", "11111", "11111", "11111", "11111"],
"■": ["01110", "11111", "11111", "11111", "11111", "11111", "01110"],
};

/** Visible fallback for a Unicode glyph outside the compact bitmap font. */
const UNKNOWN_GLYPH = [
"11111",
"10001",
"10101",
"10101",
"10101",
"10001",
"11111",
];

/** Draw a single line of compact bitmap text, clipped to its terminal cells. */
/** Draw a Cozette text line, scaled and clipped to its terminal cells. */
export function drawPixelText(
image: DecodedImage,
text: string,
options: PixelTextOptions
): void {
const scale = Math.max(
1,
Math.min(
Math.floor((options.cellWidth - 2) / 5),
Math.floor((options.cellHeight - 2) / 7)
)
);
const cellWidth = Math.max(1, Math.floor(options.cellWidth));
const cellHeight = Math.max(1, Math.floor(options.cellHeight));

let column = 0;
for (const rawChar of text) {
if (column >= options.maxColumns) {
break;
}
const glyph = FONT[rawChar.toUpperCase()] ?? UNKNOWN_GLYPH;
const glyphWidth = 5 * scale;
const glyphHeight = 7 * scale;
const glyphX =
options.x +
column * options.cellWidth +
Math.max(0, Math.floor((options.cellWidth - glyphWidth) / 2));
const glyphY =
options.y +
Math.max(0, Math.floor((options.cellHeight - glyphHeight) / 2));
drawCozetteGlyph(image, getCozetteGlyph(rawChar), {
x: options.x + column * cellWidth,
y: options.y,
cellWidth,
cellHeight,
color: options.color,
});
column += 1;
}
}

for (let row = 0; row < glyph.length; row += 1) {
const pattern = glyph[row] ?? "00000";
for (let pixel = 0; pixel < pattern.length; pixel += 1) {
if (pattern[pixel] === "1") {
drawPixelRect(image, {
x: glyphX + pixel * scale,
y: glyphY + row * scale,
width: scale,
height: scale,
color: options.color,
});
}
type CozetteGlyphOptions = {
x: number;
y: number;
cellWidth: number;
cellHeight: number;
color: Rgb;
};

function drawCozetteGlyph(
image: DecodedImage,
glyph: Uint8Array,
options: CozetteGlyphOptions
): void {
for (let row = 0; row < COZETTE_CELL_HEIGHT; row += 1) {
const pattern = glyph[row] ?? 0;
for (let column = 0; column < COZETTE_CELL_WIDTH; column += 1) {
if (!isCozettePixelSet(pattern, column)) {
continue;
}
drawCozettePixel(image, { ...options, row, column });
}
column += 1;
}
}

function isCozettePixelSet(pattern: number, column: number): boolean {
const divisor = 2 ** (COZETTE_CELL_WIDTH - 1 - column);
return Math.floor(pattern / divisor) % 2 === 1;
Comment thread
sentry[bot] marked this conversation as resolved.
}

type CozettePixelOptions = CozetteGlyphOptions & {
row: number;
column: number;
};

function drawCozettePixel(
image: DecodedImage,
options: CozettePixelOptions
): void {
const x =
options.x +
scaleCoordinate(options.column, options.cellWidth, COZETTE_CELL_WIDTH);
const y =
options.y +
scaleCoordinate(options.row, options.cellHeight, COZETTE_CELL_HEIGHT);
const right =
options.x +
scaleCoordinate(options.column + 1, options.cellWidth, COZETTE_CELL_WIDTH);
const bottom =
options.y +
scaleCoordinate(options.row + 1, options.cellHeight, COZETTE_CELL_HEIGHT);
drawPixelRect(image, {
x,
y,
width: right - x,
height: bottom - y,
color: options.color,
});
}

/** Scale a Cozette bitmap coordinate to its terminal-cell boundary. */
function scaleCoordinate(
coordinate: number,
cellSize: number,
glyphSize: number
): number {
return Math.floor((coordinate * cellSize) / glyphSize);
}
Loading
Loading