start work on porting box drawing to C

This commit is contained in:
Kovid Goyal 2024-12-20 08:09:06 +05:30
parent ec3a6cc26e
commit fdb9b17943
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 614 additions and 61 deletions

View file

@ -8,6 +8,11 @@
#include "decorations.h"
#include "state.h"
typedef uint32_t uint;
static uint max(uint a, uint b) { return a > b ? a : b; }
static uint min(uint a, uint b) { return a < b ? a : b; }
#define STRAIGHT_UNDERLINE_LOOP \
unsigned half = fcm.underline_thickness / 2; \
DecorationGeometry ans = {.top = half > fcm.underline_position ? 0 : fcm.underline_position - half}; \
@ -35,8 +40,8 @@ add_strikethrough(uint8_t *buf, FontCellMetrics fcm) {
DecorationGeometry
add_missing_glyph(uint8_t *buf, FontCellMetrics fcm) {
DecorationGeometry ans = {.height=fcm.cell_height};
unsigned thickness = MIN(fcm.underline_thickness, fcm.strikethrough_thickness);
thickness = MIN(thickness, fcm.cell_width);
unsigned thickness = min(fcm.underline_thickness, fcm.strikethrough_thickness);
thickness = min(thickness, fcm.cell_width);
for (unsigned y = 0; y < ans.height; y++) {
uint8_t *line = buf + fcm.cell_width * y;
if (y < thickness || y >= ans.height - thickness) memset(line, 0xff, fcm.cell_width);
@ -51,9 +56,9 @@ add_missing_glyph(uint8_t *buf, FontCellMetrics fcm) {
DecorationGeometry
add_double_underline(uint8_t *buf, FontCellMetrics fcm) {
unsigned a = fcm.underline_position > fcm.underline_thickness ? fcm.underline_position - fcm.underline_thickness : 0;
a = MIN(a, fcm.cell_height - 1);
unsigned b = MIN(fcm.underline_position, fcm.cell_height - 1);
unsigned top = MIN(a, b), bottom = MAX(a, b);
a = min(a, fcm.cell_height - 1);
unsigned b = min(fcm.underline_position, fcm.cell_height - 1);
unsigned top = min(a, b), bottom = max(a, b);
int deficit = 2 - (bottom - top);
if (deficit > 0) {
if (bottom + deficit < fcm.cell_height) bottom += deficit;
@ -62,8 +67,8 @@ add_double_underline(uint8_t *buf, FontCellMetrics fcm) {
if (deficit > 1) top -= deficit - 1;
} else top -= deficit;
}
top = MAX(0u, MIN(top, fcm.cell_height - 1u));
bottom = MAX(0u, MIN(bottom, fcm.cell_height - 1u));
top = max(0u, min(top, fcm.cell_height - 1u));
bottom = max(0u, min(bottom, fcm.cell_height - 1u));
memset(buf + fcm.cell_width * top, 0xff, fcm.cell_width);
memset(buf + fcm.cell_width * bottom, 0xff, fcm.cell_width);
DecorationGeometry ans = {.top=top, .height = bottom + 1 - top};
@ -72,7 +77,7 @@ add_double_underline(uint8_t *buf, FontCellMetrics fcm) {
static unsigned
distribute_dots(unsigned available_space, unsigned num_of_dots, unsigned *summed_gaps, unsigned *gaps) {
unsigned dot_size = MAX(1u, available_space / (2u * num_of_dots));
unsigned dot_size = max(1u, available_space / (2u * num_of_dots));
unsigned extra = 2 * num_of_dots * dot_size;
extra = available_space > extra ? available_space - extra : 0;
for (unsigned i = 0; i < num_of_dots; i++) gaps[i] = dot_size;
@ -124,9 +129,9 @@ add_dashed_underline(uint8_t *buf, FontCellMetrics fcm) {
static unsigned
add_intensity(uint8_t *buf, unsigned x, unsigned y, uint8_t val, unsigned max_y, unsigned position, unsigned cell_width) {
y += position;
y = MIN(y, max_y);
y = min(y, max_y);
unsigned idx = cell_width * y + x;
buf[idx] = MIN(255, buf[idx] + val);
buf[idx] = min(255, buf[idx] + val);
return y;
}
@ -137,10 +142,10 @@ add_curl_underline(uint8_t *buf, FontCellMetrics fcm) {
unsigned half_thickness = fcm.underline_thickness / 2;
unsigned top = fcm.underline_position > half_thickness ? fcm.underline_position - half_thickness : 0;
unsigned max_height = fcm.cell_height - top; // descender from the font
unsigned half_height = MAX(1u, max_height / 4u);
unsigned half_height = max(1u, max_height / 4u);
unsigned thickness;
if (OPT(undercurl_style) & 2) thickness = MAX(half_height, fcm.underline_thickness);
else thickness = MAX(1u, fcm.underline_thickness) - (fcm.underline_thickness < 3u ? 1u : 2u);
if (OPT(undercurl_style) & 2) thickness = max(half_height, fcm.underline_thickness);
else thickness = max(1u, fcm.underline_thickness) - (fcm.underline_thickness < 3u ? 1u : 2u);
unsigned position = fcm.underline_position;
// Ensure curve doesn't exceed cell boundary at the bottom
@ -168,7 +173,7 @@ add_curl_underline(uint8_t *buf, FontCellMetrics fcm) {
static void
vert(uint8_t *ans, bool is_left_edge, double width_pt, double dpi_x, FontCellMetrics fcm) {
unsigned width = MAX(1u, MIN((unsigned)(round(width_pt * dpi_x / 72.0)), fcm.cell_width));
unsigned width = max(1u, min((unsigned)(round(width_pt * dpi_x / 72.0)), fcm.cell_width));
const unsigned left = is_left_edge ? 0 : (fcm.cell_width > width ? fcm.cell_width - width : 0);
for (unsigned y = 0; y < fcm.cell_height; y++) {
const unsigned offset = y * fcm.cell_width + left;
@ -178,7 +183,7 @@ vert(uint8_t *ans, bool is_left_edge, double width_pt, double dpi_x, FontCellMet
static unsigned
horz(uint8_t *ans, bool is_top_edge, double height_pt, double dpi_y, FontCellMetrics fcm) {
unsigned height = MAX(1u, MIN((unsigned)(round(height_pt * dpi_y / 72.0)), fcm.cell_height));
unsigned height = max(1u, min((unsigned)(round(height_pt * dpi_y / 72.0)), fcm.cell_height));
const unsigned top = is_top_edge ? 0 : (fcm.cell_height > height ? fcm.cell_height - height : 0);
for (unsigned y = top; y < top + height; y++) {
const unsigned offset = y * fcm.cell_width;
@ -210,3 +215,437 @@ add_hollow_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x, double dpi_y)
DecorationGeometry ans = {.height=fcm.cell_height};
return ans;
}
typedef struct Range {
uint start, end;
} Range;
typedef struct Canvas {
uint8_t *mask;
uint width, height, supersample_factor;
struct { double x, y; } dpi;
Range *holes; uint holes_count, holes_capacity;
struct { double upper, lower; } *y_limits; uint y_limits_count, y_limits_capacity;
} Canvas;
static void
append_hole(Canvas *self, Range hole) {
ensure_space_for(self, holes, self->holes[0], self->holes_count + 1, holes_capacity, self->width, false);
self->holes[self->holes_count++] = hole;
}
static void
append_limit(Canvas *self, double upper, double lower) {
ensure_space_for(self, y_limits, self->y_limits[0], self->y_limits_count + 1, y_limits_capacity, self->width, false);
self->y_limits[self->y_limits_count].upper = upper;
self->y_limits[self->y_limits_count++].lower = lower;
}
static uint
thickness(Canvas *self, uint level, bool horizontal) {
level = min(level, arraysz(OPT(box_drawing_scale)));
double pts = OPT(box_drawing_scale)[level];
double dpi = horizontal ? self->dpi.x : self->dpi.y;
return self->supersample_factor * (uint)ceil(pts * dpi / 72.0);
}
static uint
minus(uint a, uint b) { // saturating subtraction (a > b ? a - b : 0)
uint res = a - b;
res &= -(res <= a);
return res;
}
static const uint hole_factor = 8;
static void
get_holes(Canvas *self, uint sz, uint hole_sz, uint num) {
uint all_holes_use = (num + 1) * hole_sz;
uint individual_block_size = max(1u, minus(sz, all_holes_use) / (num + 1));
uint half_hole_sz = hole_sz / 2;
int pos = - half_hole_sz;
while (pos < (int)sz) {
uint left = pos > 0 ? pos : 0;
uint right = min(sz, pos + hole_sz);
if (right > left) append_hole(self, (Range){left, right});
pos = right + individual_block_size;
}
}
static void
add_hholes(Canvas *self, uint level, uint num) {
uint line_sz = thickness(self, level, true);
uint hole_sz = self->width / hole_factor;
uint start = minus(self->height / 2, line_sz / 2);
get_holes(self, self->width, hole_sz, num);
for (uint y = 0; y < start + line_sz; y++) {
uint offset = y * self->width;
for (uint i = 0; i < self->holes_count; i++) memset(self->mask + offset + self->holes[i].start, 0, self->holes[i].end - self->holes[i].start);
}
}
static void
add_vholes(Canvas *self, uint level, uint num) {
uint line_sz = thickness(self, level, false);
uint hole_sz = self->height / hole_factor;
uint start = minus(self->width / 2, line_sz / 2);
get_holes(self, self->height, hole_sz, num);
for (uint i = 0; i < self->holes_count; i++) {
for (uint y = self->holes[i].start; y < self->holes[i].end; y++) {
uint offset = y * self->width;
memset(self->mask + offset + start, 0, line_sz);
}
}
}
static void
draw_hline(Canvas *self, uint x1, uint x2, uint y, uint level) {
// Draw a horizontal line between [x1, x2) centered at y with the thickness given by level and self->supersample_factor
uint sz = thickness(self, level, false);
uint start = minus(y, sz / 2);
for (uint y = start; y < min(start + sz, self->height); y++) {
uint8_t *py = self->mask + y * self->width;
memset(py + x1, 255, minus(x2, x1));
}
}
static void
draw_vline(Canvas *self, uint y1, uint y2, uint x, uint level) {
// Draw a vertical line between [y1, y2) centered at x with the thickness given by level and self->supersample_factor
uint sz = thickness(self, level, true);
uint start = minus(x, sz / 2), end = min(start + sz, self->width), xsz = minus(end, start);
for (uint y = y1; y < y2; y++) {
uint8_t *py = self->mask + y * self->width;
memset(py + start, 255, xsz);
}
}
static void
half_hline(Canvas *self, uint level, bool right_half, uint extend_by) {
uint x1, x2;
if (right_half) {
x1 = minus(self->width / 2u, extend_by); x2 = self->width;
} else {
x1 = 0; x2 = self->width / 2 + extend_by;
}
draw_hline(self, x1, x2, self->height / 2, level);
}
static void
half_vline(Canvas *self, uint level, bool bottom_half, uint extend_by) {
uint y1, y2;
if (bottom_half) {
y1 = minus(self->height / 2u, extend_by); y2 = self->height;
} else {
y1 = 0; y2 = self->height / 2 + extend_by;
}
draw_vline(self, y1, y2, self->width / 2, level);
}
static void
hline(Canvas *self, uint level) {
half_hline(self, level, false, 0);
half_hline(self, level, true, 0);
}
static void
vline(Canvas *self, uint level) {
half_vline(self, level, false, 0);
half_vline(self, level, true, 0);
}
static void
hholes(Canvas *self, uint level, uint num) {
hline(self, level);
add_hholes(self, level, num);
}
static void
vholes(Canvas *self, uint level, uint num) {
vline(self, level);
add_vholes(self, level, num);
}
static uint8_t
plus(uint8_t a, uint8_t b) {
uint8_t res = a + b;
res |= -(res < a);
return res;
}
static uint8_t
average_intensity(const Canvas *src, uint dest_x, uint dest_y) {
uint src_x = dest_x * src->supersample_factor, src_y = dest_y * src->supersample_factor;
uint total = 0;
for (uint y = src_y; y < src_y + src->supersample_factor; y++) {
uint offset = src->width * y;
for (uint x = src_x; x < src_x + src->supersample_factor; x++) total += src->mask[offset + x];
}
return (total / (src->supersample_factor * src->supersample_factor)) & 0xff;
}
static void
downsample(const Canvas *src, Canvas *dest) {
for (uint y = 0; y < dest->height; y++) {
uint offset = dest->width * y;
for (uint x = 0; x < dest->width; x++) {
dest->mask[offset + x] = plus(dest->mask[offset + x], average_intensity(src, x, y));
}
}
}
typedef struct StraightLine {
double m, c;
} StraightLine;
static StraightLine
line_from_points(int x1, int y1, int x2, int y2) {
StraightLine ans = {.m = (y2 - y1) / ((double)(x2 - x1))};
ans.c = y1 - ans.m * x1;
return ans;
}
static double
line_y(StraightLine l, int x) {
return l.m * x + l.c;
}
#define calc_limits(self, lower_y, upper_y) { \
if (!self->y_limits) { \
self->y_limits_count = self->width; self->y_limits = malloc(sizeof(self->y_limits[0]) * self->y_limits_count); \
if (!self->y_limits) fatal("Out of memory"); \
} \
for (uint x = 0; x < self->width; x++) { self->y_limits[x].lower = lower_y; self->y_limits[x].upper = upper_y; } \
}
static void
fill_region(Canvas *self, bool inverted) {
uint8_t full = 0, empty = 0; if (inverted) empty = 255; else full = 255;
for (uint y = 0; y < self->height; y++) {
uint offset = y * self->width;
for (uint x = 0; x < self->width && x < self->y_limits_count; x++) {
self->mask[offset + x] = self->y_limits[x].lower <= y && y <= self->y_limits[x].upper ? full : empty;
}
}
}
static void
triangle(Canvas *self, bool left, bool inverted) {
int ay1 = 0, by1 = self->height - 1, y2 = self->height / 2, x1 = 0, x2 = 0;
if (left) x2 = self->width - 1; else x1 = self->width - 1;
StraightLine uppery = line_from_points(x1, ay1, x2, y2);
StraightLine lowery = line_from_points(x1, by1, x2, y2);
calc_limits(self, line_y(uppery, x), line_y(lowery, x));
fill_region(self, inverted);
}
typedef enum Corner {
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
} Corner;
typedef struct Point { int x, y; } Point;
static void
thick_line(Canvas *self, uint thickness_in_pixels, Point p1, Point p2) {
if (p1.x > p2.x) SWAP(p1, p2);
StraightLine l = line_from_points(p1.x, p1.y, p2.x, p2.y);
div_t d = div(thickness_in_pixels, 2);
int delta = d.quot, extra = d.rem;
for (int x = p1.x > 0 ? p1.x : 0; x < (int)self->width && x < p2.x + 1; x++) {
int y_p = (int)line_y(l, x);
for (int y = MAX(0, y_p - delta); y < MIN(y_p + delta + extra, (int)self->height); y++) {
self->mask[x + y * self->width] = 255;
}
}
}
static void
half_cross_line(Canvas *self, uint level, Corner corner) {
uint my = minus(self->height, 1) / 2; Point p1 = {0}, p2 = {0};
switch (corner) {
case TOP_LEFT: p2.x = minus(self->width, 1); p2.y = my; break;
case BOTTOM_LEFT: p1.x = minus(self->width, 1); p1.y = my; p2.y = self->height -1; break;
case TOP_RIGHT: p1.x = minus(self->width, 1); p2.y = my; break;
case BOTTOM_RIGHT: p2.x = minus(self->width, 1), p2.y = minus(self->height, 1); p1.y = my; break;
}
thick_line(self, thickness(self, level, true), p1, p2);
}
typedef struct CubicBezier {
Point start, c1, c2, end;
} CubicBezier;
#define bezier_eq(which) { \
double tm1 = 1 - t; \
double tm1_3 = tm1 * tm1 * tm1; \
double t_3 = t * t * t; \
return tm1_3 * cb.start.which + 3 * t * tm1 * (tm1 * cb.c1.which + t * cb.c2.which) + t_3 * cb.end.which; \
}
static double
bezier_x(CubicBezier cb, double t) { bezier_eq(x); }
static double
bezier_y(CubicBezier cb, double t) { bezier_eq(y); }
#undef bezier_eq
static int
find_bezier_for_D(int width, int height) {
int cx = width - 1, last_cx = cx;
CubicBezier cb = {.end={0, height - 1}, .c2={0, height - 1}};
while (true) {
cb.c1.x = cx; cb.c2.x = cx;
if (bezier_x(cb, 0.5) > width - 1) return last_cx;
last_cx = cx++;
}
}
static double
find_t_for_x(CubicBezier cb, int x, double start_t) {
if (fabs(bezier_x(cb, start_t) - x) < 0.1) return start_t;
static const double t_limit = 0.5;
double increment = t_limit - start_t;
if (increment <= 0) return start_t;
while (true) {
double q = bezier_x(cb, start_t + increment);
if (fabs(q - x) < 0.1) return start_t + increment;
if (q > x) {
increment /= 2.0;
if (increment < 1e-6) {
log_error("Failed to find cubic bezier t for x=%d\n", x);
return start_t;
}
} else {
start_t += increment;
increment = t_limit - start_t;
if (increment <= 0) return start_t;
}
}
}
static void
get_bezier_limits(Canvas *self, CubicBezier cb) {
int start_x = (int)bezier_x(cb, 0), max_x = (int)bezier_x(cb, 0.5);
double last_t = 0.;
for (int x = start_x; x < max_x + 1; x++) {
if (x > start_x) last_t = find_t_for_x(cb, x, last_t);
double upper = bezier_y(cb, last_t), lower = bezier_y(cb, 1.0 - last_t);
if (fabs(upper - lower) <= 2.0) break; // avoid pip on end of D
append_limit(self, lower, upper);
}
}
static void
filled_D(Canvas *self, bool left) {
int c1x = find_bezier_for_D(self->width, self->height);
CubicBezier cb = {.end={0, self->height-1}, .c1 = {c1x, 0}, .c2 = {c1x, self->height - 1}};
get_bezier_limits(self, cb);
if (left) fill_region(self, false);
else {
RAII_ALLOC(uint8_t, mbuf, calloc(self->width, self->height));
if (!mbuf) fatal("Out of memory");
uint8_t *buf = self->mask;
self->mask = mbuf;
fill_region(self, false);
self->mask = buf;
for (uint y = 0; y < self->height; y++) {
uint offset = y * self->width;
for (uint src_x = 0; src_x < self->width; src_x++) {
uint dest_x = self->width - 1 - src_x;
buf[offset + dest_x] = mbuf[offset + src_x];
}
}
}
}
#define NAME position_set
#define KEY_TY int64_t
#include "kitty-verstable.h"
#define draw_parametrized_curve(self, level, xfunc, yfunc) { \
div_t d = div(thickness(self, level, true)); \
int delta = d.quot, extra = d.rem; \
uint num_samples = self->height * 8; \
position_set seen; vt_init(&seen); \
for (uint i = 0; i < num_samples; i++) { \
double t = i / (double)num_samples; \
int32_t x_p = xfunc, y_p = yfunc; \
int64_t key = (x_p << 32) | y_p; \
position_set_itr q = vt_get(&seen, key); \
if (!vt_is_end(q)) continue; \
if (vt_is_end(vt_insert(&seen, key))) fatal("Out of memory"); \
for (int y = MAX(0, y_p - delta); y < MIN(y_p + delta + extra, (int)self->height); y++) { \
uint offset = y * self->width, start = MAX(0, x_p - delta); \
memset(self->mask + offset + start, 255, minus((uint)MIN(x_p + delta + extra, self->width), start)); \
} \
} \
vt_cleanup(&seen); \
}
void
render_box_char(char_type ch, uint8_t *buf, unsigned width, unsigned height, double dpi_x, double dpi_y) {
Canvas canvas = {.mask=buf, .width = width, .height = height, .dpi={.x=dpi_x, .y=dpi_y}, .supersample_factor=1u}, ss = canvas;
ss.mask = buf + width*height; ss.supersample_factor = SUPERSAMPLE_FACTOR; ss.width *= SUPERSAMPLE_FACTOR; ss.height *= SUPERSAMPLE_FACTOR;
memset(canvas.mask, 0, width * height * sizeof(canvas.mask[0]));
Canvas *c = &canvas;
#define CC(expr) expr; break
#define SS(expr) memset(ss.mask, 0, ss.width * ss.height * sizeof(ss.mask[0])); c = &ss, expr; downsample(&ss, &canvas); break
#define C(ch, func, ...) case ch: CC(func(c, __VA_ARGS__))
#define S(ch, func, ...) case ch: SS(func(c, __VA_ARGS__))
switch(ch) {
C(L'', hline, 1);
C(L'', hline, 3);
C(L'', vline, 1);
C(L'', vline, 3);
C(L'', hholes, 1, 1);
C(L'', hholes, 3, 1);
C(L'', hholes, 1, 2);
C(L'', hholes, 3, 2);
C(L'', hholes, 1, 3);
C(L'', hholes, 3, 3);
C(L'', vholes, 1, 1);
C(L'', vholes, 3, 1);
C(L'', vholes, 1, 2);
C(L'', vholes, 3, 2);
C(L'', vholes, 1, 3);
C(L'', vholes, 3, 3);
C(L'', half_hline, 1, false, 0);
C(L'', half_vline, 1, false, 0);
C(L'', half_hline, 1, true, 0);
C(L'', half_vline, 1, true, 0);
C(L'', half_hline, 3, false, 0);
C(L'', half_vline, 3, false, 0);
C(L'', half_hline, 3, true, 0);
C(L'', half_vline, 3, true, 0);
case L'': CC(half_hline(c, 3, false, 0); half_hline(c, 1, true, 0));
case L'': CC(half_hline(c, 1, false, 0); half_hline(c, 3, true, 0));
case L'': CC(half_vline(c, 3, false, 0); half_vline(c, 1, true, 0));
case L'': CC(half_vline(c, 1, false, 0); half_vline(c, 3, true, 0));
S(L'', triangle, true, false);
S(L'', triangle, true, true);
case L'': SS(half_cross_line(c, 1, TOP_LEFT); half_cross_line(c, 1, BOTTOM_LEFT));
S(L'', triangle, false, false);
S(L'', triangle, false, true);
case L'': SS(half_cross_line(c, 1, TOP_RIGHT); half_cross_line(c, 1, BOTTOM_RIGHT));
S(L'', filled_D, true);
S(L'', filled_D, true);
S(L'', filled_D, false);
S(L'', filled_D, false);
}
#undef CC
#undef SS
#undef C
#undef S
free(canvas.holes); free(canvas.y_limits);
free(ss.holes); free(ss.y_limits);
}

View file

@ -23,3 +23,5 @@ DecorationGeometry add_missing_glyph(uint8_t *buf, FontCellMetrics fcm);
DecorationGeometry add_beam_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x);
DecorationGeometry add_underline_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_y);
DecorationGeometry add_hollow_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x, double dpi_y);
void render_box_char(char_type ch, uint8_t *buf, unsigned width, unsigned height, double dpi_x, double dpi_y);
#define SUPERSAMPLE_FACTOR 4u

View file

@ -998,7 +998,7 @@ def ring_bell() -> None:
pass
def concat_cells(cell_width: int, cell_height: int, is_32_bit: bool, cells: Tuple[bytes, ...]) -> bytes:
def concat_cells(cell_width: int, cell_height: int, is_32_bit: bool, cells: Tuple[bytes, ...], bgcolor: int = 0) -> bytes:
pass
@ -1715,6 +1715,7 @@ def glfw_get_system_color_theme(query_if_unintialized: bool = True) -> Literal['
def set_redirect_keys_to_overlay(os_window_id: int, tab_id: int, window_id: int, overlay_window_id: int) -> None: ...
def buffer_keys_in_window(os_window_id: int, tab_id: int, window_id: int, enabled: bool = True) -> bool: ...
def sprite_idx_to_pos(idx: int, xnum: int, ynum: int) -> tuple[int, int, int]: ...
def render_box_char(ch: int, width: int, height: int, dpi_x: float = 96.0, dpi_y: float = 96.0) -> bytes: ...
class MousePosition(TypedDict):
cell_x: int

View file

@ -67,8 +67,9 @@ typedef struct {
typedef struct Canvas {
pixel *buf;
uint8_t *alpha_mask;
unsigned current_cells, alloced_cells, alloced_scale, current_scale;
size_t size_in_bytes;
size_t size_in_bytes, alpha_mask_sz_in_bytes;
} Canvas;
#define NAME fallback_font_map_t
@ -175,6 +176,12 @@ ensure_canvas_can_fit(FontGroup *fg, unsigned cells, unsigned scale) {
fg->canvas.current_scale = scale;
if (fg->canvas.buf) memset(fg->canvas.buf, 0, cs(cells, scale));
#undef cs
size_in_bytes = (sizeof(fg->canvas.alpha_mask[0]) * SUPERSAMPLE_FACTOR * SUPERSAMPLE_FACTOR * 2 * fg->fcm.cell_width * fg->fcm.cell_height * scale * scale);
if (size_in_bytes > fg->canvas.alpha_mask_sz_in_bytes) {
fg->canvas.alpha_mask_sz_in_bytes = size_in_bytes;
fg->canvas.alpha_mask = malloc(fg->canvas.alpha_mask_sz_in_bytes * sizeof(fg->canvas.alpha_mask[0]));
if (!fg->canvas.alpha_mask) fatal("Out of memory allocating canvas");
}
}
@ -225,7 +232,7 @@ del_font(Font *f) {
static void
del_font_group(FontGroup *fg) {
free(fg->canvas.buf); fg->canvas.buf = NULL; fg->canvas = (Canvas){0};
free(fg->canvas.buf); free(fg->canvas.alpha_mask); fg->canvas = (Canvas){0};
free_sprite_data((FONTS_DATA_HANDLE)fg);
vt_cleanup(&fg->fallback_font_map);
vt_cleanup(&fg->scaled_font_map);
@ -1034,18 +1041,16 @@ render_box_cell(FontGroup *fg, RunFont rf, CPUCell *cpu_cell, GPUCell *gpu_cell,
RAII_PyObject(ret, NULL); uint8_t *alpha_mask = NULL;
ensure_canvas_can_fit(fg, num_glyphs + 1, rf.scale);
if (num_glyphs == 1) {
ret = PyObject_CallFunction(box_drawing_function, "IIId", ch, width, height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0);
if (ret == NULL) failed;
alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0));
render_box_char(ch, fg->canvas.alpha_mask, width, height, fg->logical_dpi_x, fg->logical_dpi_y);
alpha_mask = fg->canvas.alpha_mask;
} else {
alpha_mask = ((uint8_t*)fg->canvas.buf) + fg->canvas.size_in_bytes - (num_glyphs * width * height);
unsigned cnum = 0;
for (unsigned i = 0; i < num_glyphs; i++) {
unsigned int ch = global_glyph_render_scratch.lc->chars[cnum++];
while (!ch) ch = global_glyph_render_scratch.lc->chars[cnum++];
RAII_PyObject(r, PyObject_CallFunction(box_drawing_function, "IIId", ch, width, height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0));
if (r == NULL) failed;
uint8_t *src = PyLong_AsVoidPtr(PyTuple_GET_ITEM(r, 0));
render_box_char(ch, fg->canvas.alpha_mask, width, height, fg->logical_dpi_x, fg->logical_dpi_y);
uint8_t *src = fg->canvas.alpha_mask;
for (unsigned y = 0; y < height; y++) {
uint8_t *dest_row = alpha_mask + y*num_glyphs*width + i*width;
memcpy(dest_row, src + y * width, width);
@ -2076,13 +2081,27 @@ test_render_line(PyObject UNUSED *self, PyObject *args) {
Py_RETURN_NONE;
}
static uint32_t
alpha_blend(uint32_t fg, uint32_t bg) {
uint32_t r1 = (fg >> 16) & 0xFF, g1 = (fg >> 8) & 0xFF, b1 = fg & 0xFF, a = (fg >> 24) & 0xff;
uint32_t r2 = (bg >> 16) & 0xFF, g2 = (bg >> 8) & 0xFF, b2 = bg & 0xFF;
float alpha = a / 255.f;
#define mix(x) uint32_t x = ((uint32_t)(alpha * x##1 + (1.0f - alpha) * x##2)) & 0xff;
mix(r); mix(g); mix(b);
#undef mix
// Combine components into result color
return (0xff000000) | (r << 16) | (g << 8) | b;
}
static PyObject*
concat_cells(PyObject UNUSED *self, PyObject *args) {
// Concatenate cells returning RGBA data
unsigned int cell_width, cell_height;
int is_32_bit;
PyObject *cells;
if (!PyArg_ParseTuple(args, "IIpO!", &cell_width, &cell_height, &is_32_bit, &PyTuple_Type, &cells)) return NULL;
unsigned long bgcolor = 0;
if (!PyArg_ParseTuple(args, "IIpO!|k", &cell_width, &cell_height, &is_32_bit, &PyTuple_Type, &cells, &bgcolor)) return NULL;
size_t num_cells = PyTuple_GET_SIZE(cells), r, c, i;
PyObject *ans = PyBytes_FromStringAndSize(NULL, (size_t)4 * cell_width * cell_height * num_cells);
if (ans == NULL) return PyErr_NoMemory();
@ -2092,22 +2111,11 @@ concat_cells(PyObject UNUSED *self, PyObject *args) {
void *s = ((uint8_t*)PyBytes_AS_STRING(PyTuple_GET_ITEM(cells, c)));
if (is_32_bit) {
pixel *src = (pixel*)s + cell_width * r;
for (i = 0; i < cell_width; i++, dest++) {
uint8_t *rgba = (uint8_t*)dest;
rgba[0] = (src[i] >> 24) & 0xff;
rgba[1] = (src[i] >> 16) & 0xff;
rgba[2] = (src[i] >> 8) & 0xff;
rgba[3] = src[i] & 0xff;
}
for (i = 0; i < cell_width; i++, dest++) dest[0] = alpha_blend(src[0], bgcolor);
} else {
uint8_t *src = (uint8_t*)s + cell_width * r;
for (i = 0; i < cell_width; i++, dest++) {
uint8_t *rgba = (uint8_t*)dest;
if (src[i]) { memset(rgba, 0xff, 3); rgba[3] = src[i]; }
else *dest = 0;
}
for (i = 0; i < cell_width; i++, dest++) dest[0] = alpha_blend(0x00ffffff | ((src[i] & 0xff) << 24), bgcolor);
}
}
}
return ans;
@ -2293,6 +2301,18 @@ sprite_idx_to_pos(PyObject *self UNUSED, PyObject *args) {
return Py_BuildValue("III", x, y, z);
}
static PyObject*
pyrender_box_char(PyObject *self UNUSED, PyObject *args) {
unsigned int ch;
unsigned long width, height; double dpi_x = 96., dpi_y = 96.;
if (!PyArg_ParseTuple(args, "Ikk|dd", &ch, &width, &height, &dpi_x, &dpi_y)) return NULL;
RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, width*16 * height*16));
if (!ans) return NULL;
render_box_char(ch, (uint8_t*)PyBytes_AS_STRING(ans), width, height, dpi_x, dpi_y);
if (_PyBytes_Resize(&ans, width * height) != 0) return NULL;
return Py_NewRef(ans);
}
static PyMethodDef module_methods[] = {
METHODB(set_font_data, METH_VARARGS),
METHODB(sprite_idx_to_pos, METH_VARARGS),
@ -2308,6 +2328,7 @@ static PyMethodDef module_methods[] = {
METHODB(test_render_line, METH_VARARGS),
METHODB(get_fallback_font, METH_VARARGS),
{"specialize_font_descriptor", (PyCFunction)pyspecialize_font_descriptor, METH_VARARGS, ""},
{"render_box_char", (PyCFunction)pyrender_box_char, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View file

@ -8,8 +8,7 @@
import math
from collections.abc import Iterable, Iterator, MutableSequence, Sequence
from functools import lru_cache, wraps
from functools import partial as p
from functools import lru_cache, partial
from itertools import repeat
from typing import Any, Callable, Literal, Optional
@ -18,6 +17,13 @@ _dpi = 96.0
BufType = MutableSequence[int]
def p(f: Any, *a: Any, **kw: Any) -> Any:
ans = partial(f, *a, **kw)
if hasattr(f, 'supersample_factor'):
setattr(ans, 'supersample_factor', f.supersample_factor)
return ans
def set_scale(new_scale: Sequence[float]) -> None:
global scale
scale = (new_scale[0], new_scale[1], new_scale[2], new_scale[3])
@ -144,6 +150,11 @@ def cross(buf: BufType, width: int, height: int, a: int = 1, b: int = 1, c: int
half_vline(buf, width, height, level=d, which='bottom')
def print_hash(x: bytes, prefix: str = 'native:') -> None:
from hashlib import sha256
print(prefix, sha256(x).hexdigest())
def downsample(src: BufType, dest: BufType, dest_width: int, dest_height: int, factor: int = 4) -> None:
src_width = factor * dest_width
@ -167,19 +178,23 @@ class SSByteArray(bytearray):
supersample_factor = 1
def ss(buf: BufType, width: int, height: int, *funcs: Callable[..., None]) -> None:
supersample_factor = getattr(funcs[0], 'supersample_factor')
w, h = supersample_factor * width, supersample_factor * height
ssbuf = SSByteArray(w * h)
ssbuf.supersample_factor = supersample_factor
for f in funcs:
f(ssbuf, w, h)
downsample(ssbuf, buf, width, height, factor=supersample_factor)
def supersampled(supersample_factor: int = 4) -> Callable[[Callable[..., None]], Callable[..., None]]:
# Anti-alias the drawing performed by the wrapped function by
# using supersampling
def create_wrapper(f: Callable[..., None]) -> Callable[..., None]:
@wraps(f)
def supersampled_wrapper(buf: BufType, width: int, height: int, *args: Any, **kw: Any) -> None:
w, h = supersample_factor * width, supersample_factor * height
ssbuf = SSByteArray(w * h)
ssbuf.supersample_factor = supersample_factor
f(ssbuf, w, h, *args, **kw)
downsample(ssbuf, buf, width, height, factor=supersample_factor)
return supersampled_wrapper
setattr(f, 'supersample_factor', supersample_factor)
return f
return create_wrapper
@ -1429,32 +1444,84 @@ for i in range(1, 7):
def render_box_char(ch: str, buf: BufType, width: int, height: int, dpi: float = 96.0) -> BufType:
global _dpi
_dpi = dpi
for func in box_chars[ch]:
func(buf, width, height)
funcs = box_chars[ch]
if hasattr(funcs[0], 'supersample_factor'):
ss(buf, width, height, *funcs)
else:
for func in box_chars[ch]:
func(buf, width, height)
return buf
def test_char(ch: str, sz: int = 48) -> None:
# kitty +runpy "from kitty.fonts.box_drawing import test_char; test_char('XXX')"
def test_chars(chars: str = '', sz: int = 128) -> None:
# kitty +runpy "from kitty.fonts.box_drawing import test_chars; test_chars('XXX')"
from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu
from kitty.fast_data_types import render_box_char as native_render_box_char
from .render import display_bitmap, setup_for_testing
if not chars:
import sys
chars = sys.argv[-1]
with setup_for_testing('monospace', sz) as (_, width, height):
buf = bytearray(width * height)
try:
render_box_char(ch, buf, width, height)
for ch in chars:
print('Rendering', ch)
buf = bytearray(width * height)
render_box_char(ch, buf, width, height)
def join_cells(*cells: bytes) -> bytes:
cells = tuple(bytes(x) for x in cells)
return concat_cells(width, height, False, cells)
def join_cells(*cells: bytes) -> bytes:
cells = tuple(bytes(x) for x in cells)
return concat_cells(width, height, False, cells)
rgb_data = join_cells(buf)
display_bitmap(rgb_data, width, height)
print()
rgb_data = join_cells(buf)
display_bitmap(rgb_data, width, height)
print()
nb = native_render_box_char(ord(ch), width, height)
rgb_data = concat_cells(width, height, False, (nb,))
display_bitmap(rgb_data, width, height)
print()
finally:
set_send_sprite_to_gpu(None)
def port_chars() -> None:
from kitty.fast_data_types import concat_cells
from kitty.fast_data_types import render_box_char as native_render_box_char
from .render import display_bitmap, setup_for_testing
def join_cells(*cells: bytes) -> bytes:
cells = tuple(bytes(x) for x in cells)
return concat_cells(width, height, False, cells)
for sz in (127, 8, 11, 12, 13):
with setup_for_testing('monospace', sz) as (_, width, height):
for ch in box_chars:
buf = bytearray(width * height)
render_box_char(ch, buf, width, height)
nb = native_render_box_char(ord(ch), width, height)
if bytes(buf) != nb:
print(f'Failed to match for char: {ch=} ({hex(ord(ch))}) at {width=} {height=}')
count = 0
for y in range(height):
for x in range(width):
if buf[y*width + x] != nb[y*width + x]:
print(f'differing byte at {x=} {y=}. Expected: {buf[y*width + x]} Actual: {nb[y*width + x]}')
count += 1
if count > 5:
break
if count > 5:
break
rgb_data = join_cells(buf)
display_bitmap(rgb_data, width, height)
print()
rgb_data = concat_cells(width, height, False, (nb,))
display_bitmap(rgb_data, width, height)
print()
raise SystemExit(1)
def test_drawing(sz: int = 48, family: str = 'monospace', start: int = 0x2500, num_rows: int = 10, num_cols: int = 16) -> None:
from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu

View file

@ -199,7 +199,7 @@ might cause rendering artifacts, so use with care.
''')
opt('box_drawing_scale', '0.001, 1, 1.5, 2',
option_type='box_drawing_scale',
option_type='box_drawing_scale', ctype='!box_drawing_scale',
long_text='''
The sizes of the lines used for the box drawing Unicode characters. These values
are in pts. They will be scaled by the monitor DPI to arrive at a pixel value.

View file

@ -70,6 +70,19 @@ convert_from_opts_modify_font(PyObject *py_opts, Options *opts) {
Py_DECREF(ret);
}
static void
convert_from_python_box_drawing_scale(PyObject *val, Options *opts) {
box_drawing_scale(val, opts);
}
static void
convert_from_opts_box_drawing_scale(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "box_drawing_scale");
if (ret == NULL) return;
convert_from_python_box_drawing_scale(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_undercurl_style(PyObject *val, Options *opts) {
opts->undercurl_style = undercurl_style(val);
@ -1174,6 +1187,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
if (PyErr_Occurred()) return false;
convert_from_opts_modify_font(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_box_drawing_scale(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_undercurl_style(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_underline_exclusion(py_opts, opts);

View file

@ -390,6 +390,13 @@ underline_exclusion(PyObject *val, Options *opts) {
else opts->underline_exclusion.unit = 0;
}
static inline void
box_drawing_scale(PyObject *val, Options *opts) {
for (unsigned i = 0; i < MIN(arraysz(opts->box_drawing_scale), (size_t)PyTuple_GET_SIZE(val)); i++) {
opts->box_drawing_scale[i] = PyFloat_AsFloat(PyTuple_GET_ITEM(val, i));
}
}
static inline void
text_composition_strategy(PyObject *val, Options *opts) {
if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; }

View file

@ -125,6 +125,7 @@ typedef struct {
struct { Animation *cursor, *visual_bell; } animation;
unsigned undercurl_style;
struct { float thickness; int unit; } underline_exclusion;
float box_drawing_scale[4];
} Options;
typedef struct WindowLogoRenderData {