More work on line based rendering

Should now build on all platforms
This commit is contained in:
Kovid Goyal 2017-10-31 02:59:39 +05:30
parent d873d11202
commit 38a5e76c50
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 148 additions and 124 deletions

View File

@ -118,18 +118,7 @@ coretext_all_fonts(PyObject UNUSED *_self) {
}
static inline bool
apply_size(Face *self, float point_sz, float dpi) {
self->point_sz = point_sz;
self->dpi = dpi;
self->scaled_point_sz = (dpi / 72.0) * point_sz;
if (self->font) {
CTFontRef f = CTFontCreateCopyWithAttributes(self->font, self->scaled_point_sz, NULL, NULL);
CFRelease(self->font);
self->font = f;
} else {
self->font = CTFontCreateWithFontDescriptor(self->descriptor, self->scaled_point_sz, NULL);
}
if (self->font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create or copy font object"); return false; }
init_font(Face *self) {
self->units_per_em = CTFontGetUnitsPerEm(self->font);
self->ascent = CTFontGetAscent(self->font);
self->descent = CTFontGetDescent(self->font);
@ -151,6 +140,34 @@ apply_size(Face *self, float point_sz, float dpi) {
return true;
}
static inline bool
apply_size(Face *self, float point_sz, float dpi) {
self->point_sz = point_sz;
self->dpi = dpi;
self->scaled_point_sz = (dpi / 72.0) * point_sz;
if (self->font) {
CTFontRef f = CTFontCreateCopyWithAttributes(self->font, self->scaled_point_sz, NULL, NULL);
CFRelease(self->font);
self->font = f;
} else {
self->font = CTFontCreateWithFontDescriptor(self->descriptor, self->scaled_point_sz, NULL);
}
if (self->font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create or copy font object"); return false; }
return init_font(self);
}
static inline bool
init_font_names(Face *self) {
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1);
self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1);
NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute);
self->path = PyUnicode_FromString([[url path] UTF8String]);
[url release];
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) {return false;}
return true;
}
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
Face *self;
@ -163,13 +180,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
if (desc) {
self->descriptor = desc;
if (apply_size(self, point_sz, dpi)) {
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1);
self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1);
NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute);
self->path = PyUnicode_FromString([[url path] UTF8String]);
[url release];
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
if (!init_font_names(self)) Py_CLEAR(self);
} else Py_CLEAR(self);
} else {
Py_CLEAR(self);
@ -209,6 +220,32 @@ face_has_codepoint(PyObject *s, char_type ch) {
return CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count);
}
static PyObject*
face_for_text(Face *self, PyObject *args) {
char *s;
int bold, italic;
Face *ans = NULL;
if (!PyArg_ParseTuple(args, "espp", "UTF-8", &s, &bold, &italic)) return NULL;
CFStringRef str = CFStringCreateWithCString(NULL, s, kCFStringEncodingUTF8);
if (!str) return PyErr_NoMemory();
unichar chars[10] = {0};
CFRange range = CFRangeMake(0, CFStringGetLength(str));
CFStringGetCharacters(str, range, chars);
CTFontRef font = CTFontCreateForString(self->font, str, range);
if (font == self->font) return (PyObject*)self;
if (font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback font"); goto end; }
ans = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
if (ans == NULL) { PyErr_NoMemory(); goto end; }
ans->font = font;
if (!init_font(ans) || !init_font_names(ans)) { Py_CLEAR(ans); goto end; }
ans->point_sz = self->point_sz;
ans->dpi = self->dpi;
end:
CFRelease(str);
if (ans) return (PyObject*)ans;
return NULL;
}
static inline void
calc_cell_size(Face *self, unsigned int *cell_width, unsigned int *cell_height) {
#define count (128 - 32)
@ -284,6 +321,7 @@ static PyMemberDef members[] = {
};
static PyMethodDef methods[] = {
METHODB(face_for_text, METH_VARARGS),
{NULL} /* Sentinel */
};

View File

@ -2,17 +2,17 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
import re
import sys
from collections import namedtuple
from kitty.fast_data_types import CTFace as Face, coretext_all_fonts
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth, adjust_line_height
from kitty.utils import safe_print
main_font = {}
symbol_map = {}
cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
attr_map = {(False, False): 'font_family',
(True, False): 'bold_font',
(False, True): 'italic_font',
(True, True): 'bold_italic_font'}
def create_font_map(all_fonts):
@ -27,11 +27,21 @@ def create_font_map(all_fonts):
return ans
def find_best_match(font_map, family, bold, italic):
def all_fonts_map():
ans = getattr(all_fonts_map, 'ans', None)
if ans is None:
ans = all_fonts_map.ans = create_font_map(coretext_all_fonts())
return ans
def find_best_match(family, bold, italic):
q = re.sub(r'\s+', ' ', family.lower())
font_map = all_fonts_map()
def score(candidate):
style_match = 1 if candidate['bold'] == bold and candidate['italic'] == italic else 0
style_match = 1 if candidate['bold'] == bold and candidate[
'italic'
] == italic else 0
monospace_match = 1 if candidate['monospace'] else 0
return style_match, monospace_match
@ -45,7 +55,10 @@ def find_best_match(font_map, family, bold, italic):
# Let CoreText choose the font if the family exists, otherwise
# fallback to Menlo
if q not in font_map['family_map']:
safe_print('The font {} was not found, falling back to Menlo'.format(family), file=sys.stderr)
safe_print(
'The font {} was not found, falling back to Menlo'.format(family),
file=sys.stderr
)
family = 'Menlo'
return {
'monospace': True,
@ -55,53 +68,65 @@ def find_best_match(font_map, family, bold, italic):
}
def get_face(font_map, family, main_family, font_size, dpi, bold=False, italic=False):
def resolve_family(f):
if (bold or italic) and f == 'auto':
f = main_family
if f.lower() == 'monospace':
f = 'Menlo'
return f
descriptor = find_best_match(font_map, resolve_family(family), bold, italic)
def resolve_family(f, main_family, bold, italic):
if (bold or italic) and f == 'auto':
f = main_family
if f.lower() == 'monospace':
f = 'Menlo'
return f
FaceDescription = namedtuple(
'FaceDescription', 'resolved_family family bold italic'
)
def face_description(family, main_family, bold=False, italic=False):
return FaceDescription(
resolve_family(family, main_family, bold, italic), family, bold, italic
)
def get_face(family, font_size, dpi, bold=False, italic=False):
descriptor = find_best_match(family, bold, italic)
return Face(descriptor, font_size, dpi)
def install_symbol_map(all_fonts, val, font_size, dpi):
global symbol_map
symbol_map = {}
family_map = {f: get_face(all_fonts, f, 'Menlo', font_size, dpi) for f in set(val.values())}
for ch, family in val.items():
symbol_map[ch] = family_map[family]
def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False):
global cell_width, cell_height, baseline, CellTexture, WideCellTexture, underline_thickness, underline_position
try:
dpi = get_logical_dpi()
except Exception:
if not ignore_dpi_failure:
raise
dpi = (72, 72) # Happens when running via develop() in an ssh session
dpi = sum(dpi) / 2.0
font_size = override_font_size or opts.font_size
all_fonts = create_font_map(coretext_all_fonts())
def get_font_files(opts):
ans = {}
for (bold, italic), attr in attr_map.items():
main_font[(bold, italic)] = get_face(all_fonts, getattr(opts, attr), opts.font_family, font_size, dpi, bold, italic)
install_symbol_map(all_fonts, opts.symbol_map, font_size, dpi)
mf = main_font[(False, False)]
cell_width, cell_height = mf.cell_size()
cell_height = adjust_line_height(cell_height, opts.adjust_line_height)
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(baseline - mf.underline_position))
underline_thickness = ceil_int(mf.underline_thickness)
return cell_width, cell_height
face = face_description(
getattr(opts, attr), opts.font_family, bold, italic
)
key = {(False, False): 'medium',
(True, False): 'bold',
(False, True): 'italic',
(True, True): 'bi'}[(bold, italic)]
ans[key] = face
if key == 'medium':
save_medium_face.family = face.resolved_family
return ans
def test_font_matching(name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0):
def face_from_font(font, pt_sz, xdpi, ydpi):
return get_face(font.resolved_family, pt_sz, (xdpi + ydpi) / 2, bold=font.bold, italic=font.italic)
def save_medium_face(face, family):
save_medium_face.face = face
def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False):
save_medium_face.face.face_for_text(text, bold, italic)
def font_for_family(family):
return face_description(family, save_medium_face.family)
def test_font_matching(
name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0
):
all_fonts = create_font_map(coretext_all_fonts())
face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic)
return face
@ -111,55 +136,7 @@ def test_family_matching(name='Menlo', dpi=72.0, font_size=11.0):
all_fonts = create_font_map(coretext_all_fonts())
for bold in (False, True):
for italic in (False, True):
face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic)
face = get_face(
all_fonts, name, 'Menlo', font_size, dpi, bold, italic
)
print(bold, italic, face)
def current_cell():
return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position
def split(buf, cell_width, cell_height):
first, second = CellTexture(), CellTexture()
for y in range(cell_height):
offset, woffset = y * cell_width, y * cell_width * 2
for x in range(cell_width):
first[offset + x] = buf[woffset + x]
second[offset + x] = buf[woffset + cell_width + x]
return first, second
def render_cell(text=' ', bold=False, italic=False):
ch = text[0]
width = wcwidth(ch)
face = symbol_map.get(ch) or 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))
if width == 2:
first, second = split(buf, cell_width, cell_height)
else:
first, second = buf, None
return first, second
def develop(family='monospace', sz=288):
import pickle
from .render import render_string
from kitty.fast_data_types import glfw_init
from kitty.config import defaults
import os
glfw_init()
try:
os.remove('/tmp/cell.data')
except EnvironmentError:
pass
opts = defaults._replace(font_family=family, font_size=sz)
set_font_family(opts, ignore_dpi_failure=True)
for (bold, italic), face in main_font.items():
print('bold: {} italic: {} {}'.format(bold, italic, face))
print('cell_width: {}, cell_height: {}, baseline: {}'.format(cell_width, cell_height, baseline))
buf, w, h = render_string()
open('/tmp/cell.data', 'wb').write(pickle.dumps((bytearray(buf), w, h)))

View File

@ -11,10 +11,13 @@ from kitty.utils import get_logical_dpi
from kitty.fast_data_types import set_font, set_font_size
from .box_drawing import render_box_char, is_renderable_box_char
if isosx:
pass
from .core_text import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face
else:
from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family
def save_medium_face(f):
pass
def create_face(font):
s = set_font_family.state
@ -50,6 +53,7 @@ def set_font_family(opts, override_font_size=None):
set_font_family.state = FontState('', sz, xdpi, ydpi, 0, 0, 0, 0, 0)
font_map = get_font_files(opts)
faces = [create_face(font_map['medium'])]
save_medium_face(faces[0])
for k in 'bold italic bi'.split():
if k in font_map:
faces.append(create_face(font_map[k]))
@ -103,11 +107,15 @@ def add_curl(buf, cell_width, position, thickness, cell_height):
def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False):
CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position = current_cell()
s = set_font_family.state
cell_width, cell_height, baseline = s.cell_width, s.cell_height, s.baseline
underline_thickness, underline_position = s.underline_thickness, s.underline_position
CharTexture = ctypes.c_ubyte * cell_width * cell_height
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)
first = CharTexture()
second = None
def dl(f, *a):
f(first, cell_width, *a)
@ -181,7 +189,8 @@ def render_string(text='\'Qing👁a⧽', underline=2, strikethrough=True):
current_text = c
if current_text:
render_one(current_text)
cell_width, cell_height = current_cell()[1:3]
s = set_font_family.state
cell_width, cell_height = s.cell_width, s.cell_height
char_data = join_cells(cell_width, cell_height, *cells)
return char_data, cell_width * len(cells), cell_height