Refactor the cell rendering code for greater re-use
Also start work on CoreText based rendering
This commit is contained in:
parent
282d6faa5f
commit
dff91759a2
@ -8,6 +8,8 @@
|
||||
#include "data-types.h"
|
||||
#include <structmember.h>
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#import <CoreGraphics/CGBitmapContext.h>
|
||||
#import <CoreText/CTFont.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
@ -62,6 +64,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
self->underline_thickness = CTFontGetUnderlineThickness(self->font);
|
||||
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font));
|
||||
self->full_name = convert_cfstring(CTFontCopyFullName(self->font));
|
||||
self->scaled_point_sz = CTFontGetSize(self->font);
|
||||
if (self->family_name == NULL || self->full_name == NULL) { Py_CLEAR(self); }
|
||||
}
|
||||
} else {
|
||||
@ -104,6 +107,84 @@ has_char(Face *self, PyObject *args) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
font_units_to_pixels(Face *self, PyObject *args) {
|
||||
#define font_units_to_pixels_doc "Convert the specified value from font units to pixels at the current font size"
|
||||
double x;
|
||||
if (!PyArg_ParseTuple(args, "d", &x)) return NULL;
|
||||
x *= self->scaled_point_sz / self->units_per_em;
|
||||
return Py_BuildValue("i", (int)ceil(x));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
cell_size(Face *self) {
|
||||
#define cell_size_doc "Return the best cell size for this font based on the advances for the ASCII chars from 32 to 127"
|
||||
#define count (128 - 32)
|
||||
unichar chars[count+1] = {0};
|
||||
CGGlyph glyphs[count+1] = {0};
|
||||
for (int i = 0; i < count; i++) chars[i] = 32 + i;
|
||||
CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count);
|
||||
CGSize advances[1] = {0};
|
||||
unsigned int width = 0, w;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (glyphs[i]) {
|
||||
CTFontGetAdvancesForGlyphs(self->font, kCTFontHorizontalOrientation, glyphs+1, advances, 1);
|
||||
w = (unsigned int)(ceilf(advances[0].width));
|
||||
if (w > width) width = w;
|
||||
}
|
||||
}
|
||||
return Py_BuildValue("I", width + 2); // + 2 for antialiasing which needs pixels on either side
|
||||
#undef count
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
render_char(Face *self, PyObject *args) {
|
||||
#define render_char_doc ""
|
||||
char *s;
|
||||
unsigned int width, height;
|
||||
PyObject *pbuf;
|
||||
CGColorSpaceRef color_space = NULL;
|
||||
CGContextRef ctx = NULL;
|
||||
CTFontRef font = NULL;
|
||||
if (!PyArg_ParseTuple(args, "esIIO!", "UTF-8", &s, &width, &height, &PyLong_Type, &pbuf)) return NULL;
|
||||
uint8_t *buf = (uint8_t*)PyLong_AsVoidPtr(pbuf);
|
||||
CFStringRef str = CFStringCreateWithCString(NULL, s, kCFStringEncodingUTF8);
|
||||
if (!str) return PyErr_NoMemory();
|
||||
CGGlyph glyphs[10] = {0};
|
||||
unichar chars[10] = {0};
|
||||
CFRange range = CFRangeMake(0, CFStringGetLength(str));
|
||||
CFStringGetCharacters(str, range, chars);
|
||||
font = CTFontCreateForString(self->font, str, range);
|
||||
if (font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback font"); goto end; }
|
||||
CTFontGetGlyphsForCharacters(font, chars, glyphs, range.length);
|
||||
color_space = CGColorSpaceCreateDeviceGray();
|
||||
if (color_space == NULL) { PyErr_NoMemory(); goto end; }
|
||||
ctx = CGBitmapContextCreate(buf, width, height, 8, width, color_space, (kCGBitmapAlphaInfoMask & kCGImageAlphaNone));
|
||||
if (ctx == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create bitmap context"); goto end; }
|
||||
CGContextSetShouldAntialias(ctx, true);
|
||||
CGContextSetShouldSmoothFonts(ctx, true); // sub-pixel antialias
|
||||
CGContextSetRGBFillColor(ctx, 1, 1, 1, 1); // white glyphs
|
||||
CGAffineTransform transform = CGAffineTransformIdentity;
|
||||
CGContextSetTextDrawingMode(ctx, kCGTextFill);
|
||||
CGGlyph glyph = glyphs[0];
|
||||
if (glyph) {
|
||||
CGRect rect = CTFontGetBoundingRectsForGlyphs(font, kCTFontHorizontalOrientation, glyphs, 0, 1);
|
||||
// TODO: Scale the glyph if its bbox is larger than the image by using a non-identity transform
|
||||
CGContextSetTextMatrix(ctx, transform);
|
||||
CGFloat pos_x = -rect.origin.x + 1, pos_y = height - rect.origin.y - rect.size.height;
|
||||
CGContextSetTextPosition(ctx, pos_x, pos_y);
|
||||
CTFontDrawGlyphs(font, &glyph, &CGPointZero, 1, ctx);
|
||||
}
|
||||
|
||||
end:
|
||||
CFRelease(str);
|
||||
if (ctx) CGContextRelease(ctx);
|
||||
if (color_space) CGColorSpaceRelease(color_space);
|
||||
if (font && font != self->font) CFRelease(font);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Boilerplate {{{
|
||||
|
||||
static PyMemberDef members[] = {
|
||||
@ -123,6 +204,9 @@ static PyMemberDef members[] = {
|
||||
|
||||
static PyMethodDef methods[] = {
|
||||
METHOD(has_char, METH_VARARGS)
|
||||
METHOD(cell_size, METH_NOARGS)
|
||||
METHOD(font_units_to_pixels, METH_VARARGS)
|
||||
METHOD(render_char, METH_VARARGS)
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
@ -350,7 +350,7 @@ def join_rows(width, height, rows):
|
||||
|
||||
|
||||
def test_drawing(sz=32, family='monospace'):
|
||||
from .freetype import join_cells, display_bitmap, render_cell, set_font_family
|
||||
from .render import join_cells, display_bitmap, render_cell, set_font_family
|
||||
width, height = set_font_family(family, sz)
|
||||
pos = 0x2500
|
||||
rows = []
|
||||
|
||||
@ -2,25 +2,47 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import ctypes
|
||||
from kitty.fast_data_types import CTFace as Face
|
||||
from kitty.utils import get_dpi
|
||||
|
||||
from kitty.utils import get_logical_dpi, wcwidth, ceil_int
|
||||
|
||||
main_font = {}
|
||||
cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None
|
||||
|
||||
|
||||
def set_font_family(family, size_in_pts):
|
||||
dpi = get_dpi()['logical']
|
||||
global cell_width, cell_height, baseline, CellTexture, WideCellTexture, underline_thickness, underline_position
|
||||
dpi = get_logical_dpi()
|
||||
dpi = sum(dpi) / 2.0
|
||||
if family.lower() == 'monospace':
|
||||
family = 'Menlo'
|
||||
for bold in (False, True):
|
||||
for italic in (False, True):
|
||||
main_font[(bold, italic)] = Face(family, bold, italic, True, size_in_pts, dpi)
|
||||
mf = main_font[(False, False)]
|
||||
cell_width = mf.cell_size()
|
||||
cell_height = ceil_int(mf.ascent + mf.descent)
|
||||
cell_height += 1 * int(round(dpi / 72.0)) # some line spacing
|
||||
CellTexture = ctypes.c_ubyte * (cell_width * cell_height)
|
||||
WideCellTexture = ctypes.c_ubyte * (2 * cell_width * cell_height)
|
||||
baseline = int(round(mf.ascent))
|
||||
underline_position = int(round(mf.underline_position))
|
||||
underline_thickness = ceil_int(mf.underline_thickness)
|
||||
return cell_width, cell_height
|
||||
|
||||
|
||||
def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False):
|
||||
pass
|
||||
def current_cell():
|
||||
return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position
|
||||
|
||||
|
||||
def render_cell(text=' ', bold=False, italic=False):
|
||||
width = wcwidth(text[0])
|
||||
face = main_font[(bold, italic)]
|
||||
if width == 2:
|
||||
buf, width = WideCellTexture(), cell_width * 2
|
||||
else:
|
||||
buf, width = CellTexture(), cell_width
|
||||
face.render_char(text, width, cell_height, ctypes.addressof(buf))
|
||||
|
||||
|
||||
def develop():
|
||||
@ -32,3 +54,4 @@ def develop():
|
||||
f = main_font[(False, False)]
|
||||
for attr in 'units_per_em ascent descent leading underline_position underline_thickness scaled_point_sz'.split():
|
||||
print(attr, getattr(f, attr))
|
||||
print('cell_width: {}, cell_height: {}, baseline: {}'.format(cell_width, cell_height, baseline))
|
||||
|
||||
@ -4,14 +4,13 @@
|
||||
|
||||
import unicodedata
|
||||
import ctypes
|
||||
import math
|
||||
from collections import namedtuple
|
||||
from functools import lru_cache
|
||||
from threading import Lock
|
||||
|
||||
from kitty.fast_data_types import Face, FT_PIXEL_MODE_GRAY
|
||||
from kitty.utils import ceil_int
|
||||
from .fontconfig import find_font_for_character, get_font_files
|
||||
from .box_drawing import is_renderable_box_char, render_box_char
|
||||
|
||||
from kitty.utils import get_logical_dpi, wcwidth
|
||||
|
||||
@ -29,10 +28,6 @@ def load_char(font, face, text):
|
||||
face.load_char(text, font.hinting, font.hintstyle)
|
||||
|
||||
|
||||
def ceil_int(x):
|
||||
return int(math.ceil(x))
|
||||
|
||||
|
||||
def calc_cell_width(font, face):
|
||||
ans = 0
|
||||
for i in range(32, 128):
|
||||
@ -169,33 +164,14 @@ def split_char_bitmap(bitmap_char):
|
||||
return first, second
|
||||
|
||||
|
||||
def add_line(buf, position, thickness):
|
||||
y = position - thickness // 2
|
||||
while thickness:
|
||||
thickness -= 1
|
||||
offset = cell_width * y
|
||||
for x in range(cell_width):
|
||||
buf[offset + x] = 255
|
||||
y += 1
|
||||
def current_cell():
|
||||
return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position
|
||||
|
||||
|
||||
def add_curl(buf, position, thickness):
|
||||
for y in range(position - thickness, position):
|
||||
for x in range(0, cell_width // 2):
|
||||
offset = cell_width * y
|
||||
buf[offset + x] = 255
|
||||
for y in range(position, position + thickness):
|
||||
for x in range(cell_width // 2, cell_width):
|
||||
offset = cell_width * y
|
||||
buf[offset + x] = 255
|
||||
|
||||
|
||||
def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False):
|
||||
def render_cell(text=' ', bold=False, italic=False):
|
||||
# TODO: Handle non-normalizable combining chars. Probably need to use
|
||||
# harfbuzz for that
|
||||
text = unicodedata.normalize('NFC', text)[0]
|
||||
if is_renderable_box_char(text):
|
||||
return render_box_char(text, CharTexture(), cell_width, cell_height), None
|
||||
width = wcwidth(text)
|
||||
bitmap_char = render_char(text, bold, italic, width)
|
||||
second = None
|
||||
@ -208,19 +184,6 @@ def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=F
|
||||
|
||||
first = place_char_in_cell(bitmap_char)
|
||||
|
||||
def dl(f, *a):
|
||||
f(first, *a)
|
||||
if second is not None:
|
||||
f(second, pos, underline_thickness)
|
||||
|
||||
if underline:
|
||||
t = underline_thickness
|
||||
if underline == 2:
|
||||
t = max(1, min(cell_height - underline_position - 1, t))
|
||||
dl(add_curl if underline == 2 else add_line, underline_position, t)
|
||||
if strikethrough:
|
||||
pos = int(0.65 * baseline)
|
||||
dl(add_line, pos, underline_thickness)
|
||||
return first, second
|
||||
|
||||
|
||||
@ -232,34 +195,3 @@ def create_cell_buffer(bitmap_char, src_start_row, dest_start_row, row_count, sr
|
||||
sr, dr = src_start_column + (src_start_row + r) * src_stride, dest_start_column + (dest_start_row + r) * cell_width
|
||||
dest[dr:dr + column_count] = src[sr:sr + column_count]
|
||||
return dest
|
||||
|
||||
|
||||
def join_cells(cell_width, cell_height, *cells):
|
||||
dstride = len(cells) * cell_width
|
||||
ans = (ctypes.c_ubyte * (cell_height * dstride))()
|
||||
for r in range(cell_height):
|
||||
soff = r * cell_width
|
||||
doff = r * dstride
|
||||
for cnum, cell in enumerate(cells):
|
||||
doff2 = doff + (cnum * cell_width)
|
||||
ans[doff2:doff2 + cell_width] = cell[soff:soff + cell_width]
|
||||
return ans
|
||||
|
||||
|
||||
def display_bitmap(data, w, h):
|
||||
from PIL import Image
|
||||
img = Image.new('L', (w, h))
|
||||
img.putdata(data)
|
||||
img.show()
|
||||
|
||||
|
||||
def test_rendering(text='\'Ping👁a⧽', sz=144, family='Ubuntu Mono for Kovid'):
|
||||
set_font_family(family, sz)
|
||||
cells = []
|
||||
for c in text:
|
||||
f, s = render_cell(c, underline=2, strikethrough=True)
|
||||
cells.append(f)
|
||||
if s is not None:
|
||||
cells.append(s)
|
||||
char_data = join_cells(cell_width, cell_height, *cells)
|
||||
display_bitmap(char_data, cell_width * len(cells), cell_height)
|
||||
|
||||
@ -2,9 +2,88 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from kitty.constants import isosx
|
||||
import ctypes
|
||||
|
||||
from kitty.constants import isosx
|
||||
from .box_drawing import render_box_char, is_renderable_box_char
|
||||
if isosx:
|
||||
from .core_text import set_font_family, render_cell # noqa
|
||||
from .core_text import set_font_family, render_cell as rc, current_cell # noqa
|
||||
else:
|
||||
from .freetype import set_font_family, render_cell # noqa
|
||||
from .freetype import set_font_family, render_cell as rc, current_cell # noqa
|
||||
|
||||
|
||||
def add_line(buf, cell_width, position, thickness):
|
||||
y = position - thickness // 2
|
||||
while thickness:
|
||||
thickness -= 1
|
||||
offset = cell_width * y
|
||||
for x in range(cell_width):
|
||||
buf[offset + x] = 255
|
||||
y += 1
|
||||
|
||||
|
||||
def add_curl(buf, cell_width, position, thickness):
|
||||
for y in range(position - thickness, position):
|
||||
for x in range(0, cell_width // 2):
|
||||
offset = cell_width * y
|
||||
buf[offset + x] = 255
|
||||
for y in range(position, position + thickness):
|
||||
for x in range(cell_width // 2, cell_width):
|
||||
offset = cell_width * y
|
||||
buf[offset + x] = 255
|
||||
|
||||
|
||||
def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False):
|
||||
CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position = current_cell()
|
||||
if is_renderable_box_char(text):
|
||||
first, second = render_box_char(text, CharTexture(), cell_width, cell_height), None
|
||||
else:
|
||||
first, second = rc(text, bold, italic)
|
||||
|
||||
def dl(f, *a):
|
||||
f(first, cell_width, *a)
|
||||
if second is not None:
|
||||
f(second, cell_width, *a)
|
||||
|
||||
if underline:
|
||||
t = underline_thickness
|
||||
if underline == 2:
|
||||
t = max(1, min(cell_height - underline_position - 1, t))
|
||||
dl(add_curl if underline == 2 else add_line, underline_position, t)
|
||||
if strikethrough:
|
||||
pos = int(0.65 * baseline)
|
||||
dl(add_line, pos, underline_thickness)
|
||||
|
||||
return first, second
|
||||
|
||||
|
||||
def join_cells(cell_width, cell_height, *cells):
|
||||
dstride = len(cells) * cell_width
|
||||
ans = (ctypes.c_ubyte * (cell_height * dstride))()
|
||||
for r in range(cell_height):
|
||||
soff = r * cell_width
|
||||
doff = r * dstride
|
||||
for cnum, cell in enumerate(cells):
|
||||
doff2 = doff + (cnum * cell_width)
|
||||
ans[doff2:doff2 + cell_width] = cell[soff:soff + cell_width]
|
||||
return ans
|
||||
|
||||
|
||||
def display_bitmap(data, w, h):
|
||||
from PIL import Image
|
||||
img = Image.new('L', (w, h))
|
||||
img.putdata(data)
|
||||
img.show()
|
||||
|
||||
|
||||
def test_rendering(text='\'Ping👁a⧽', sz=144, family='Ubuntu Mono for Kovid'):
|
||||
set_font_family(family, sz)
|
||||
cells = []
|
||||
for c in text:
|
||||
f, s = render_cell(c, underline=2, strikethrough=True)
|
||||
cells.append(f)
|
||||
if s is not None:
|
||||
cells.append(s)
|
||||
cell_width, cell_height = current_cell()[1:3]
|
||||
char_data = join_cells(cell_width, cell_height, *cells)
|
||||
display_bitmap(char_data, cell_width * len(cells), cell_height)
|
||||
|
||||
@ -8,6 +8,7 @@ import signal
|
||||
import shlex
|
||||
import subprocess
|
||||
import ctypes
|
||||
import math
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
@ -30,6 +31,10 @@ def safe_print(*a, **k):
|
||||
pass
|
||||
|
||||
|
||||
def ceil_int(x):
|
||||
return int(math.ceil(x))
|
||||
|
||||
|
||||
@lru_cache(maxsize=2**13)
|
||||
def wcwidth(c: str) -> int:
|
||||
ans = min(2, wcwidth_native(c))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user