text: use HarfBuzz for text shaping; support right-to-left text

This commit is contained in:
rdb 2017-04-17 12:39:02 +02:00
parent 353ccfebc1
commit 160f652d58
12 changed files with 325 additions and 28 deletions

View File

@ -35,7 +35,8 @@ class OnscreenText(NodePath):
font = None,
parent = None,
sort = 0,
mayChange = True):
mayChange = True,
direction = None):
"""
Make a text node from string, put it into the 2d sg and set it
up with all the indicated parameters.
@ -95,6 +96,9 @@ class OnscreenText(NodePath):
mayChange: pass true if the text or its properties may need
to be changed at runtime, false if it is static once
created (which leads to better memory optimization).
direction: this can be set to 'ltr' or 'rtl' to override the
direction of the text.
"""
if parent == None:
parent = aspect2d
@ -192,6 +196,17 @@ class OnscreenText(NodePath):
textNode.setFrameColor(frame[0], frame[1], frame[2], frame[3])
textNode.setFrameAsMargin(0.1, 0.1, 0.1, 0.1)
if direction is not None:
if isinstance(direction, str):
direction = direction.lower()
if direction == 'rtl':
direction = TextProperties.D_rtl
elif direction == 'ltr':
direction = TextProperties.D_ltr
else:
raise ValueError('invalid direction')
textNode.setDirection(direction)
# Create a transform for the text for our scale and position.
# We'd rather do it here, on the text itself, rather than on
# our NodePath, so we have one fewer transforms in the scene

View File

@ -29,6 +29,7 @@ class FT_Library;
class FT_Bitmap;
class FT_Vector;
class FT_Span;
class FT_Outline;
#endif

View File

@ -80,8 +80,9 @@ PkgListSet(["PYTHON", "DIRECT", # Python support
"VORBIS", "FFMPEG", "SWSCALE", "SWRESAMPLE", # Audio decoding
"ODE", "PHYSX", "BULLET", "PANDAPHYSICS", # Physics
"SPEEDTREE", # SpeedTree
"ZLIB", "PNG", "JPEG", "TIFF", "OPENEXR", "SQUISH", "FREETYPE", # 2D Formats support
"ZLIB", "PNG", "JPEG", "TIFF", "OPENEXR", "SQUISH", # 2D Formats support
] + MAYAVERSIONS + MAXVERSIONS + [ "FCOLLADA", "ASSIMP", "EGG", # 3D Formats support
"FREETYPE", "HARFBUZZ", # Text rendering
"VRPN", "OPENSSL", # Transport
"FFTW", # Algorithm helpers
"ARTOOLKIT", "OPENCV", "DIRECTCAM", "VISION", # Augmented Reality
@ -638,6 +639,7 @@ if (COMPILER == "MSVC"):
if (PkgSkip("NVIDIACG")==0): LibName("CGDX9", GetThirdpartyDir() + "nvidiacg/lib/cgD3D9.lib")
if (PkgSkip("NVIDIACG")==0): LibName("NVIDIACG", GetThirdpartyDir() + "nvidiacg/lib/cg.lib")
if (PkgSkip("FREETYPE")==0): LibName("FREETYPE", GetThirdpartyDir() + "freetype/lib/freetype.lib")
if (PkgSkip("HARFBUZZ")==0): LibName("HARFBUZZ", GetThirdpartyDir() + "harfbuzz/lib/harfbuzz.lib")
if (PkgSkip("FFTW")==0): LibName("FFTW", GetThirdpartyDir() + "fftw/lib/rfftw.lib")
if (PkgSkip("FFTW")==0): LibName("FFTW", GetThirdpartyDir() + "fftw/lib/fftw.lib")
if (PkgSkip("ARTOOLKIT")==0):LibName("ARTOOLKIT",GetThirdpartyDir() + "artoolkit/lib/libAR.lib")
@ -803,6 +805,7 @@ if (COMPILER=="GCC"):
SmartPkgEnable("FFTW", "", ("rfftw", "fftw"), ("fftw.h", "rfftw.h"))
SmartPkgEnable("FMODEX", "", ("fmodex"), ("fmodex", "fmodex/fmod.h"))
SmartPkgEnable("FREETYPE", "freetype2", ("freetype"), ("freetype2", "freetype2/freetype/freetype.h"))
SmartPkgEnable("HARFBUZZ", "harfbuzz", ("harfbuzz"), ("harfbuzz", "harfbuzz/hb-ft.h"))
SmartPkgEnable("GL", "gl", ("GL"), ("GL/gl.h"), framework = "OpenGL")
SmartPkgEnable("GLES", "glesv1_cm", ("GLESv1_CM"), ("GLES/gl.h"), framework = "OpenGLES")
SmartPkgEnable("GLES2", "glesv2", ("GLESv2"), ("GLES2/gl2.h")) #framework = "OpenGLES"?
@ -3806,7 +3809,9 @@ if (PkgSkip("FREETYPE")==0 and not RUNTIME):
#
if (not RUNTIME):
OPTS=['DIR:panda/src/text', 'BUILDING:PANDA', 'ZLIB', 'FREETYPE']
DefSymbol("HARFBUZZ", "HAVE_HARFBUZZ")
OPTS=['DIR:panda/src/text', 'BUILDING:PANDA', 'ZLIB', 'FREETYPE', 'HARFBUZZ']
TargetAdd('p3text_composite1.obj', opts=OPTS, input='p3text_composite1.cxx')
TargetAdd('p3text_composite2.obj', opts=OPTS, input='p3text_composite2.cxx')
@ -3956,7 +3961,7 @@ if (not RUNTIME):
#
if (not RUNTIME):
OPTS=['DIR:panda/metalibs/panda', 'BUILDING:PANDA', 'JPEG', 'PNG',
OPTS=['DIR:panda/metalibs/panda', 'BUILDING:PANDA', 'JPEG', 'PNG', 'HARFBUZZ',
'TIFF', 'OPENEXR', 'ZLIB', 'OPENSSL', 'FREETYPE', 'FFTW', 'ADVAPI', 'WINSOCK2',
'SQUISH', 'NVIDIACG', 'VORBIS', 'WINUSER', 'WINMM', 'WINGDI', 'IPHLPAPI']

View File

@ -52,7 +52,13 @@ ConfigVariableBool text_kerning
("text-kerning", false,
PRC_DESC("Set this true to enable kerning when the font provides kerning "
"tables. This can result in more aesthetically pleasing spacing "
"between individual glyphs."));
"between individual glyphs. Has no effect when text-use-harfbuzz "
"is true, since HarfBuzz offers superior kerning support."));
ConfigVariableBool text_use_harfbuzz
("text-use-harfbuzz", false,
PRC_DESC("Set this true to enable HarfBuzz support, which offers superior "
"text shaping and better support for non-Latin text."));
ConfigVariableInt text_anisotropic_degree
("text-anisotropic-degree", 1,

View File

@ -31,6 +31,7 @@ NotifyCategoryDecl(text, EXPCL_PANDA_TEXT, EXPTP_PANDA_TEXT);
extern ConfigVariableBool text_flatten;
extern ConfigVariableBool text_dynamic_merge;
extern ConfigVariableBool text_kerning;
extern ConfigVariableBool text_use_harfbuzz;
extern ConfigVariableInt text_anisotropic_degree;
extern ConfigVariableInt text_texture_margin;
extern ConfigVariableDouble text_poly_margin;

View File

@ -44,6 +44,10 @@
#include "textureAttrib.h"
#include "transparencyAttrib.h"
#ifdef HAVE_HARFBUZZ
#include <hb-ft.h>
#endif
TypeHandle DynamicTextFont::_type_handle;
@ -114,7 +118,8 @@ DynamicTextFont(const DynamicTextFont &copy) :
_has_outline(copy._has_outline),
_tex_format(copy._tex_format),
_needs_image_processing(copy._needs_image_processing),
_preferred_page(0)
_preferred_page(0),
_hb_font(nullptr)
{
}
@ -123,6 +128,11 @@ DynamicTextFont(const DynamicTextFont &copy) :
*/
DynamicTextFont::
~DynamicTextFont() {
#ifdef HAVE_HARFBUZZ
if (_hb_font != nullptr) {
hb_font_destroy(_hb_font);
}
#endif
}
/**
@ -203,6 +213,13 @@ clear() {
_cache.clear();
_pages.clear();
_empty_glyphs.clear();
#ifdef HAVE_HARFBUZZ
if (_hb_font != nullptr) {
hb_font_destroy(_hb_font);
_hb_font = nullptr;
}
#endif
}
/**
@ -305,6 +322,55 @@ get_kerning(int first, int second) const {
return delta.x / (_font_pixels_per_unit * 64);
}
/**
* Like get_glyph, but uses a glyph index.
*/
bool DynamicTextFont::
get_glyph_by_index(int character, int glyph_index, CPT(TextGlyph) &glyph) {
if (!_is_valid) {
glyph = nullptr;
return false;
}
Cache::iterator ci = _cache.find(glyph_index);
if (ci != _cache.end()) {
glyph = (*ci).second;
} else {
FT_Face face = acquire_face();
glyph = make_glyph(character, face, glyph_index);
_cache.insert(Cache::value_type(glyph_index, glyph.p()));
release_face(face);
}
if (glyph.is_null()) {
glyph = get_invalid_glyph();
return false;
}
return true;
}
/**
* If Panda was compiled with HarfBuzz enabled, returns a HarfBuzz font for
* this font.
*/
hb_font_t *DynamicTextFont::
get_hb_font() const {
#ifdef HAVE_HARFBUZZ
if (_hb_font != nullptr) {
return _hb_font;
}
FT_Face face = acquire_face();
_hb_font = hb_ft_font_create(face, nullptr);
release_face(face);
return _hb_font;
#else
return nullptr;
#endif
}
/**
* Called from both constructors to set up some initial values.
*/
@ -328,6 +394,8 @@ initialize() {
_winding_order = WO_default;
_preferred_page = 0;
_hb_font = nullptr;
}
/**

View File

@ -31,6 +31,8 @@
class NurbsCurveResult;
typedef struct hb_font_t hb_font_t;
/**
* A DynamicTextFont is a special TextFont object that rasterizes its glyphs
* from a standard font file (e.g. a TTF file) on the fly. It requires the
@ -125,6 +127,9 @@ public:
virtual bool get_glyph(int character, CPT(TextGlyph) &glyph);
virtual PN_stdfloat get_kerning(int first, int second) const;
bool get_glyph_by_index(int character, int glyph_index, CPT(TextGlyph) &glyph);
hb_font_t *get_hb_font() const;
private:
void initialize();
void update_filters();
@ -171,6 +176,8 @@ private:
typedef pvector< PT(TextGlyph) > EmptyGlyphs;
EmptyGlyphs _empty_glyphs;
mutable hb_font_t *_hb_font;
public:
static TypeHandle get_class_type() {
return _type_handle;

View File

@ -31,10 +31,15 @@
#include "geomVertexData.h"
#include "geom.h"
#include "modelNode.h"
#include "dynamicTextFont.h"
#include <ctype.h>
#include <stdio.h> // for sprintf
#ifdef HAVE_HARFBUZZ
#include <hb.h>
#endif
// This is the factor by which CT_small scales the character down.
static const PN_stdfloat small_accent_scale = 0.6f;
@ -1408,6 +1413,12 @@ assemble_row(TextAssembler::TextRow &row,
PN_stdfloat underscore_start = 0.0f;
const TextProperties *underscore_properties = NULL;
const ComputedProperties *prev_cprops;
#ifdef HAVE_HARFBUZZ
hb_buffer_t *harfbuff = nullptr;
#endif
TextString::const_iterator si;
for (si = row._string.begin(); si != row._string.end(); ++si) {
const TextCharacter &tch = (*si);
@ -1448,10 +1459,29 @@ assemble_row(TextAssembler::TextRow &row,
LVecBase4 frame = graphic->get_frame();
line_height = max(line_height, frame[3] - frame[2]);
} else {
// [fabius] this is not the right place to calc line height (see below)
// line_height = max(line_height, font->get_line_height());
line_height = max(line_height, font->get_line_height() * properties->get_glyph_scale() * properties->get_text_scale());
}
#ifdef HAVE_HARFBUZZ
if (tch._cprops != prev_cprops || graphic != nullptr) {
if (harfbuff != nullptr && hb_buffer_get_length(harfbuff) > 0) {
// Shape the buffer accumulated so far.
shape_buffer(harfbuff, placed_glyphs, xpos, prev_cprops->_properties);
hb_buffer_reset(harfbuff);
} else if (harfbuff == nullptr && text_use_harfbuzz &&
font->is_of_type(DynamicTextFont::get_class_type())) {
harfbuff = hb_buffer_create();
}
prev_cprops = tch._cprops;
}
if (graphic == nullptr && harfbuff != nullptr) {
hb_buffer_add(harfbuff, character, character);
continue;
}
#endif
if (character == ' ') {
// A space is a special case.
xpos += properties->get_glyph_scale() * properties->get_text_scale() * font->get_space_advance();
@ -1613,10 +1643,16 @@ assemble_row(TextAssembler::TextRow &row,
}
xpos += advance * glyph_scale;
line_height = max(line_height, font->get_line_height() * glyph_scale);
}
}
#ifdef HAVE_HARFBUZZ
if (harfbuff != nullptr && hb_buffer_get_length(harfbuff) > 0) {
shape_buffer(harfbuff, placed_glyphs, xpos, prev_cprops->_properties);
}
hb_buffer_destroy(harfbuff);
#endif
if (underscore && underscore_start != xpos) {
draw_underscore(placed_glyphs, underscore_start, xpos,
underscore_properties);
@ -1640,6 +1676,88 @@ assemble_row(TextAssembler::TextRow &row,
}
}
/**
* Places the glyphs collected from a HarfBuzz buffer.
*/
void TextAssembler::
shape_buffer(hb_buffer_t *buf, PlacedGlyphs &placed_glyphs, PN_stdfloat &xpos,
const TextProperties &properties) {
#ifdef HAVE_HARFBUZZ
// If we did not specify a text direction, harfbuzz will guess it based on
// the script we are using.
hb_direction_t direction = HB_DIRECTION_INVALID;
if (properties.has_direction()) {
switch (properties.get_direction()) {
case TextProperties::D_ltr:
direction = HB_DIRECTION_LTR;
break;
case TextProperties::D_rtl:
direction = HB_DIRECTION_RTL;
break;
}
}
hb_buffer_set_direction(buf, direction);
hb_buffer_guess_segment_properties(buf);
DynamicTextFont *font = DCAST(DynamicTextFont, properties.get_font());
hb_font_t *hb_font = font->get_hb_font();
hb_shape(hb_font, buf, NULL, 0);
PN_stdfloat glyph_scale = properties.get_glyph_scale() * properties.get_text_scale();
PN_stdfloat scale = glyph_scale / (font->get_pixels_per_unit() * font->get_scale_factor() * 64.0);
unsigned int glyph_count;
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count);
for (unsigned int i = 0; i < glyph_count; ++i) {
int character = glyph_info[i].cluster;
int glyph_index = glyph_info[i].codepoint;
CPT(TextGlyph) glyph;
if (!font->get_glyph_by_index(character, glyph_index, glyph)) {
char buffer[512];
sprintf(buffer, "U+%04x", character);
text_cat.warning()
<< "No definition in " << font->get_name()
<< " for character " << buffer;
if (character < 128 && isprint((unsigned int)character)) {
text_cat.warning(false)
<< " ('" << (char)character << "')";
}
text_cat.warning(false)
<< "\n";
}
PN_stdfloat advance = glyph_pos[i].x_advance * scale;
if (glyph->is_whitespace()) {
// A space is a special case.
xpos += advance;
continue;
}
PN_stdfloat x_offset = glyph_pos[i].x_offset * scale;
PN_stdfloat y_offset = glyph_pos[i].y_offset * scale;
// Build up a GlyphPlacement, indicating all of the Geoms that go into
// this character. Normally, there is only one Geom per character, but
// it may involve multiple Geoms if we need to add cheesy accents or
// ligatures.
GlyphPlacement placement;
placement._glyph = move(glyph);
placement._scale = glyph_scale;
placement._xpos = xpos + x_offset;
placement._ypos = properties.get_glyph_shift() + y_offset;
placement._slant = properties.get_slant();
placement._properties = &properties;
placed_glyphs.push_back(placement);
xpos += advance;
}
#endif
}
/**
* Creates the geometry to render the underscore line for the indicated range
* of glyphs in this row.

View File

@ -28,6 +28,7 @@
#include "pmap.h"
typedef struct hb_buffer_t hb_buffer_t;
class TextEncoder;
class TextGraphic;
@ -247,6 +248,9 @@ private:
PN_stdfloat &row_width, PN_stdfloat &line_height,
TextProperties::Alignment &align, PN_stdfloat &wordwrap);
void shape_buffer(hb_buffer_t *buf, PlacedGlyphs &glyphs, PN_stdfloat &xpos,
const TextProperties &properties);
// These interfaces are for implementing cheesy accent marks and ligatures
// when the font doesn't support them.
enum CheesyPosition {

View File

@ -797,3 +797,39 @@ INLINE PN_stdfloat TextProperties::
get_text_scale() const {
return _text_scale;
}
/**
* Specifies the text direction. If none is specified, it will be guessed
* based on the contents of the string.
*/
INLINE void TextProperties::
set_direction(Direction direction) {
_direction = direction;
_specified |= F_has_direction;
}
/**
* Clears the text direction setting. If no text direction is specified, it
* will be guessed based on the contents of the string.
*/
INLINE void TextProperties::
clear_direction() {
_specified &= ~F_has_direction;
_direction = D_ltr;
}
/**
*
*/
INLINE bool TextProperties::
has_direction() const {
return (_specified & F_has_direction) != 0;
}
/**
* Returns the direction of the text as specified by set_direction().
*/
INLINE TextProperties::Direction TextProperties::
get_direction() const {
return _direction;
}

View File

@ -31,26 +31,27 @@ TypeHandle TextProperties::_type_handle;
*
*/
TextProperties::
TextProperties() {
_specified = 0;
TextProperties() :
_specified(0),
_small_caps = text_small_caps;
_small_caps_scale = text_small_caps_scale;
_slant = 0.0f;
_underscore = false;
_underscore_height = 0.0f;
_align = A_left;
_indent_width = 0.0f;
_wordwrap_width = 0.0f;
_preserve_trailing_whitespace = false;
_text_color.set(1.0f, 1.0f, 1.0f, 1.0f);
_shadow_color.set(0.0f, 0.0f, 0.0f, 1.0f);
_shadow_offset.set(0.0f, 0.0f);
_draw_order = 1;
_tab_width = text_tab_width;
_glyph_scale = 1.0f;
_glyph_shift = 0.0f;
_text_scale = 1.0f;
_small_caps(text_small_caps),
_small_caps_scale(text_small_caps_scale),
_slant(0.0f),
_underscore(false),
_underscore_height(0.0f),
_align(A_left),
_indent_width(0.0f),
_wordwrap_width(0.0f),
_preserve_trailing_whitespace(false),
_text_color(1.0f, 1.0f, 1.0f, 1.0f),
_shadow_color(0.0f, 0.0f, 0.0f, 1.0f),
_shadow_offset(0.0f, 0.0f),
_draw_order(1),
_tab_width(text_tab_width),
_glyph_scale(1.0f),
_glyph_shift(0.0f),
_text_scale(1.0f),
_direction(D_rtl) {
}
/**
@ -89,6 +90,7 @@ operator = (const TextProperties &copy) {
_glyph_scale = copy._glyph_scale;
_glyph_shift = copy._glyph_shift;
_text_scale = copy._text_scale;
_direction = copy._direction;
_text_state.clear();
_shadow_state.clear();
@ -163,6 +165,9 @@ operator == (const TextProperties &other) const {
if ((_specified & F_has_text_scale) && _text_scale != other._text_scale) {
return false;
}
if ((_specified & F_has_direction) && _direction != other._direction) {
return false;
}
return true;
}
@ -238,6 +243,9 @@ add_properties(const TextProperties &other) {
if (other.has_text_scale()) {
set_text_scale(other.get_text_scale());
}
if (other.has_direction()) {
set_direction(other.get_direction());
}
}
@ -361,6 +369,20 @@ write(ostream &out, int indent_level) const {
indent(out, indent_level)
<< "text scale is " << get_text_scale() << "\n";
}
if (has_direction()) {
indent(out, indent_level)
<< "direction is ";
switch (get_direction()) {
case D_ltr:
out << "D_ltr\n";
break;
case D_rtl:
out << "D_rtl\n";
break;
}
}
}
/**

View File

@ -49,6 +49,11 @@ PUBLISHED:
A_boxed_center
};
enum Direction {
D_ltr,
D_rtl,
};
TextProperties();
TextProperties(const TextProperties &copy);
void operator = (const TextProperties &copy);
@ -160,6 +165,11 @@ PUBLISHED:
INLINE bool has_text_scale() const;
INLINE PN_stdfloat get_text_scale() const;
INLINE void set_direction(Direction direction);
INLINE void clear_direction();
INLINE bool has_direction() const;
INLINE Direction get_direction() const;
void add_properties(const TextProperties &other);
void write(ostream &out, int indent_level = 0) const;
@ -197,6 +207,8 @@ PUBLISHED:
set_glyph_shift, clear_glyph_shift);
MAKE_PROPERTY2(text_scale, has_text_scale, get_text_scale,
set_text_scale, clear_text_scale);
MAKE_PROPERTY2(direction, has_direction, get_direction,
set_direction, clear_direction);
public:
const RenderState *get_text_state() const;
@ -225,6 +237,7 @@ private:
F_has_underscore = 0x00010000,
F_has_underscore_height = 0x00020000,
F_has_text_scale = 0x00040000,
F_has_direction = 0x00080000,
};
int _specified;
@ -248,6 +261,7 @@ private:
PN_stdfloat _glyph_scale;
PN_stdfloat _glyph_shift;
PN_stdfloat _text_scale;
Direction _direction;
mutable CPT(RenderState) _text_state;
mutable CPT(RenderState) _shadow_state;