r22017 by jghali - #14794: insert kerning between CJK letters and only Latin letters
scribus-commit
scribus-commit at lists.scribus.net
Wed May 10 21:03:49 UTC 2017
Author: jghali
Date: Wed May 10 21:03:49 2017
New Revision: 22017
URL: http://scribus.net/websvn/listing.php?repname=Scribus&sc=1&rev=22017
Log:
#14794: insert kerning between CJK letters and only Latin letters
Modified:
trunk/Scribus/scribus/text/specialchars.cpp
trunk/Scribus/scribus/text/specialchars.h
trunk/Scribus/scribus/text/textshaper.cpp
Modified: trunk/Scribus/scribus/text/specialchars.cpp
URL: http://scribus.net/websvn/diff.php?repname=Scribus&rev=22017&path=/trunk/Scribus/scribus/text/specialchars.cpp
==============================================================================
--- trunk/Scribus/scribus/text/specialchars.cpp (original)
+++ trunk/Scribus/scribus/text/specialchars.cpp Wed May 10 21:03:49 2017
@@ -209,6 +209,32 @@
return false;
}
+
+bool SpecialChars::isLetterRequiringSpaceAroundCJK(uint ch) {
+ return (0x0030 <= ch && ch <= 0x0039) || // ASCII digits
+ (0x0041 <= ch && ch <= 0x005A) || // Latin uppercase alphabet
+ (0x0061 <= ch && ch <= 0x007A) || // Latin lowercase alphabet
+ (0x00C0 <= ch && ch <= 0x00D6) || // Letters in Latin-1 Supplement
+ (0x00D8 <= ch && ch <= 0x00F6) || // Letters in Latin-1 Supplement
+ (0x00F8 <= ch && ch <= 0x00FF) || // Letters in Latin-1 Supplement
+ (0x0100 <= ch && ch <= 0x017F) || // Letters in Latin Extended-A
+ (0x0180 <= ch && ch <= 0x024F) || // Letters in Latin Extended-B
+ (0x0250 <= ch && ch <= 0x02AF) || // Letters in IPA Extensions
+ (0x1D00 <= ch && ch <= 0x1D25) || // Letters in Phonetic Extentions
+ (0x1D6B <= ch && ch <= 0x1D70) || // Letters in Phonetic Extentions
+ (0x1D80 <= ch && ch <= 0x1D9A) || // Letters in Phonetic Extentions Supplement
+ (0x1E02 <= ch && ch <= 0x1EF3) || // Letters in Latin Extended Additional
+ (0x2C60 <= ch && ch <= 0x2C7F) || // Letters in Latin Extended-C
+ (0xA722 <= ch && ch <= 0xA787) || // Letters in Latin Extended-D
+ (0xA78B <= ch && ch <= 0xA7AE) || // Letters in Latin Extended-D
+ (0xA7B0 <= ch && ch <= 0xA7B7) || // Letters in Latin Extended-D
+ (0xA7F7 <= ch && ch <= 0xA7BF) || // Letters in Latin Extended-D
+ (0xAB30 <= ch && ch <= 0xAB5A) || // Letters in Latin Extended-D
+ (0xAB5C <= ch && ch <= 0xAB64) || // Letters in Latin Extended-D
+ (0xFB00 <= ch && ch <= 0xFB06) ; // Alphabetic Presentation Forms
+}
+
+
bool SpecialChars::isIgnorableCodePoint(uint ch)
{
// based on list of Default_Ignorable_Code_Point in Unicode 9
Modified: trunk/Scribus/scribus/text/specialchars.h
URL: http://scribus.net/websvn/diff.php?repname=Scribus&rev=22017&path=/trunk/Scribus/scribus/text/specialchars.h
==============================================================================
--- trunk/Scribus/scribus/text/specialchars.h (original)
+++ trunk/Scribus/scribus/text/specialchars.h Wed May 10 21:03:49 2017
@@ -78,6 +78,7 @@
static int getCJKAttr(QChar c);
static bool isCJK(uint ch);
+ static bool isLetterRequiringSpaceAroundCJK(uint ch);
static bool isIgnorableCodePoint(uint ch);
};
Modified: trunk/Scribus/scribus/text/textshaper.cpp
URL: http://scribus.net/websvn/diff.php?repname=Scribus&rev=22017&path=/trunk/Scribus/scribus/text/textshaper.cpp
==============================================================================
--- trunk/Scribus/scribus/text/textshaper.cpp (original)
+++ trunk/Scribus/scribus/text/textshaper.cpp Wed May 10 21:03:49 2017
@@ -1,584 +1,592 @@
-#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);
-
- 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 currStat = SpecialChars::getCJKAttr(m_story.text(lastChar));
- int nextStat = SpecialChars::getCJKAttr(m_story.text(lastChar + 1));
- if (currStat != 0)
- { // current char is CJK
- if (nextStat == 0
- && !SpecialChars::isBreakingSpace(m_story.text(lastChar + 1))
- && SpecialChars::isCJK(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 { // next char is CJK, too
- 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;
- }
- }
- } else { // current char is not CJK
- if (nextStat != 0
- && !SpecialChars::isBreakingSpace(m_story.text(lastChar))
- && !SpecialChars::isCJK(m_story.text(lastChar + 1).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;
- }
- }
- }
- }
-
- result.glyphs().append(run);
- }
- hb_buffer_destroy(hbBuffer);
-
- }
-
- m_textMap.clear();
- m_text = "";
- result.needsContext(m_contextNeeded);
- return result;
-}
+#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);
+
+ 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 currStat = SpecialChars::getCJKAttr(m_story.text(lastChar));
+ 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;
+}
More information about the scribus-commit
mailing list