Rev 24826 |
Rev 25223 |
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/brkiter.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"
using namespace icu;
TextShaper::TextShaper(ITextContext* context, ITextSource &story, int firstChar, bool singlePar)
: m_context(context),
m_story(story),
m_firstChar(firstChar),
m_singlePar(singlePar)
{ }
TextShaper::TextShaper(ITextSource &story, int firstChar)
: m_context(nullptr),
m_story(story),
m_firstChar(firstChar),
m_singlePar(false)
{
m_text.reserve(m_story.length() - m_firstChar + 1);
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(int fromPos) const
{
QList<TextRun> textRuns;
UBiDi *obj = ubidi_open();
UErrorCode err = U_ZERO_ERROR;
UBiDiLevel parLevel = UBIDI_LTR;
ParagraphStyle style = m_story.paragraphStyle(fromPos);
if (style.direction() == ParagraphStyle::RTL)
parLevel = UBIDI_RTL;
ubidi_setPara(obj, (const UChar*) m_text.utf16(), m_text.length(), parLevel, nullptr, &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) const
{
QList<TextRun> newRuns;
ScriptRun scriptrun((const UChar*) m_text.utf16(), m_text.length());
for (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) const
{
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) const
{
QList<TextRun> newRuns;
for (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.clear();
if (toPos > m_story.length() || toPos < 0)
toPos = m_story.length();
if (m_text.capacity() < (toPos - fromPos + 1))
m_text.reserve(toPos - fromPos + 1);
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 != nullptr)
{
str = m_context->expand(m_story.expansionPoint(i));
if (str.isEmpty())
str = SpecialChars::ZWNBSPACE;
}
else
{
str = SpecialChars::OBJECT;
}
}
str.replace(SpecialChars::SHYPHEN, SpecialChars::ZWNJ);
//set style for paragraph effects
if (m_story.isBlockStart(i) && (m_context != nullptr) && (m_context->getDoc() != nullptr))
{
const ScribusDoc* doc = m_context->getDoc();
const ParagraphStyle& style = m_story.paragraphStyle(i);
if (style.hasDropCap() || style.hasBullet() || style.hasNum())
{
CharStyle charStyle = (m_story.text(i) != SpecialChars::PARSEP) ? m_story.charStyle(i) : style.charStyle();
const QString& curParent(style.hasParent() ? style.parent() : style.name());
CharStyle newStyle(charStyle);
if (style.peCharStyleName().isEmpty())
newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
else if (charStyle.name() != style.peCharStyleName())
newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
charStyle.setStyle(newStyle);
m_story.setCharStyle(i, 1, charStyle);
}
else if (!style.peCharStyleName().isEmpty())
{
//par effect is cleared but is set dcCharStyleName = clear drop cap char style
CharStyle charStyle = (m_story.text(i) != SpecialChars::PARSEP) ? m_story.charStyle(i) : style.charStyle();
if (charStyle.parent() == style.peCharStyleName())
{
const QString& curParent(style.hasParent() ? style.parent() : style.name());
if (doc->charStyles().contains(style.peCharStyleName()))
charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
m_story.setCharStyle(i, 1, charStyle);
}
}
}
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(&m_story, fromPos, toPos, m_context);
QVector<int> smallCaps;
buildText(fromPos, toPos, smallCaps);
QList<TextRun> bidiRuns = itemizeBiDi(fromPos);
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 separate words
for (const TextRun& run : qAsConst(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;
}
}
for (const TextRun& textRun : qAsConst(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 == nullptr)
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;
const QList<FeaturesRun> featuresRuns = itemizeFeatures(textRun);
for (const FeaturesRun& featuresRun : featuresRuns)
{
const QStringList& features = featuresRun.features;
hbFeatures.reserve(features.length());
for (const QString& feature : features)
{
hb_feature_t hbFeature;
std::string strFeature(feature.toStdString());
hb_bool_t ok = hb_feature_from_string(strFeature.c_str(), strFeature.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", nullptr };
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, nullptr);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hbBuffer, nullptr);
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 - fromPos, 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 (SpecialChars::isFixedSpace(ch))
run.setFlag(ScLayout_FixedSpace);
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::isImplicitSpace(m_story.text(firstChar - 1).unicode(), m_story.text(firstChar).unicode()))
run.setFlag(ScLayout_ImplicitSpace);
int firstStat = SpecialChars::getCJKAttr(m_story.text(firstChar));
int currStat = (firstChar != lastChar) ? SpecialChars::getCJKAttr(m_story.text(lastChar)) : firstStat;
int prevStat = (firstChar > 0) ? SpecialChars::getCJKAttr(m_story.text(firstChar - 1)) : 0;
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))
{
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());
GlyphMetrics metrics = scFace.glyphBBox(gl.glyph, style.fontSize());
positions[i].x_advance = metrics.width;
}
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 != nullptr)
gl.xadvance = m_context->getVisualBoundingBox(m_story.object(firstChar)).width();
}
if ((effects & ScStyle_Superscript) || (effects & ScStyle_Subscript))
{
m_contextNeeded = true;
if (m_context != nullptr)
{
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 != nullptr)
{
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
// https://www.w3.org/TR/jlreq/
// 1. add 1/4 aki (space) between a CJK letter and
// - a latin letter
// - an ASCII digit
if (firstChar > 0)
{
if (prevStat == 0)
{
// <Latin> <<CJK>>
if (SpecialChars::isLetterRequiringSpaceAroundCJK(m_story.text(firstChar - 1).unicode()))
{
switch (currStat & SpecialChars::CJK_CHAR_MASK)
{
case SpecialChars::CJK_KANJI:
case SpecialChars::CJK_KANA:
case SpecialChars::CJK_NOTOP:
run.setFlag(ScLayout_CJKLatinSpace);
}
}
}
else
{
// <CJK> <<Latin>>
if (SpecialChars::isLetterRequiringSpaceAroundCJK(m_story.text(firstChar).unicode()))
{
switch (prevStat & SpecialChars::CJK_CHAR_MASK)
{
case SpecialChars::CJK_KANJI:
case SpecialChars::CJK_KANA:
case SpecialChars::CJK_NOTOP:
// use the size of the current Latin char
// instead of the previous CJK char
run.setFlag(ScLayout_CJKLatinSpace);
}
}
}
}
// 2. remove spaces from glyphs with the following CJK attributes
if (lastChar + 1 < m_story.length())
{
if (currStat != 0)
{ // current char is CJK
double halfEM = run.style().fontSize() / 10 / 2;
int nextStat = SpecialChars::getCJKAttr(m_story.text(lastChar + 1));
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:
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;
}