From f553e4e09cd2baf15fc041daab8a08bd46e352f0 Mon Sep 17 00:00:00 2001 From: herb Date: Mon, 9 Nov 2015 08:51:56 -0800 Subject: [PATCH] Extract the glyph picking and placing code. There is a common piece of code which finds and positions glyphs and is used in four places. Some places copied the code, some places added callbacks. Here is a list of code: SkDraw::drawPosText GrAtlasTextContext::internalDrawBMPPosText GrAtlasTextContext::internalDrawDFPosText SkXPSDevice::drawPosText This only extracts the code from SkDraw::drawPosText. I would like to use it in the other three places. I think this code is performance neutral. BUG=skia: Review URL: https://codereview.chromium.org/1420973005 --- src/core/SkDraw.cpp | 600 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 500 insertions(+), 100 deletions(-) diff --git a/src/core/SkDraw.cpp b/src/core/SkDraw.cpp index d8d6fcf..f884d3b 100644 --- a/src/core/SkDraw.cpp +++ b/src/core/SkDraw.cpp @@ -14,6 +14,7 @@ #include "SkDeviceLooper.h" #include "SkFixed.h" #include "SkMaskFilter.h" +#include "SkMatrix.h" #include "SkPaint.h" #include "SkPathEffect.h" #include "SkRasterClip.h" @@ -25,8 +26,10 @@ #include "SkString.h" #include "SkStroke.h" #include "SkStrokeRec.h" +#include "SkTemplates.h" #include "SkTextMapStateProc.h" #include "SkTLazy.h" +#include "SkUtility.h" #include "SkUtils.h" #include "SkVertState.h" @@ -1439,7 +1442,7 @@ void SkDraw::drawText_asPaths(const char text[], size_t byteLength, #pragma warning ( disable : 4701 ) #endif -////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// static void D1G_RectClip(const SkDraw1Glyph& state, Sk48Dot16 fx, Sk48Dot16 fy, const SkGlyph& glyph) { // Prevent glyphs from being drawn outside of or straddling the edge of device space. @@ -1726,6 +1729,491 @@ void SkDraw::drawPosText_asPaths(const char text[], size_t byteLength, } } +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Calculate a type with the same size as the max of all the Ts. +template struct MaxSizeOf; + +template <> struct MaxSizeOf<> { static const size_t value = 0; }; + +template struct MaxSizeOf { + static const size_t value = + sizeof(H) >= MaxSizeOf::value ? sizeof(H) : MaxSizeOf::value; +}; + +// UntaggedVariant is a pile of memory that can hold one of the Ts. It provides a way +// to initialize that memory in a typesafe way. +template +class UntaggedVariant { +public: + UntaggedVariant() {} + ~UntaggedVariant() {} + UntaggedVariant(const UntaggedVariant&) = delete; + UntaggedVariant& operator=(const UntaggedVariant&) = delete; + UntaggedVariant(UntaggedVariant&&) = delete; + UntaggedVariant& operator=(UntaggedVariant&&) = delete; + + template + void initialize(Args &&... args) { + SkASSERT(sizeof(Variant) <= sizeof(fSpace)); + #if defined(_MSC_VER) && _MSC_VER < 1900 + #define alignof __alignof + #endif + SkASSERT(alignof(Variant) <= alignof(Space)); + new (&fSpace) Variant(skstd::forward(args)...); + } + +private: + typedef SkAlignedSStorage::value> Space; + Space fSpace; +}; + +// PolymorphicVariant holds subclasses of Base without slicing. Ts must be subclasses of Base. +template +class PolymorphicVariant { +public: + typedef UntaggedVariant Variants; + template + PolymorphicVariant(Initializer&& initializer) { + initializer(&fVariants); + } + ~PolymorphicVariant() { get()->~Base(); } + Base* get() const { return reinterpret_cast(&fVariants); } + Base* operator->() const { return get(); } + Base& operator*() const { return *get(); } + +private: + mutable Variants fVariants; +}; + +// PositionReaderInterface reads a point from the pos vector. +// * HorizontalPositions - assumes a common Y for many X values. +// * ArbitraryPositions - a list of (X,Y) pairs. +class PositionReaderInterface : SkNoncopyable { +public: + virtual ~PositionReaderInterface() { } + virtual SkPoint nextPoint() = 0; +}; + +class HorizontalPositions final : public PositionReaderInterface { +public: + HorizontalPositions(const SkScalar* positions) + : fPositions(positions) { } + + SkPoint nextPoint() override { + SkScalar x = *fPositions++; + return {x, 0}; + } +private: + const SkScalar* fPositions; +}; + +class ArbitraryPositions final : public PositionReaderInterface { +public: + ArbitraryPositions(const SkScalar* positions) + : fPositions(positions) { } + SkPoint nextPoint() override { + SkPoint to_return {fPositions[0], fPositions[1]}; + fPositions += 2; + return to_return; + } + +private: + const SkScalar* fPositions; +}; + +typedef PolymorphicVariant + PositionReader; + +// MapperInterface given a point map it through the matrix. There are several shortcut variants. +// * TranslationMapper - assumes a translation only matrix. +// * XScaleMapper - assumes an X scaling and a translation. +// * GeneralMapper - Does all other matricies. +class MapperInterface : SkNoncopyable { +public: + virtual ~MapperInterface() {} + virtual SkPoint map(SkPoint position) const = 0; +}; + +class TranslationMapper final : public MapperInterface { +public: + TranslationMapper(const SkMatrix& matrix, const SkPoint origin) + : fTranslate(matrix.mapXY(origin.fX, origin.fY)) { } + SkPoint map(SkPoint position) const override { + return position + fTranslate; + } + +private: + const SkPoint fTranslate; +}; + +class XScaleMapper final : public MapperInterface { +public: + XScaleMapper(const SkMatrix& matrix, const SkPoint origin) + : fTranslate(matrix.mapXY(origin.fX, origin.fY)) + , fXScale(matrix.getScaleX()) { } + SkPoint map(SkPoint position) const override { + return {fXScale * position.fX + fTranslate.fX, fTranslate.fY}; + } + +private: + const SkPoint fTranslate; + const SkScalar fXScale; +}; + +// The caller must keep matrix alive while this class is used. +class GeneralMapper final : public MapperInterface { +public: + GeneralMapper(const SkMatrix& matrix, const SkPoint origin) + : fOrigin(origin) + , fMatrix(matrix) + , fMapProc(matrix.getMapXYProc()) { } + SkPoint map(SkPoint position) const override { + SkPoint result; + fMapProc(fMatrix, position.fX + fOrigin.fX, position.fY + fOrigin.fY, &result); + return result; + } + +private: + const SkPoint fOrigin; + const SkMatrix& fMatrix; + const SkMatrix::MapXYProc fMapProc; +}; + +typedef PolymorphicVariant Mapper; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Text alignment handles shifting the glyph based on its width. +static SkPoint text_alignment_adjustment(SkPaint::Align textAlignment, const SkGlyph& glyph) { + switch (textAlignment) { + case SkPaint::kLeft_Align: + return {0.0f, 0.0f}; + break; + case SkPaint::kCenter_Align: + return {SkFixedToScalar(glyph.fAdvanceX >> 1), + SkFixedToScalar(glyph.fAdvanceY >> 1)}; + break; + case SkPaint::kRight_Align: + return {SkFixedToScalar(glyph.fAdvanceX), + SkFixedToScalar(glyph.fAdvanceY)}; + break; + } + // Even though the entire enum is covered above, MVSC doesn't think so. Make it happy. + SkFAIL("Should never get here."); + return {0.0f, 0.0f}; +} + +// The "call" to SkFixedToScalar is actually a macro. It's macros all the way down. +static const SkScalar kSubpixelRounding = SkFixedToScalar(SkGlyph::kSubpixelRound); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Functions for handling sub-pixel aligned positions. +// The subpixel_position_rounding function returns a point suitable for rounding a sub-pixel +// positioned glyph. +static SkPoint subpixel_position_rounding(SkAxisAlignment axisAlignment) { + switch (axisAlignment) { + case kX_SkAxisAlignment: + return {SkFixedToScalar(SkGlyph::kSubpixelRound), SK_ScalarHalf}; + case kY_SkAxisAlignment: + return {SK_ScalarHalf, kSubpixelRounding}; + case kNone_SkAxisAlignment: + return {kSubpixelRounding, kSubpixelRounding}; + } + SkFAIL("Should not get here."); + return {0.0f, 0.0f}; +} + +// The subpixel_position_alignment function produces a suitable position for the glyph cache to +// produce the correct sub-pixel alignment. If a position is aligned with an axis a shortcut of 0 +// is used for the sub-pixel position. +static SkIPoint subpixel_position_alignment(SkAxisAlignment axisAlignment, SkPoint position) { + switch (axisAlignment) { + case kX_SkAxisAlignment: + return {SkScalarToFixed(position.fX + kSubpixelRounding), 0}; + case kY_SkAxisAlignment: + return {0, SkScalarToFixed(position.fY + kSubpixelRounding)}; + case kNone_SkAxisAlignment: + return {SkScalarToFixed(position.fX + kSubpixelRounding), + SkScalarToFixed(position.fY + kSubpixelRounding)}; + } + SkFAIL("Should not get here."); + return {0, 0}; +} + +// GlyphFindAndPlaceInterface given the text and position finds the correct glyph and does glyph +// specific position adjustment. The findAndPositionGlyph method takes text and position and calls +// processOneGlyph with the correct glyph, final position and rounding terms. The final position +// is not rounded yet and is the responsibility of processOneGlyph. +template +class GlyphFindAndPlaceInterface : SkNoncopyable { +public: + struct Result { + const SkGlyph* fGlyph; + Sk48Dot16 fX; + Sk48Dot16 fY; + }; + virtual ~GlyphFindAndPlaceInterface() { }; + // This should be a pure virtual, but some versions of GCC <= 4.8 have a bug that causes a + // compile error. + // See GCC bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60277 + virtual void findAndPositionGlyph(const char** text, SkPoint position, + ProcessOneGlyph&& processOneGlyph) {}; +}; + +// GlyphFindAndPlaceForSubpixel handles finding and placing glyphs when sub-pixel positioning is +// requested. After it has found and placed the glyph it calls the templated function +// ProcessOneGlyph in order to actually perform an action. +template +class GlyphFindAndPlaceForSubpixel final : public GlyphFindAndPlaceInterface { +public: + GlyphFindAndPlaceForSubpixel(SkGlyphCache* cache, SkDrawCacheProc glyphCacheProc) + : fCache(cache) + , fGlyphCacheProc(glyphCacheProc) { + } + void findAndPositionGlyph(const char** text, SkPoint position, + ProcessOneGlyph&& processOneGlyph) override { + SkPoint finalPosition = position; + if (kTextAlignment != SkPaint::kLeft_Align) { + // Get the width of an un-sub-pixel positioned glyph for calculating the alignment. + // This is not needed for kLeftAlign because its adjustment is always {0, 0}. + const char* tempText = *text; + const SkGlyph& metricGlyph = fGlyphCacheProc(fCache, &tempText, 0, 0); + + if (metricGlyph.fWidth <= 0) { + return; + } + + // Adjust the final position by the alignment adjustment. + finalPosition -= text_alignment_adjustment(kTextAlignment, metricGlyph); + } + + // Find the glyph. + SkIPoint lookupPosition = subpixel_position_alignment(kAxisAlignment, finalPosition); + const SkGlyph& renderGlyph = fGlyphCacheProc( + fCache, text, lookupPosition.fX, lookupPosition.fY); + + // If the glyph has no width (no pixels) then don't bother processing it. + if (renderGlyph.fWidth > 0) { + processOneGlyph(renderGlyph, finalPosition, subpixel_position_rounding(kAxisAlignment)); + } + } + +private: + SkGlyphCache* const fCache; + SkDrawCacheProc fGlyphCacheProc; +}; + +// GlyphFindAndPlaceForFullPixel handles finding and placing glyphs when no sub-pixel positioning +// is requested. +template +class GlyphFindAndPlaceForFullPixel final : public GlyphFindAndPlaceInterface { +public: + GlyphFindAndPlaceForFullPixel(SkGlyphCache* cache, SkDrawCacheProc glyphCacheProc) + : fCache(cache) + , fGlyphCacheProc(glyphCacheProc) { } + void findAndPositionGlyph(const char** text, SkPoint position, + ProcessOneGlyph&& processOneGlyph) override { + SkPoint finalPosition = position; + const SkGlyph& glyph = fGlyphCacheProc(fCache, text, 0, 0); + if (glyph.fWidth <= 0) { + return; + } + finalPosition -= text_alignment_adjustment(kTextAlignment, glyph); + processOneGlyph(glyph, finalPosition, {SK_ScalarHalf, SK_ScalarHalf}); + } + +private: + SkGlyphCache* const fCache; + SkDrawCacheProc fGlyphCacheProc; +}; + +// GlyphFindAndPlace is a large variant that encapsulates the multiple types of finding and +// placing a glyph. There are three factors that go into the different factors. +// * Is sub-pixel positioned - a boolean that says whether to use sub-pixel positioning. +// * Text alignment - indicates if the glyph should be placed to the right, centered or left of a +// given position. +// * Axis alignment - indicates if the glyphs final sub-pixel position should be rounded to a +// whole pixel if the glyph is aligned with an axis. This is only used for sub-pixel positioning +// and allows the baseline to look crisp. +template +using GlyphFindAndPlace = + PolymorphicVariant< + GlyphFindAndPlaceInterface, + // Subpixel + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + GlyphFindAndPlaceForSubpixel, + // Full pixel + GlyphFindAndPlaceForFullPixel, + GlyphFindAndPlaceForFullPixel, + GlyphFindAndPlaceForFullPixel + >; + +// init_subpixel is a helper function for initializing all the variants of +// GlyphFindAndPlaceForSubpixel. +template +static void init_subpixel( + typename GlyphFindAndPlace::Variants* to_init, + SkAxisAlignment axisAlignment, + SkGlyphCache* cache, + SkDrawCacheProc glyphCacheProc) { + switch (axisAlignment) { + case kX_SkAxisAlignment: + to_init->template initialize>( + cache, glyphCacheProc); + break; + case kNone_SkAxisAlignment: + to_init->template initialize>( + cache, glyphCacheProc); + break; + case kY_SkAxisAlignment: + to_init->template initialize>( + cache, glyphCacheProc); + break; + } +} + +// specialized_process_pos_text is a version of ProcessPosText that de-virtualizes the different +// components used. It returns true if it can handle the situation, otherwise it returns false. +// This allows greater inlining freedom to the compiler. Currently, there is only one specialized +// variant: sub-pixel position, left-aligned, x-axis-aligned, translation, and one scalar per +// position entry. +// * This is by far the most common type of text Blink draws. +template +static bool specialized_process_pos_text(const char* const text, size_t byteLength, + const SkPoint& offset, const SkMatrix& matrix, + const SkScalar pos[], int scalarsPerPosition, + SkPaint::Align textAlignment, + SkDrawCacheProc& glyphCacheProc, + SkGlyphCache* cache, ProcessOneGlyph&& processOneGlyph) { + SkAxisAlignment axisAlignment = SkComputeAxisAlignmentForHText(matrix); + uint32_t mtype = matrix.getType(); + if (scalarsPerPosition == 1 + && textAlignment == SkPaint::kLeft_Align + && axisAlignment == kX_SkAxisAlignment + && cache->isSubpixel() + && mtype <= SkMatrix::kTranslate_Mask) { + typedef GlyphFindAndPlaceForSubpixel< + ProcessOneGlyph, SkPaint::kLeft_Align, kX_SkAxisAlignment> Positioner; + HorizontalPositions positions{pos}; + TranslationMapper mapper{matrix, offset}; + Positioner positioner(cache, glyphCacheProc); + const char *cursor = text; + const char *stop = text + byteLength; + while (cursor < stop) { + SkPoint mappedPoint = mapper.TranslationMapper::map( + positions.HorizontalPositions::nextPoint()); + positioner.Positioner::findAndPositionGlyph( + &cursor, mappedPoint, skstd::forward(processOneGlyph)); + } + return true; + } + return false; +} + +// ProcessPosText handles all cases for finding and positioning glyphs. It has a very large +// multiplicity. It figures out the glyph, position and rounding and pass those parameters to +// processOneGlyph. +// +// The routine processOneGlyph passed in by the client has the following signature: +// void f(const SkGlyph& glyph, SkPoint position, SkPoint rounding); +// +// * Sub-pixel positioning (2) - use sub-pixel positioning. +// * Text alignment (3) - text alignment with respect to the glyph's width. +// * Matrix type (3) - special cases for translation and X-coordinate scaling. +// * Components per position (2) - the positions vector can have a common Y with different Xs, or +// XY-pairs. +// * Axis Alignment (for sub-pixel positioning) (3) - when using sub-pixel positioning, round to +// a whole coordinate instead of using sub-pixel positioning. +// The number of variations is 108 for sub-pixel and 36 for full-pixel. +// This routine handles all of them using inline polymorphic variable (no heap allocation). +template +static void process_pos_text(const char text[], size_t byteLength, + const SkPoint& offset, const SkMatrix& matrix, + const SkScalar pos[], int scalarsPerPosition, + SkPaint::Align textAlignment, SkDrawCacheProc& glyphCacheProc, + SkGlyphCache* cache, ProcessOneGlyph&& processOneGlyph) { + + PositionReader positionReader { + [&](PositionReader::Variants* to_init) { + if (2 == scalarsPerPosition) { + to_init->initialize(pos); + } else { + to_init->initialize(pos); + } + } + }; + + Mapper mapper { + [&] (Mapper::Variants* to_init) { + uint32_t mtype = matrix.getType(); + if (mtype & (SkMatrix::kAffine_Mask | SkMatrix::kPerspective_Mask) + || scalarsPerPosition == 2) { + to_init->initialize(matrix, offset); + } else if (mtype & SkMatrix::kScale_Mask) { + to_init->initialize(matrix, offset); + } else { + to_init->initialize(matrix, offset); + } + } + }; + + GlyphFindAndPlace findAndPosition{ + [&](typename GlyphFindAndPlace::Variants* to_init) { + if (cache->isSubpixel()) { + SkAxisAlignment axisAlignment = SkComputeAxisAlignmentForHText(matrix); + switch (textAlignment) { + case SkPaint::kLeft_Align: + init_subpixel( + to_init, axisAlignment, cache, glyphCacheProc); + break; + case SkPaint::kCenter_Align: + init_subpixel( + to_init, axisAlignment, cache, glyphCacheProc); + break; + case SkPaint::kRight_Align: + init_subpixel( + to_init, axisAlignment, cache, glyphCacheProc); + break; + } + } else { + switch (textAlignment) { + case SkPaint::kLeft_Align: + to_init->template initialize>( + cache, glyphCacheProc); + break; + case SkPaint::kCenter_Align: + to_init->template initialize>( + cache, glyphCacheProc); + break; + case SkPaint::kRight_Align: + to_init->template initialize>( + cache, glyphCacheProc); + break; + } + } + } + }; + + const char* stop = text + byteLength; + while (text < stop) { + SkPoint mappedPoint = mapper->map(positionReader->nextPoint()); + findAndPosition->findAndPositionGlyph( + &text, mappedPoint, skstd::forward(processOneGlyph)); + } +} + void SkDraw::drawPosText(const char text[], size_t byteLength, const SkScalar pos[], int scalarsPerPosition, const SkPoint& offset, const SkPaint& paint) const { @@ -1760,108 +2248,20 @@ void SkDraw::drawPosText(const char text[], size_t byteLength, } } - const char* stop = text + byteLength; - SkTextAlignProc alignProc(paint.getTextAlign()); SkDraw1Glyph d1g; SkDraw1Glyph::Proc proc = d1g.init(this, blitter, cache, paint); - SkTextMapStateProc tmsProc(*fMatrix, offset, scalarsPerPosition); - - if (cache->isSubpixel()) { - // maybe we should skip the rounding if linearText is set - SkAxisAlignment baseline = SkComputeAxisAlignmentForHText(*fMatrix); - - SkFixed fxMask = ~0; - SkFixed fyMask = ~0; - if (kX_SkAxisAlignment == baseline) { - fyMask = 0; - d1g.fHalfSampleY = SK_ScalarHalf; - } else if (kY_SkAxisAlignment == baseline) { - fxMask = 0; - d1g.fHalfSampleX = SK_ScalarHalf; - } - if (SkPaint::kLeft_Align == paint.getTextAlign()) { - while (text < stop) { - SkPoint tmsLoc; - tmsProc(pos, &tmsLoc); - - Sk48Dot16 fx = SkScalarTo48Dot16(tmsLoc.fX + d1g.fHalfSampleX); - Sk48Dot16 fy = SkScalarTo48Dot16(tmsLoc.fY + d1g.fHalfSampleY); - - const SkGlyph& glyph = glyphCacheProc(cache, &text, fx & fxMask, fy & fyMask); - - if (glyph.fWidth) { - proc(d1g, fx, fy, glyph); - } - pos += scalarsPerPosition; - } - } else { - while (text < stop) { - const char* currentText = text; - const SkGlyph& metricGlyph = glyphCacheProc(cache, &text, 0, 0); - - if (metricGlyph.fWidth) { - SkDEBUGCODE(SkFixed prevAdvX = metricGlyph.fAdvanceX;) - SkDEBUGCODE(SkFixed prevAdvY = metricGlyph.fAdvanceY;) - SkPoint tmsLoc; - tmsProc(pos, &tmsLoc); - - SkPoint alignLoc; - alignProc(tmsLoc, metricGlyph, &alignLoc); - - Sk48Dot16 fx = SkScalarTo48Dot16(alignLoc.fX + d1g.fHalfSampleX); - Sk48Dot16 fy = SkScalarTo48Dot16(alignLoc.fY + d1g.fHalfSampleY); - - // have to call again, now that we've been "aligned" - const SkGlyph& glyph = glyphCacheProc(cache, ¤tText, - fx & fxMask, fy & fyMask); - // the assumption is that the metrics haven't changed - SkASSERT(prevAdvX == glyph.fAdvanceX); - SkASSERT(prevAdvY == glyph.fAdvanceY); - SkASSERT(glyph.fWidth); - - proc(d1g, fx, fy, glyph); - } - pos += scalarsPerPosition; - } - } - } else { // not subpixel - if (SkPaint::kLeft_Align == paint.getTextAlign()) { - while (text < stop) { - // the last 2 parameters are ignored - const SkGlyph& glyph = glyphCacheProc(cache, &text, 0, 0); - - if (glyph.fWidth) { - SkPoint tmsLoc; - tmsProc(pos, &tmsLoc); - - proc(d1g, - SkScalarTo48Dot16(tmsLoc.fX + SK_ScalarHalf), //d1g.fHalfSampleX, - SkScalarTo48Dot16(tmsLoc.fY + SK_ScalarHalf), //d1g.fHalfSampleY, - glyph); - } - pos += scalarsPerPosition; - } - } else { - while (text < stop) { - // the last 2 parameters are ignored - const SkGlyph& glyph = glyphCacheProc(cache, &text, 0, 0); - - if (glyph.fWidth) { - SkPoint tmsLoc; - tmsProc(pos, &tmsLoc); - - SkPoint alignLoc; - alignProc(tmsLoc, glyph, &alignLoc); - - proc(d1g, - SkScalarTo48Dot16(alignLoc.fX + SK_ScalarHalf), //d1g.fHalfSampleX, - SkScalarTo48Dot16(alignLoc.fY + SK_ScalarHalf), //d1g.fHalfSampleY, - glyph); - } - pos += scalarsPerPosition; - } - } + auto processOneGlyph = + [&](const SkGlyph& glyph, SkPoint position, SkPoint rounding) { + position += rounding; + proc(d1g, SkScalarToFixed(position.fX), SkScalarToFixed(position.fY), glyph); + }; + + SkPaint::Align textAlignment = paint.getTextAlign(); + if (!specialized_process_pos_text(text, byteLength, offset, *fMatrix, pos, scalarsPerPosition, + textAlignment, glyphCacheProc, cache, processOneGlyph)) { + process_pos_text(text, byteLength, offset, *fMatrix, pos, scalarsPerPosition, + textAlignment, glyphCacheProc, cache, processOneGlyph); } } -- 2.7.4