Subversion Repositories Scribus

Rev

Rev 22017 | Rev 22367 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

#include "textshaper.h"

#include <harfbuzz/hb.h>
#include <harfbuzz/hb-ft.h>
#include <harfbuzz/hb-icu.h>
#include <unicode/ubidi.h>

#include "scrptrun.h"

#include "glyphcluster.h"
#include "pageitem.h"
#include "scribusdoc.h"
#include "storytext.h"
#include "styles/paragraphstyle.h"
#include "util.h"


TextShaper::TextShaper(ITextContext* context, ITextSource &story, int firstChar, bool singlePar)
        : m_context(context),
        m_contextNeeded(false),
        m_story(story),
        m_firstChar(firstChar),
        m_singlePar(singlePar)
{ }

TextShaper::TextShaper(ITextSource &story, int firstChar)
        : m_context(NULL),
        m_contextNeeded(false),
        m_story(story),
        m_firstChar(firstChar),
        m_singlePar(false)
{
        for (int i = m_firstChar; i < m_story.length(); ++i)
        {
                QChar ch = m_story.text(i);
                if (ch == SpecialChars::PARSEP || ch == SpecialChars::LINEBREAK)
                        continue;
                QString str(ch);
                m_textMap.insert(i, i);
                m_text.append(str);
        }
}

QList<TextShaper::TextRun> TextShaper::itemizeBiDi()
{
        QList<TextRun> textRuns;
        UBiDi *obj = ubidi_open();
        UErrorCode err = U_ZERO_ERROR;

        UBiDiLevel parLevel = UBIDI_LTR;
        ParagraphStyle style = m_story.paragraphStyle(m_firstChar);
        if (style.direction() == ParagraphStyle::RTL)
                parLevel = UBIDI_RTL;

        ubidi_setPara(obj, (const UChar*) m_text.utf16(), m_text.length(), parLevel, NULL, &err);
        if (U_SUCCESS(err))
        {
                int32_t count = ubidi_countRuns(obj, &err);
                if (U_SUCCESS(err))
                {
                        textRuns.reserve(count);
                        for (int32_t i = 0; i < count; i++)
                        {
                                int32_t start, length;
                                UBiDiDirection dir = ubidi_getVisualRun(obj, i, &start, &length);
                                textRuns.append(TextRun(start, length, dir));
                        }
                }
        }

        ubidi_close(obj);
        return textRuns;
}

QList<TextShaper::TextRun> TextShaper::itemizeScripts(const QList<TextRun> &runs)
{
        QList<TextRun> newRuns;
        ScriptRun scriptrun((const UChar*) m_text.utf16(), m_text.length());

        foreach (TextRun run, runs)
        {
                int start = run.start;
                QList<TextRun> subRuns;

                while (scriptrun.next())
                {
                        if (scriptrun.getScriptStart() <= start && scriptrun.getScriptEnd() > start)
                                break;
                }

                while (start < run.start + run.len)
                {
                        int end = qMin(scriptrun.getScriptEnd(), run.start + run.len);
                        UScriptCode script = scriptrun.getScriptCode();
                        if (run.dir == UBIDI_RTL)
                                subRuns.prepend(TextRun(start, end - start, run.dir, script));
                        else
                                subRuns.append(TextRun(start, end - start, run.dir, script));

                        start = end;
                        scriptrun.next();
                }

                scriptrun.reset();
                newRuns.append(subRuns);
        }

        return newRuns;
}

QList<TextShaper::FeaturesRun> TextShaper::itemizeFeatures(const TextRun &run)
{
        QList<FeaturesRun> newRuns;
        QList<FeaturesRun> subfeature;
        int start = run.start;

        while (start < run.start + run.len)
        {
                int end = start;
                QStringList startFeatures = m_story.charStyle(m_textMap.value(start)).fontFeatures().split(",");
                while (end < run.start + run.len)
                {
                        QStringList endFeatures = m_story.charStyle(m_textMap.value(end)).fontFeatures().split(",");
                        if (startFeatures != endFeatures)
                                break;
                        end++;
                }
                subfeature.append(FeaturesRun(start, end - start, startFeatures));
                start = end;
                startFeatures.clear();
        }
        newRuns.append(subfeature);
        return newRuns;
}

QList<TextShaper::TextRun> TextShaper::itemizeStyles(const QList<TextRun> &runs)
{
        QList<TextRun> newRuns;

        foreach (TextRun run, runs) {
                int start = run.start;
                QList<TextRun> subRuns;

                while (start < run.start + run.len)
                {
                        int end = start;
                        const CharStyle &startStyle = m_story.charStyle(m_textMap.value(start));
                        while (end < run.start + run.len)
                        {
                                const CharStyle &endStyle = m_story.charStyle(m_textMap.value(end));
                                if (!startStyle.equivForShaping(endStyle))
                                        break;
                                end++;
                        }
                        if (run.dir == UBIDI_RTL)
                                subRuns.prepend(TextRun(start, end - start, run.dir, run.script));
                        else
                                subRuns.append(TextRun(start, end - start, run.dir, run.script));
                        start = end;
                }

                newRuns.append(subRuns);
        }

        return newRuns;
}

void TextShaper::buildText(int fromPos, int toPos, QVector<int>& smallCaps)
{
        m_text = "";
       
        if (toPos > m_story.length() || toPos < 0)
                toPos = m_story.length();
       
        for (int i = fromPos; i < toPos; ++i)
        {
                QString str = m_story.text(i,1);
               
                if (m_singlePar)
                {
                        QChar ch = str[0];
                        if (ch == SpecialChars::PARSEP || ch == SpecialChars::LINEBREAK)
                                continue;
                }

                if (m_story.hasExpansionPoint(i))
                {
                        m_contextNeeded = true;
                        if (m_context != NULL)
                        {
                                str = m_context->expand(m_story.expansionPoint(i));
                                if (str.isEmpty())
                                        str = SpecialChars::ZWNBSPACE;
                        }
                        else
                        {
                                str = SpecialChars::OBJECT;
                        }
                }
               
                str.replace(SpecialChars::SHYPHEN, SpecialChars::ZWNJ);

                const CharStyle &style = m_story.charStyle(i);
                int effects = style.effects() & ScStyle_UserStyles;
                bool hasSmallCap = false;
                if ((effects & ScStyle_AllCaps) || (effects & ScStyle_SmallCaps))
                {
                        QLocale locale(style.language());
                        QString upper = locale.toUpper(str);
                        if (upper != str)
                        {
                                if (effects & ScStyle_SmallCaps)
                                        hasSmallCap = true;
                                str = upper;
                        }
                }

                for (int j = 0; j < str.length(); j++)
                {
                        m_textMap.insert(m_text.length() + j, i);
                        if (hasSmallCap)
                                smallCaps.append(m_text.length() + j);
                }

                m_text.append(str);
        }
}


ShapedText TextShaper::shape(int fromPos, int toPos)
{
        m_contextNeeded = false;
       
        ShapedText result(ShapedText(&m_story, fromPos, toPos, m_context));
       

        QVector<int> smallCaps;

        buildText(fromPos, toPos, smallCaps);

        QList<TextRun> bidiRuns = itemizeBiDi();
        QList<TextRun> scriptRuns = itemizeScripts(bidiRuns);
        QList<TextRun> textRuns = itemizeStyles(scriptRuns);

        QVector<int32_t> lineBreaks;
        BreakIterator* lineIt = StoryText::getLineIterator();
        // FIXME-HOST: add some fallback code if the iterator failed
        if (lineIt)
        {
                lineIt->setText((const UChar*) m_text.utf16());
                for (int32_t pos = lineIt->first(); pos != BreakIterator::DONE; pos = lineIt->next())
                        lineBreaks.append(pos);
        }

        QVector<int32_t> justificationTracking;

        // Insert implicit spaces in justification between characters
        // in scripts that do not use spaces to seperate words
        foreach (const TextRun& run, scriptRuns) {
                switch (run.script) {
                // clustered scripts from https://drafts.csswg.org/css-text-3/#script-groups
                case USCRIPT_KHMER:
                case USCRIPT_LAO:
                case USCRIPT_MYANMAR:
                case USCRIPT_NEW_TAI_LUE:
                case USCRIPT_TAI_LE:
                case USCRIPT_TAI_VIET:
                case USCRIPT_THAI:
                {
                        BreakIterator* charIt = StoryText::getGraphemeIterator();
                        if (charIt)
                        {
                                const QString text = m_text.mid(run.start, run.len);
                                charIt->setText((const UChar*) text.utf16());
                                int32_t pos = charIt->first();
                                while (pos != BreakIterator::DONE && pos < text.length())
                                {
                                        UErrorCode status = U_ZERO_ERROR;
                                        UScriptCode sc = uscript_getScript(text.at(pos).unicode(), &status);
                                        // do not insert implicit space before punctuation
                                        // or other non-script specific characters
                                        if (sc != USCRIPT_COMMON)
                                                justificationTracking.append(run.start + pos - 1);
                                        pos = charIt->next();
                                }
                        }
                        break;
                }
                default:
                        break;
                }
        }

        foreach (const TextRun& textRun, textRuns) {
                const CharStyle &style = m_story.charStyle(m_textMap.value(textRun.start));

                const ScFace &scFace = style.font();
                hb_font_t *hbFont = reinterpret_cast<hb_font_t*>(scFace.hbFont());
                if (hbFont == NULL)
                        continue;

                hb_font_set_scale(hbFont, style.fontSize(), style.fontSize());
                FT_Face ftFace = hb_ft_font_get_face(hbFont);
                if (ftFace)
                        FT_Set_Char_Size(ftFace, style.fontSize(), 0, 72, 0);

                hb_direction_t hbDirection = (textRun.dir == UBIDI_LTR) ? HB_DIRECTION_LTR : HB_DIRECTION_RTL;
                hb_script_t hbScript = hb_icu_script_to_script(textRun.script);
                std::string language = style.language().toStdString();
                hb_language_t hbLanguage = hb_language_from_string(language.c_str(), language.length());

                hb_buffer_t *hbBuffer = hb_buffer_create();
                hb_buffer_add_utf16(hbBuffer, m_text.utf16(), m_text.length(), textRun.start, textRun.len);
                hb_buffer_set_direction(hbBuffer, hbDirection);
                hb_buffer_set_script(hbBuffer, hbScript);
                hb_buffer_set_language(hbBuffer, hbLanguage);
                hb_buffer_set_cluster_level(hbBuffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS);

                QVector<hb_feature_t> hbFeatures;
                QList<FeaturesRun> featuresRuns = itemizeFeatures(textRun);
                foreach (const FeaturesRun& featuresRun, featuresRuns)
                {
                        const QStringList& features = featuresRun.features;
                        hbFeatures.reserve(features.length());
                        foreach (const QString& feature, features) {
                                hb_feature_t hbFeature;
                                hb_bool_t ok = hb_feature_from_string(feature.toStdString().c_str(), feature.toStdString().length(), &hbFeature);
                                if (ok)
                                {
                                        hbFeature.start = featuresRun.start;
                                        hbFeature.end = featuresRun.len + featuresRun.start;
                                        hbFeatures.append(hbFeature);
                                }
                        }
                }

                // #14523: harfbuzz proritize graphite for graphite enabled fonts, however
                // at the point, shaping with graphite fonts is either buggy (harfbuzz 1.4.2)
                // or trigger weird results (harfbuzz 1.4.3), so disable graphite for now.
                // Prevent also use of platform specific shapers for cross-platform reasons
                const char* shapers[] = { "ot", "fallback", 0 };
                hb_shape_full(hbFont, hbBuffer, hbFeatures.data(), hbFeatures.length(), shapers);

                unsigned int count = hb_buffer_get_length(hbBuffer);
                hb_glyph_info_t *glyphs = hb_buffer_get_glyph_infos(hbBuffer, NULL);
                hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hbBuffer, NULL);

                result.glyphs().reserve(result.glyphs().size() + count);
                for (size_t i = 0; i < count; )
                {
                        uint32_t firstCluster = glyphs[i].cluster;
                        uint32_t nextCluster = firstCluster;
                        if (hbDirection == HB_DIRECTION_LTR)
                        {
                                size_t j = i + 1;
                                while (j < count && nextCluster == firstCluster)
                                {
                                        nextCluster = glyphs[j].cluster;
                                        j++;
                                }
                                if (j == count && nextCluster == firstCluster)
                                        nextCluster = textRun.start + textRun.len;
                        }
                        else
                        {
                                int j = i - 1;
                                while (j >= 0 && nextCluster == firstCluster)
                                {
                                        nextCluster = glyphs[j].cluster;
                                        j--;
                                }
                                if (j <= 0 && nextCluster == firstCluster)
                                        nextCluster = textRun.start + textRun.len;
                        }

                        assert(m_textMap.contains(firstCluster));
                        assert(m_textMap.contains(nextCluster - 1));
                        int firstChar = m_textMap.value(firstCluster);
                        int lastChar = m_textMap.value(nextCluster - 1);
                       
                        QChar ch = m_story.text(firstChar);
                        LayoutFlags flags = m_story.flags(firstChar);
                        const CharStyle& charStyle(m_story.charStyle(firstChar));
                        const StyleFlag& effects = charStyle.effects();

                        QString str = m_text.mid(firstChar, lastChar-firstChar+1);
                        GlyphCluster run(&charStyle, flags, firstChar, lastChar, m_story.object(firstChar), result.glyphs().length(), str);

                        run.clearFlag(ScLayout_HyphenationPossible);
                        if (m_story.hasFlag(lastChar, ScLayout_HyphenationPossible))
                                run.setFlag(ScLayout_HyphenationPossible);
                       
                        if (textRun.dir == UBIDI_RTL)
                                run.setFlag(ScLayout_RightToLeft);

                        if (lineBreaks.contains(firstCluster))
                                run.setFlag(ScLayout_LineBoundary);

                        if (SpecialChars::isExpandingSpace(ch))
                                run.setFlag(ScLayout_ExpandingSpace);
                        else if (justificationTracking.contains(firstCluster))
                                run.setFlag(ScLayout_JustificationTracking);

                        if (effects & ScStyle_Underline)
                                run.setFlag(ScLayout_Underlined);
                        if (effects & ScStyle_UnderlineWords && !ch.isSpace())
                                run.setFlag(ScLayout_Underlined);

                        if (firstChar != 0 &&
                            SpecialChars::isCJK(m_story.text(firstChar).unicode()) &&
                            SpecialChars::isCJK(m_story.text(firstChar - 1).unicode()))
                                run.setFlag(ScLayout_ImplicitSpace);

                        int firstStat = SpecialChars::getCJKAttr(m_story.text(firstChar));
                        int currStat  = (firstChar != lastChar) ? SpecialChars::getCJKAttr(m_story.text(lastChar)) : firstStat;

                        if (firstStat & SpecialChars::CJK_NOBREAK_BEFORE)
                                run.setFlag(ScLayout_NoBreakBefore);

                        if (currStat & SpecialChars::CJK_NOBREAK_AFTER)
                                run.setFlag(ScLayout_NoBreakAfter);

                        if ((firstChar > 0) && (firstStat != 0) && ((firstStat & SpecialChars::CJK_NOBREAK_BEFORE) == 0))
                        {
                                int prevStat = SpecialChars::getCJKAttr(m_story.text(firstChar - 1));
                                if (prevStat != 0 && ((prevStat & SpecialChars::CJK_NOBREAK_AFTER) == 0))
                                        run.setFlag(ScLayout_LineBoundary);
                        }

                        run.setScaleH(charStyle.scaleH() / 1000.0);
                        run.setScaleV(charStyle.scaleV() / 1000.0);

                        while (i < count && glyphs[i].cluster == firstCluster)
                        {
                                GlyphLayout gl;
                                gl.glyph = glyphs[i].codepoint;
                                if (gl.glyph == 0 ||
                                    (ch == SpecialChars::LINEBREAK || ch == SpecialChars::PARSEP ||
                                     ch == SpecialChars::FRAMEBREAK || ch == SpecialChars::COLBREAK))
                                {
                                        gl.glyph = scFace.emulateGlyph(ch.unicode());
                                }

                                if (gl.glyph < ScFace::CONTROL_GLYPHS)
                                {
                                        gl.xoffset = positions[i].x_offset / 10.0;
                                        gl.yoffset = -positions[i].y_offset / 10.0;
                                        gl.xadvance = positions[i].x_advance / 10.0;
                                        gl.yadvance = positions[i].y_advance / 10.0;
                                }

#if 0
                                if (m_story.hasMark(firstChar))
                                {
                                        GlyphLayout control;
                                        control.glyph = SpecialChars::OBJECT.unicode() + ScFace::CONTROL_GLYPHS;
                                        run.append(control);
                                }
#endif
                               
                                if (SpecialChars::isExpandingSpace(ch))
                                        gl.xadvance *= run.style().wordTracking();

                                if (m_story.hasObject(firstChar))
                                {
                                        m_contextNeeded = true;
                                        if (m_context != NULL)
                                                gl.xadvance = m_context->getVisualBoundingBox(m_story.object(firstChar)).width();
                                }

                                if ((effects & ScStyle_Superscript) || (effects & ScStyle_Subscript))
                                {
                                        m_contextNeeded = true;
                                        if (m_context != NULL)
                                        {
                                                double scale;
                                                double asce = style.font().ascent(style.fontSize() / 10.0);
                                                if (effects & ScStyle_Superscript)
                                                {
                                                        gl.yoffset -= asce * m_context->typographicPrefs().valueSuperScript / 100.0;
                                                        scale = qMax(m_context->typographicPrefs().scalingSuperScript / 100.0, 10.0 / style.fontSize());
                                                }
                                                else // effects & ScStyle_Subscript
                                                {
                                                        gl.yoffset += asce * m_context->typographicPrefs().valueSubScript / 100.0;
                                                        scale = qMax(m_context->typographicPrefs().scalingSubScript / 100.0, 10.0 / style.fontSize());
                                                }
                                               
                                                run.setScaleH(run.scaleH() * scale);
                                                run.setScaleV(run.scaleV() * scale);
                                        }
                                }

                                if (smallCaps.contains(firstCluster))
                                {
                                        m_contextNeeded = true;
                                        if (m_context != NULL)
                                        {
                                               
                                                double smallcapsScale = m_context->typographicPrefs().valueSmallCaps / 100.0;
                                                run.setScaleH(run.scaleH() * smallcapsScale);
                                                run.setScaleV(run.scaleV() * smallcapsScale);
                                        }
                                }

                                if (run.scaleH() == 0.0)
                                {
                                        gl.xadvance = 0.0;
                                        run.setScaleH(1.0);
                                }

                                run.append(gl);
                                i++;
                        }

                        // Apply CJK spacing according to JIS X4051
                        if (lastChar + 1 < m_story.length())
                        {
                                double halfEM = run.style().fontSize() / 10 / 2;
                                double quarterEM = run.style().fontSize() / 10 / 4;

                                int nextStat = SpecialChars::getCJKAttr(m_story.text(lastChar + 1));

                                // 1. add 1/4 aki (space) between a CJK letter and
                                //    - a latin letter
                                //    - an ASCII Digits
                                if (currStat != 0)
                                {
                                        // current char is CJK
                                        if (SpecialChars::isLetterRequiringSpaceAroundCJK(m_story.text(lastChar + 1).unicode())) {
                                                switch(currStat & SpecialChars::CJK_CHAR_MASK) {
                                                case SpecialChars::CJK_KANJI:
                                                case SpecialChars::CJK_KANA:
                                                case SpecialChars::CJK_NOTOP:
                                                        run.extraWidth += quarterEM;
                                                }
                                        }
                                }
                                else
                                {
                                        // current char is not CJK
                                        if (SpecialChars::isLetterRequiringSpaceAroundCJK(m_story.text(lastChar).unicode())) {
                                                switch(nextStat & SpecialChars::CJK_CHAR_MASK) {
                                                case SpecialChars::CJK_KANJI:
                                                case SpecialChars::CJK_KANA:
                                                case SpecialChars::CJK_NOTOP:
                                                        // use the size of the current char instead of the next one
                                                        run.extraWidth += quarterEM;
                                                }
                                        }
                                }

                                // 2. remove spaces from glyphs with the following CJK attributes
                                if (currStat != 0)
                                {       // current char is CJK
                                        switch(currStat & SpecialChars::CJK_CHAR_MASK) {
                                        case SpecialChars::CJK_FENCE_END:
                                                switch(nextStat & SpecialChars::CJK_CHAR_MASK) {
                                                case SpecialChars::CJK_FENCE_BEGIN:
                                                case SpecialChars::CJK_FENCE_END:
                                                case SpecialChars::CJK_COMMA:
                                                case SpecialChars::CJK_PERIOD:
                                                case SpecialChars::CJK_MIDPOINT:
                                                        run.extraWidth -= halfEM;
                                                }
                                                break;
                                        case SpecialChars::CJK_COMMA:
                                        case SpecialChars::CJK_PERIOD:
                                                switch(nextStat & SpecialChars::CJK_CHAR_MASK) {
                                                case SpecialChars::CJK_FENCE_BEGIN:
                                                case SpecialChars::CJK_FENCE_END:
                                                        run.extraWidth -= halfEM;;
                                                }
                                                break;
                                        case SpecialChars::CJK_MIDPOINT:
                                                switch(nextStat & SpecialChars::CJK_CHAR_MASK) {
                                                case SpecialChars::CJK_FENCE_BEGIN:
                                                        run.extraWidth -= halfEM;
                                                }
                                                break;
                                        case SpecialChars::CJK_FENCE_BEGIN:
                                                int prevStat = SpecialChars::getCJKAttr(m_story.text(lastChar - 1));
                                                if ((prevStat & SpecialChars::CJK_CHAR_MASK) == SpecialChars::CJK_FENCE_BEGIN)
                                                {
                                                        run.extraWidth -= halfEM;
                                                        run.xoffset -= halfEM;
                                                }
                                                else
                                                {
                                                        run.setFlag(ScLayout_CJKFence);
                                                }
                                                break;
                                        }
                                }
                        }

                        result.glyphs().append(run);
                }
                hb_buffer_destroy(hbBuffer);

        }

        m_textMap.clear();
        m_text = "";
        result.needsContext(m_contextNeeded);
        return result;
}