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