Refactor the cell rendering code for greater re-use

Also start work on CoreText based rendering
This commit is contained in:
Kovid Goyal 2017-01-11 12:02:28 +05:30
parent 282d6faa5f
commit dff91759a2
6 changed files with 204 additions and 81 deletions

View File

@ -8,6 +8,8 @@
#include "data-types.h" #include "data-types.h"
#include <structmember.h> #include <structmember.h>
#include <stdint.h> #include <stdint.h>
#include <math.h>
#import <CoreGraphics/CGBitmapContext.h>
#import <CoreText/CTFont.h> #import <CoreText/CTFont.h>
#import <Foundation/NSString.h> #import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h> #import <Foundation/NSDictionary.h>
@ -62,6 +64,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self->underline_thickness = CTFontGetUnderlineThickness(self->font); self->underline_thickness = CTFontGetUnderlineThickness(self->font);
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font)); self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font));
self->full_name = convert_cfstring(CTFontCopyFullName(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); } if (self->family_name == NULL || self->full_name == NULL) { Py_CLEAR(self); }
} }
} else { } else {
@ -104,6 +107,84 @@ has_char(Face *self, PyObject *args) {
return ret; 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 {{{ // Boilerplate {{{
static PyMemberDef members[] = { static PyMemberDef members[] = {
@ -123,6 +204,9 @@ static PyMemberDef members[] = {
static PyMethodDef methods[] = { static PyMethodDef methods[] = {
METHOD(has_char, METH_VARARGS) METHOD(has_char, METH_VARARGS)
METHOD(cell_size, METH_NOARGS)
METHOD(font_units_to_pixels, METH_VARARGS)
METHOD(render_char, METH_VARARGS)
{NULL} /* Sentinel */ {NULL} /* Sentinel */
}; };

View File

@ -350,7 +350,7 @@ def join_rows(width, height, rows):
def test_drawing(sz=32, family='monospace'): 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) width, height = set_font_family(family, sz)
pos = 0x2500 pos = 0x2500
rows = [] rows = []

View File

@ -2,25 +2,47 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
from kitty.fast_data_types import CTFace as Face 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 = {} main_font = {}
cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None
def set_font_family(family, size_in_pts): 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 dpi = sum(dpi) / 2.0
if family.lower() == 'monospace': if family.lower() == 'monospace':
family = 'Menlo' family = 'Menlo'
for bold in (False, True): for bold in (False, True):
for italic in (False, True): for italic in (False, True):
main_font[(bold, italic)] = Face(family, bold, italic, True, size_in_pts, dpi) 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): def current_cell():
pass 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(): def develop():
@ -32,3 +54,4 @@ def develop():
f = main_font[(False, False)] f = main_font[(False, False)]
for attr in 'units_per_em ascent descent leading underline_position underline_thickness scaled_point_sz'.split(): for attr in 'units_per_em ascent descent leading underline_position underline_thickness scaled_point_sz'.split():
print(attr, getattr(f, attr)) print(attr, getattr(f, attr))
print('cell_width: {}, cell_height: {}, baseline: {}'.format(cell_width, cell_height, baseline))

View File

@ -4,14 +4,13 @@
import unicodedata import unicodedata
import ctypes import ctypes
import math
from collections import namedtuple from collections import namedtuple
from functools import lru_cache from functools import lru_cache
from threading import Lock from threading import Lock
from kitty.fast_data_types import Face, FT_PIXEL_MODE_GRAY 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 .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 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) face.load_char(text, font.hinting, font.hintstyle)
def ceil_int(x):
return int(math.ceil(x))
def calc_cell_width(font, face): def calc_cell_width(font, face):
ans = 0 ans = 0
for i in range(32, 128): for i in range(32, 128):
@ -169,33 +164,14 @@ def split_char_bitmap(bitmap_char):
return first, second return first, second
def add_line(buf, position, thickness): def current_cell():
y = position - thickness // 2 return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position
while thickness:
thickness -= 1
offset = cell_width * y
for x in range(cell_width):
buf[offset + x] = 255
y += 1
def add_curl(buf, position, thickness): def render_cell(text=' ', bold=False, italic=False):
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):
# TODO: Handle non-normalizable combining chars. Probably need to use # TODO: Handle non-normalizable combining chars. Probably need to use
# harfbuzz for that # harfbuzz for that
text = unicodedata.normalize('NFC', text)[0] 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) width = wcwidth(text)
bitmap_char = render_char(text, bold, italic, width) bitmap_char = render_char(text, bold, italic, width)
second = None 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) 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 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 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] dest[dr:dr + column_count] = src[sr:sr + column_count]
return dest 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)

View File

@ -2,9 +2,88 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # 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: 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: 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)

View File

@ -8,6 +8,7 @@ import signal
import shlex import shlex
import subprocess import subprocess
import ctypes import ctypes
import math
from collections import namedtuple from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from functools import lru_cache from functools import lru_cache
@ -30,6 +31,10 @@ def safe_print(*a, **k):
pass pass
def ceil_int(x):
return int(math.ceil(x))
@lru_cache(maxsize=2**13) @lru_cache(maxsize=2**13)
def wcwidth(c: str) -> int: def wcwidth(c: str) -> int:
ans = min(2, wcwidth_native(c)) ans = min(2, wcwidth_native(c))