Rev 24826 | Rev 25223 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
22017 | jghali | 1 | #include "textshaper.h" |
2 | |||
3 | #include <harfbuzz/hb.h> |
||
4 | #include <harfbuzz/hb-ft.h> |
||
5 | #include <harfbuzz/hb-icu.h> |
||
23661 | craig | 6 | #include <unicode/brkiter.h> |
22017 | jghali | 7 | #include <unicode/ubidi.h> |
8 | |||
9 | #include "scrptrun.h" |
||
10 | |||
11 | #include "glyphcluster.h" |
||
12 | #include "pageitem.h" |
||
13 | #include "scribusdoc.h" |
||
14 | #include "storytext.h" |
||
15 | #include "styles/paragraphstyle.h" |
||
16 | #include "util.h" |
||
17 | |||
22463 | jghali | 18 | using namespace icu; |
22017 | jghali | 19 | |
20 | TextShaper::TextShaper(ITextContext* context, ITextSource &story, int firstChar, bool singlePar) |
||
21 | : m_context(context), |
||
22 | m_story(story), |
||
23 | m_firstChar(firstChar), |
||
24 | m_singlePar(singlePar) |
||
25 | { } |
||
26 | |||
27 | TextShaper::TextShaper(ITextSource &story, int firstChar) |
||
22518 | craig | 28 | : m_context(nullptr), |
22017 | jghali | 29 | m_story(story), |
30 | m_firstChar(firstChar), |
||
31 | m_singlePar(false) |
||
32 | { |
||
23075 | jghali | 33 | m_text.reserve(m_story.length() - m_firstChar + 1); |
22017 | jghali | 34 | for (int i = m_firstChar; i < m_story.length(); ++i) |
35 | { |
||
36 | QChar ch = m_story.text(i); |
||
37 | if (ch == SpecialChars::PARSEP || ch == SpecialChars::LINEBREAK) |
||
38 | continue; |
||
39 | QString str(ch); |
||
40 | m_textMap.insert(i, i); |
||
41 | m_text.append(str); |
||
42 | } |
||
43 | } |
||
44 | |||
25209 | jghali | 45 | QList<TextShaper::TextRun> TextShaper::itemizeBiDi(int fromPos) const |
22017 | jghali | 46 | { |
47 | QList<TextRun> textRuns; |
||
48 | UBiDi *obj = ubidi_open(); |
||
49 | UErrorCode err = U_ZERO_ERROR; |
||
50 | |||
51 | UBiDiLevel parLevel = UBIDI_LTR; |
||
25209 | jghali | 52 | ParagraphStyle style = m_story.paragraphStyle(fromPos); |
22017 | jghali | 53 | if (style.direction() == ParagraphStyle::RTL) |
54 | parLevel = UBIDI_RTL; |
||
55 | |||
22518 | craig | 56 | ubidi_setPara(obj, (const UChar*) m_text.utf16(), m_text.length(), parLevel, nullptr, &err); |
22017 | jghali | 57 | if (U_SUCCESS(err)) |
58 | { |
||
59 | int32_t count = ubidi_countRuns(obj, &err); |
||
60 | if (U_SUCCESS(err)) |
||
61 | { |
||
62 | textRuns.reserve(count); |
||
63 | for (int32_t i = 0; i < count; i++) |
||
64 | { |
||
65 | int32_t start, length; |
||
66 | UBiDiDirection dir = ubidi_getVisualRun(obj, i, &start, &length); |
||
67 | textRuns.append(TextRun(start, length, dir)); |
||
68 | } |
||
69 | } |
||
70 | } |
||
71 | |||
72 | ubidi_close(obj); |
||
73 | return textRuns; |
||
74 | } |
||
75 | |||
24754 | jghali | 76 | QList<TextShaper::TextRun> TextShaper::itemizeScripts(const QList<TextRun> &runs) const |
22017 | jghali | 77 | { |
78 | QList<TextRun> newRuns; |
||
79 | ScriptRun scriptrun((const UChar*) m_text.utf16(), m_text.length()); |
||
80 | |||
22506 | jghali | 81 | for (TextRun run : runs) |
22017 | jghali | 82 | { |
83 | int start = run.start; |
||
84 | QList<TextRun> subRuns; |
||
85 | |||
86 | while (scriptrun.next()) |
||
87 | { |
||
88 | if (scriptrun.getScriptStart() <= start && scriptrun.getScriptEnd() > start) |
||
89 | break; |
||
90 | } |
||
91 | |||
92 | while (start < run.start + run.len) |
||
93 | { |
||
94 | int end = qMin(scriptrun.getScriptEnd(), run.start + run.len); |
||
95 | UScriptCode script = scriptrun.getScriptCode(); |
||
96 | if (run.dir == UBIDI_RTL) |
||
97 | subRuns.prepend(TextRun(start, end - start, run.dir, script)); |
||
98 | else |
||
99 | subRuns.append(TextRun(start, end - start, run.dir, script)); |
||
100 | |||
101 | start = end; |
||
102 | scriptrun.next(); |
||
103 | } |
||
104 | |||
105 | scriptrun.reset(); |
||
106 | newRuns.append(subRuns); |
||
107 | } |
||
108 | |||
109 | return newRuns; |
||
110 | } |
||
111 | |||
24754 | jghali | 112 | QList<TextShaper::FeaturesRun> TextShaper::itemizeFeatures(const TextRun &run) const |
22017 | jghali | 113 | { |
114 | QList<FeaturesRun> newRuns; |
||
115 | QList<FeaturesRun> subfeature; |
||
116 | int start = run.start; |
||
117 | |||
118 | while (start < run.start + run.len) |
||
119 | { |
||
120 | int end = start; |
||
121 | QStringList startFeatures = m_story.charStyle(m_textMap.value(start)).fontFeatures().split(","); |
||
122 | while (end < run.start + run.len) |
||
123 | { |
||
124 | QStringList endFeatures = m_story.charStyle(m_textMap.value(end)).fontFeatures().split(","); |
||
125 | if (startFeatures != endFeatures) |
||
126 | break; |
||
127 | end++; |
||
128 | } |
||
129 | subfeature.append(FeaturesRun(start, end - start, startFeatures)); |
||
130 | start = end; |
||
131 | startFeatures.clear(); |
||
132 | } |
||
133 | newRuns.append(subfeature); |
||
134 | return newRuns; |
||
135 | } |
||
136 | |||
24754 | jghali | 137 | QList<TextShaper::TextRun> TextShaper::itemizeStyles(const QList<TextRun> &runs) const |
22017 | jghali | 138 | { |
139 | QList<TextRun> newRuns; |
||
140 | |||
22506 | jghali | 141 | for (TextRun run : runs) |
142 | { |
||
22017 | jghali | 143 | int start = run.start; |
144 | QList<TextRun> subRuns; |
||
145 | |||
146 | while (start < run.start + run.len) |
||
147 | { |
||
148 | int end = start; |
||
149 | const CharStyle &startStyle = m_story.charStyle(m_textMap.value(start)); |
||
150 | while (end < run.start + run.len) |
||
151 | { |
||
152 | const CharStyle &endStyle = m_story.charStyle(m_textMap.value(end)); |
||
153 | if (!startStyle.equivForShaping(endStyle)) |
||
154 | break; |
||
155 | end++; |
||
156 | } |
||
157 | if (run.dir == UBIDI_RTL) |
||
158 | subRuns.prepend(TextRun(start, end - start, run.dir, run.script)); |
||
159 | else |
||
160 | subRuns.append(TextRun(start, end - start, run.dir, run.script)); |
||
161 | start = end; |
||
162 | } |
||
163 | |||
164 | newRuns.append(subRuns); |
||
165 | } |
||
166 | |||
167 | return newRuns; |
||
168 | } |
||
169 | |||
170 | void TextShaper::buildText(int fromPos, int toPos, QVector<int>& smallCaps) |
||
171 | { |
||
23075 | jghali | 172 | m_text.clear(); |
22017 | jghali | 173 | |
174 | if (toPos > m_story.length() || toPos < 0) |
||
175 | toPos = m_story.length(); |
||
23076 | jghali | 176 | |
177 | if (m_text.capacity() < (toPos - fromPos + 1)) |
||
178 | m_text.reserve(toPos - fromPos + 1); |
||
22017 | jghali | 179 | |
180 | for (int i = fromPos; i < toPos; ++i) |
||
181 | { |
||
23670 | craig | 182 | QString str(m_story.text(i,1)); |
22017 | jghali | 183 | |
184 | if (m_singlePar) |
||
185 | { |
||
186 | QChar ch = str[0]; |
||
187 | if (ch == SpecialChars::PARSEP || ch == SpecialChars::LINEBREAK) |
||
188 | continue; |
||
189 | } |
||
190 | |||
191 | if (m_story.hasExpansionPoint(i)) |
||
192 | { |
||
193 | m_contextNeeded = true; |
||
22518 | craig | 194 | if (m_context != nullptr) |
22017 | jghali | 195 | { |
196 | str = m_context->expand(m_story.expansionPoint(i)); |
||
197 | if (str.isEmpty()) |
||
198 | str = SpecialChars::ZWNBSPACE; |
||
199 | } |
||
200 | else |
||
201 | { |
||
202 | str = SpecialChars::OBJECT; |
||
203 | } |
||
204 | } |
||
205 | |||
206 | str.replace(SpecialChars::SHYPHEN, SpecialChars::ZWNJ); |
||
207 | |||
22367 | jghali | 208 | //set style for paragraph effects |
22607 | craig | 209 | if (m_story.isBlockStart(i) && (m_context != nullptr) && (m_context->getDoc() != nullptr)) |
22367 | jghali | 210 | { |
211 | const ScribusDoc* doc = m_context->getDoc(); |
||
212 | const ParagraphStyle& style = m_story.paragraphStyle(i); |
||
213 | if (style.hasDropCap() || style.hasBullet() || style.hasNum()) |
||
214 | { |
||
24754 | jghali | 215 | CharStyle charStyle = (m_story.text(i) != SpecialChars::PARSEP) ? m_story.charStyle(i) : style.charStyle(); |
22367 | jghali | 216 | const QString& curParent(style.hasParent() ? style.parent() : style.name()); |
217 | CharStyle newStyle(charStyle); |
||
218 | if (style.peCharStyleName().isEmpty()) |
||
219 | newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name()); |
||
220 | else if (charStyle.name() != style.peCharStyleName()) |
||
221 | newStyle.setParent(doc->charStyle(style.peCharStyleName()).name()); |
||
222 | charStyle.setStyle(newStyle); |
||
223 | m_story.setCharStyle(i, 1, charStyle); |
||
224 | } |
||
225 | else if (!style.peCharStyleName().isEmpty()) |
||
226 | { |
||
227 | //par effect is cleared but is set dcCharStyleName = clear drop cap char style |
||
24754 | jghali | 228 | CharStyle charStyle = (m_story.text(i) != SpecialChars::PARSEP) ? m_story.charStyle(i) : style.charStyle(); |
22367 | jghali | 229 | if (charStyle.parent() == style.peCharStyleName()) |
230 | { |
||
231 | const QString& curParent(style.hasParent() ? style.parent() : style.name()); |
||
232 | if (doc->charStyles().contains(style.peCharStyleName())) |
||
233 | charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName())); |
||
234 | charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name()); |
||
235 | m_story.setCharStyle(i, 1, charStyle); |
||
236 | } |
||
237 | } |
||
238 | } |
||
239 | |||
22017 | jghali | 240 | const CharStyle &style = m_story.charStyle(i); |
241 | int effects = style.effects() & ScStyle_UserStyles; |
||
242 | bool hasSmallCap = false; |
||
243 | if ((effects & ScStyle_AllCaps) || (effects & ScStyle_SmallCaps)) |
||
244 | { |
||
245 | QLocale locale(style.language()); |
||
246 | QString upper = locale.toUpper(str); |
||
247 | if (upper != str) |
||
248 | { |
||
249 | if (effects & ScStyle_SmallCaps) |
||
250 | hasSmallCap = true; |
||
251 | str = upper; |
||
252 | } |
||
253 | } |
||
254 | |||
255 | for (int j = 0; j < str.length(); j++) |
||
256 | { |
||
257 | m_textMap.insert(m_text.length() + j, i); |
||
258 | if (hasSmallCap) |
||
259 | smallCaps.append(m_text.length() + j); |
||
260 | } |
||
261 | |||
262 | m_text.append(str); |
||
263 | } |
||
264 | } |
||
265 | |||
266 | |||
267 | ShapedText TextShaper::shape(int fromPos, int toPos) |
||
268 | { |
||
269 | m_contextNeeded = false; |
||
270 | |||
24754 | jghali | 271 | ShapedText result(&m_story, fromPos, toPos, m_context); |
22017 | jghali | 272 | |
273 | QVector<int> smallCaps; |
||
274 | |||
275 | buildText(fromPos, toPos, smallCaps); |
||
276 | |||
25209 | jghali | 277 | QList<TextRun> bidiRuns = itemizeBiDi(fromPos); |
22017 | jghali | 278 | QList<TextRun> scriptRuns = itemizeScripts(bidiRuns); |
279 | QList<TextRun> textRuns = itemizeStyles(scriptRuns); |
||
280 | |||
281 | QVector<int32_t> lineBreaks; |
||
282 | BreakIterator* lineIt = StoryText::getLineIterator(); |
||
283 | // FIXME-HOST: add some fallback code if the iterator failed |
||
284 | if (lineIt) |
||
285 | { |
||
286 | lineIt->setText((const UChar*) m_text.utf16()); |
||
287 | for (int32_t pos = lineIt->first(); pos != BreakIterator::DONE; pos = lineIt->next()) |
||
288 | lineBreaks.append(pos); |
||
289 | } |
||
290 | |||
291 | QVector<int32_t> justificationTracking; |
||
292 | |||
293 | // Insert implicit spaces in justification between characters |
||
24665 | jghali | 294 | // in scripts that do not use spaces to separate words |
24055 | craig | 295 | for (const TextRun& run : qAsConst(scriptRuns)) |
22506 | jghali | 296 | { |
22017 | jghali | 297 | switch (run.script) { |
298 | // clustered scripts from https://drafts.csswg.org/css-text-3/#script-groups |
||
299 | case USCRIPT_KHMER: |
||
300 | case USCRIPT_LAO: |
||
301 | case USCRIPT_MYANMAR: |
||
302 | case USCRIPT_NEW_TAI_LUE: |
||
303 | case USCRIPT_TAI_LE: |
||
304 | case USCRIPT_TAI_VIET: |
||
305 | case USCRIPT_THAI: |
||
306 | { |
||
307 | BreakIterator* charIt = StoryText::getGraphemeIterator(); |
||
308 | if (charIt) |
||
309 | { |
||
310 | const QString text = m_text.mid(run.start, run.len); |
||
311 | charIt->setText((const UChar*) text.utf16()); |
||
312 | int32_t pos = charIt->first(); |
||
313 | while (pos != BreakIterator::DONE && pos < text.length()) |
||
314 | { |
||
315 | UErrorCode status = U_ZERO_ERROR; |
||
316 | UScriptCode sc = uscript_getScript(text.at(pos).unicode(), &status); |
||
317 | // do not insert implicit space before punctuation |
||
318 | // or other non-script specific characters |
||
319 | if (sc != USCRIPT_COMMON) |
||
320 | justificationTracking.append(run.start + pos - 1); |
||
321 | pos = charIt->next(); |
||
322 | } |
||
323 | } |
||
324 | break; |
||
325 | } |
||
326 | default: |
||
327 | break; |
||
328 | } |
||
329 | } |
||
330 | |||
24209 | jghali | 331 | for (const TextRun& textRun : qAsConst(textRuns)) |
22506 | jghali | 332 | { |
22017 | jghali | 333 | const CharStyle &style = m_story.charStyle(m_textMap.value(textRun.start)); |
334 | |||
335 | const ScFace &scFace = style.font(); |
||
336 | hb_font_t *hbFont = reinterpret_cast<hb_font_t*>(scFace.hbFont()); |
||
22518 | craig | 337 | if (hbFont == nullptr) |
22017 | jghali | 338 | continue; |
339 | |||
340 | hb_font_set_scale(hbFont, style.fontSize(), style.fontSize()); |
||
341 | FT_Face ftFace = hb_ft_font_get_face(hbFont); |
||
342 | if (ftFace) |
||
343 | FT_Set_Char_Size(ftFace, style.fontSize(), 0, 72, 0); |
||
344 | |||
345 | hb_direction_t hbDirection = (textRun.dir == UBIDI_LTR) ? HB_DIRECTION_LTR : HB_DIRECTION_RTL; |
||
346 | hb_script_t hbScript = hb_icu_script_to_script(textRun.script); |
||
347 | std::string language = style.language().toStdString(); |
||
348 | hb_language_t hbLanguage = hb_language_from_string(language.c_str(), language.length()); |
||
349 | |||
350 | hb_buffer_t *hbBuffer = hb_buffer_create(); |
||
351 | hb_buffer_add_utf16(hbBuffer, m_text.utf16(), m_text.length(), textRun.start, textRun.len); |
||
352 | hb_buffer_set_direction(hbBuffer, hbDirection); |
||
353 | hb_buffer_set_script(hbBuffer, hbScript); |
||
354 | hb_buffer_set_language(hbBuffer, hbLanguage); |
||
355 | hb_buffer_set_cluster_level(hbBuffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); |
||
356 | |||
357 | QVector<hb_feature_t> hbFeatures; |
||
22506 | jghali | 358 | const QList<FeaturesRun> featuresRuns = itemizeFeatures(textRun); |
359 | for (const FeaturesRun& featuresRun : featuresRuns) |
||
22017 | jghali | 360 | { |
361 | const QStringList& features = featuresRun.features; |
||
362 | hbFeatures.reserve(features.length()); |
||
22506 | jghali | 363 | for (const QString& feature : features) |
364 | { |
||
22017 | jghali | 365 | hb_feature_t hbFeature; |
22506 | jghali | 366 | std::string strFeature(feature.toStdString()); |
367 | hb_bool_t ok = hb_feature_from_string(strFeature.c_str(), strFeature.length(), &hbFeature); |
||
22017 | jghali | 368 | if (ok) |
369 | { |
||
370 | hbFeature.start = featuresRun.start; |
||
371 | hbFeature.end = featuresRun.len + featuresRun.start; |
||
372 | hbFeatures.append(hbFeature); |
||
373 | } |
||
374 | } |
||
375 | } |
||
376 | |||
377 | // #14523: harfbuzz proritize graphite for graphite enabled fonts, however |
||
378 | // at the point, shaping with graphite fonts is either buggy (harfbuzz 1.4.2) |
||
379 | // or trigger weird results (harfbuzz 1.4.3), so disable graphite for now. |
||
380 | // Prevent also use of platform specific shapers for cross-platform reasons |
||
22607 | craig | 381 | const char* shapers[] = { "ot", "fallback", nullptr }; |
22017 | jghali | 382 | hb_shape_full(hbFont, hbBuffer, hbFeatures.data(), hbFeatures.length(), shapers); |
383 | |||
384 | unsigned int count = hb_buffer_get_length(hbBuffer); |
||
22518 | craig | 385 | hb_glyph_info_t *glyphs = hb_buffer_get_glyph_infos(hbBuffer, nullptr); |
386 | hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hbBuffer, nullptr); |
||
22017 | jghali | 387 | |
388 | result.glyphs().reserve(result.glyphs().size() + count); |
||
389 | for (size_t i = 0; i < count; ) |
||
390 | { |
||
391 | uint32_t firstCluster = glyphs[i].cluster; |
||
392 | uint32_t nextCluster = firstCluster; |
||
393 | if (hbDirection == HB_DIRECTION_LTR) |
||
394 | { |
||
395 | size_t j = i + 1; |
||
396 | while (j < count && nextCluster == firstCluster) |
||
397 | { |
||
398 | nextCluster = glyphs[j].cluster; |
||
399 | j++; |
||
400 | } |
||
401 | if (j == count && nextCluster == firstCluster) |
||
402 | nextCluster = textRun.start + textRun.len; |
||
403 | } |
||
404 | else |
||
405 | { |
||
406 | int j = i - 1; |
||
407 | while (j >= 0 && nextCluster == firstCluster) |
||
408 | { |
||
409 | nextCluster = glyphs[j].cluster; |
||
410 | j--; |
||
411 | } |
||
412 | if (j <= 0 && nextCluster == firstCluster) |
||
413 | nextCluster = textRun.start + textRun.len; |
||
414 | } |
||
415 | |||
416 | assert(m_textMap.contains(firstCluster)); |
||
417 | assert(m_textMap.contains(nextCluster - 1)); |
||
418 | int firstChar = m_textMap.value(firstCluster); |
||
419 | int lastChar = m_textMap.value(nextCluster - 1); |
||
420 | |||
421 | QChar ch = m_story.text(firstChar); |
||
422 | LayoutFlags flags = m_story.flags(firstChar); |
||
423 | const CharStyle& charStyle(m_story.charStyle(firstChar)); |
||
424 | const StyleFlag& effects = charStyle.effects(); |
||
425 | |||
24430 | jghali | 426 | QString str = m_text.mid(firstChar - fromPos, lastChar - firstChar + 1); |
22017 | jghali | 427 | GlyphCluster run(&charStyle, flags, firstChar, lastChar, m_story.object(firstChar), result.glyphs().length(), str); |
428 | |||
429 | run.clearFlag(ScLayout_HyphenationPossible); |
||
430 | if (m_story.hasFlag(lastChar, ScLayout_HyphenationPossible)) |
||
431 | run.setFlag(ScLayout_HyphenationPossible); |
||
432 | |||
433 | if (textRun.dir == UBIDI_RTL) |
||
434 | run.setFlag(ScLayout_RightToLeft); |
||
435 | |||
436 | if (lineBreaks.contains(firstCluster)) |
||
437 | run.setFlag(ScLayout_LineBoundary); |
||
438 | |||
439 | if (SpecialChars::isExpandingSpace(ch)) |
||
440 | run.setFlag(ScLayout_ExpandingSpace); |
||
24492 | jghali | 441 | else if (SpecialChars::isFixedSpace(ch)) |
442 | run.setFlag(ScLayout_FixedSpace); |
||
22017 | jghali | 443 | else if (justificationTracking.contains(firstCluster)) |
444 | run.setFlag(ScLayout_JustificationTracking); |
||
445 | |||
446 | if (effects & ScStyle_Underline) |
||
447 | run.setFlag(ScLayout_Underlined); |
||
448 | if (effects & ScStyle_UnderlineWords && !ch.isSpace()) |
||
449 | run.setFlag(ScLayout_Underlined); |
||
450 | |||
23710 | jghali | 451 | if (firstChar != 0 && SpecialChars::isImplicitSpace(m_story.text(firstChar - 1).unicode(), m_story.text(firstChar).unicode())) |
22017 | jghali | 452 | run.setFlag(ScLayout_ImplicitSpace); |
453 | |||
22178 | jghali | 454 | int firstStat = SpecialChars::getCJKAttr(m_story.text(firstChar)); |
455 | int currStat = (firstChar != lastChar) ? SpecialChars::getCJKAttr(m_story.text(lastChar)) : firstStat; |
||
23709 | jghali | 456 | int prevStat = (firstChar > 0) ? SpecialChars::getCJKAttr(m_story.text(firstChar - 1)) : 0; |
22178 | jghali | 457 | |
458 | if (firstStat & SpecialChars::CJK_NOBREAK_BEFORE) |
||
459 | run.setFlag(ScLayout_NoBreakBefore); |
||
460 | |||
461 | if (currStat & SpecialChars::CJK_NOBREAK_AFTER) |
||
462 | run.setFlag(ScLayout_NoBreakAfter); |
||
463 | |||
464 | if ((firstChar > 0) && (firstStat != 0) && ((firstStat & SpecialChars::CJK_NOBREAK_BEFORE) == 0)) |
||
465 | { |
||
466 | if (prevStat != 0 && ((prevStat & SpecialChars::CJK_NOBREAK_AFTER) == 0)) |
||
467 | run.setFlag(ScLayout_LineBoundary); |
||
468 | } |
||
469 | |||
22017 | jghali | 470 | run.setScaleH(charStyle.scaleH() / 1000.0); |
471 | run.setScaleV(charStyle.scaleV() / 1000.0); |
||
472 | |||
473 | while (i < count && glyphs[i].cluster == firstCluster) |
||
474 | { |
||
475 | GlyphLayout gl; |
||
476 | gl.glyph = glyphs[i].codepoint; |
||
477 | if (gl.glyph == 0 || |
||
478 | (ch == SpecialChars::LINEBREAK || ch == SpecialChars::PARSEP || |
||
479 | ch == SpecialChars::FRAMEBREAK || ch == SpecialChars::COLBREAK)) |
||
480 | { |
||
481 | gl.glyph = scFace.emulateGlyph(ch.unicode()); |
||
22737 | jghali | 482 | |
483 | GlyphMetrics metrics = scFace.glyphBBox(gl.glyph, style.fontSize()); |
||
484 | positions[i].x_advance = metrics.width; |
||
22017 | jghali | 485 | } |
486 | |||
487 | if (gl.glyph < ScFace::CONTROL_GLYPHS) |
||
488 | { |
||
489 | gl.xoffset = positions[i].x_offset / 10.0; |
||
490 | gl.yoffset = -positions[i].y_offset / 10.0; |
||
491 | gl.xadvance = positions[i].x_advance / 10.0; |
||
492 | gl.yadvance = positions[i].y_advance / 10.0; |
||
493 | } |
||
494 | |||
495 | #if 0 |
||
496 | if (m_story.hasMark(firstChar)) |
||
497 | { |
||
498 | GlyphLayout control; |
||
499 | control.glyph = SpecialChars::OBJECT.unicode() + ScFace::CONTROL_GLYPHS; |
||
500 | run.append(control); |
||
501 | } |
||
502 | #endif |
||
503 | |||
504 | if (SpecialChars::isExpandingSpace(ch)) |
||
505 | gl.xadvance *= run.style().wordTracking(); |
||
506 | |||
507 | if (m_story.hasObject(firstChar)) |
||
508 | { |
||
509 | m_contextNeeded = true; |
||
22518 | craig | 510 | if (m_context != nullptr) |
22017 | jghali | 511 | gl.xadvance = m_context->getVisualBoundingBox(m_story.object(firstChar)).width(); |
512 | } |
||
513 | |||
514 | if ((effects & ScStyle_Superscript) || (effects & ScStyle_Subscript)) |
||
515 | { |
||
516 | m_contextNeeded = true; |
||
22518 | craig | 517 | if (m_context != nullptr) |
22017 | jghali | 518 | { |
519 | double scale; |
||
520 | double asce = style.font().ascent(style.fontSize() / 10.0); |
||
521 | if (effects & ScStyle_Superscript) |
||
522 | { |
||
523 | gl.yoffset -= asce * m_context->typographicPrefs().valueSuperScript / 100.0; |
||
524 | scale = qMax(m_context->typographicPrefs().scalingSuperScript / 100.0, 10.0 / style.fontSize()); |
||
525 | } |
||
526 | else // effects & ScStyle_Subscript |
||
527 | { |
||
528 | gl.yoffset += asce * m_context->typographicPrefs().valueSubScript / 100.0; |
||
529 | scale = qMax(m_context->typographicPrefs().scalingSubScript / 100.0, 10.0 / style.fontSize()); |
||
530 | } |
||
531 | |||
532 | run.setScaleH(run.scaleH() * scale); |
||
533 | run.setScaleV(run.scaleV() * scale); |
||
534 | } |
||
535 | } |
||
536 | |||
537 | if (smallCaps.contains(firstCluster)) |
||
538 | { |
||
539 | m_contextNeeded = true; |
||
22518 | craig | 540 | if (m_context != nullptr) |
22017 | jghali | 541 | { |
542 | double smallcapsScale = m_context->typographicPrefs().valueSmallCaps / 100.0; |
||
543 | run.setScaleH(run.scaleH() * smallcapsScale); |
||
544 | run.setScaleV(run.scaleV() * smallcapsScale); |
||
545 | } |
||
546 | } |
||
547 | |||
548 | if (run.scaleH() == 0.0) |
||
549 | { |
||
550 | gl.xadvance = 0.0; |
||
551 | run.setScaleH(1.0); |
||
552 | } |
||
553 | |||
554 | run.append(gl); |
||
555 | i++; |
||
556 | } |
||
557 | |||
558 | // Apply CJK spacing according to JIS X4051 |
||
23709 | jghali | 559 | // https://www.w3.org/TR/jlreq/ |
560 | |||
561 | // 1. add 1/4 aki (space) between a CJK letter and |
||
562 | // - a latin letter |
||
563 | // - an ASCII digit |
||
564 | if (firstChar > 0) |
||
22017 | jghali | 565 | { |
23709 | jghali | 566 | if (prevStat == 0) |
22017 | jghali | 567 | { |
23709 | jghali | 568 | // <Latin> <<CJK>> |
569 | if (SpecialChars::isLetterRequiringSpaceAroundCJK(m_story.text(firstChar - 1).unicode())) |
||
22724 | jghali | 570 | { |
571 | switch (currStat & SpecialChars::CJK_CHAR_MASK) |
||
572 | { |
||
573 | case SpecialChars::CJK_KANJI: |
||
574 | case SpecialChars::CJK_KANA: |
||
575 | case SpecialChars::CJK_NOTOP: |
||
23709 | jghali | 576 | run.setFlag(ScLayout_CJKLatinSpace); |
22017 | jghali | 577 | } |
578 | } |
||
579 | } |
||
580 | else |
||
581 | { |
||
23709 | jghali | 582 | // <CJK> <<Latin>> |
583 | if (SpecialChars::isLetterRequiringSpaceAroundCJK(m_story.text(firstChar).unicode())) |
||
22724 | jghali | 584 | { |
23709 | jghali | 585 | switch (prevStat & SpecialChars::CJK_CHAR_MASK) |
22724 | jghali | 586 | { |
587 | case SpecialChars::CJK_KANJI: |
||
588 | case SpecialChars::CJK_KANA: |
||
589 | case SpecialChars::CJK_NOTOP: |
||
23709 | jghali | 590 | // use the size of the current Latin char |
591 | // instead of the previous CJK char |
||
592 | run.setFlag(ScLayout_CJKLatinSpace); |
||
22017 | jghali | 593 | } |
594 | } |
||
595 | } |
||
23709 | jghali | 596 | } |
22017 | jghali | 597 | |
23709 | jghali | 598 | // 2. remove spaces from glyphs with the following CJK attributes |
599 | if (lastChar + 1 < m_story.length()) |
||
600 | { |
||
22017 | jghali | 601 | if (currStat != 0) |
602 | { // current char is CJK |
||
23709 | jghali | 603 | double halfEM = run.style().fontSize() / 10 / 2; |
604 | int nextStat = SpecialChars::getCJKAttr(m_story.text(lastChar + 1)); |
||
22724 | jghali | 605 | switch (currStat & SpecialChars::CJK_CHAR_MASK) |
606 | { |
||
22017 | jghali | 607 | case SpecialChars::CJK_FENCE_END: |
22724 | jghali | 608 | switch (nextStat & SpecialChars::CJK_CHAR_MASK) |
609 | { |
||
610 | case SpecialChars::CJK_FENCE_BEGIN: |
||
611 | case SpecialChars::CJK_FENCE_END: |
||
612 | case SpecialChars::CJK_COMMA: |
||
613 | case SpecialChars::CJK_PERIOD: |
||
614 | case SpecialChars::CJK_MIDPOINT: |
||
615 | run.extraWidth -= halfEM; |
||
616 | } |
||
617 | break; |
||
618 | |||
22017 | jghali | 619 | case SpecialChars::CJK_COMMA: |
620 | case SpecialChars::CJK_PERIOD: |
||
22724 | jghali | 621 | switch (nextStat & SpecialChars::CJK_CHAR_MASK) |
622 | { |
||
623 | case SpecialChars::CJK_FENCE_BEGIN: |
||
624 | case SpecialChars::CJK_FENCE_END: |
||
625 | run.extraWidth -= halfEM; |
||
626 | } |
||
627 | break; |
||
628 | |||
22017 | jghali | 629 | case SpecialChars::CJK_MIDPOINT: |
22724 | jghali | 630 | switch (nextStat & SpecialChars::CJK_CHAR_MASK) |
631 | { |
||
632 | case SpecialChars::CJK_FENCE_BEGIN: |
||
633 | run.extraWidth -= halfEM; |
||
634 | } |
||
635 | break; |
||
636 | |||
22017 | jghali | 637 | case SpecialChars::CJK_FENCE_BEGIN: |
22724 | jghali | 638 | if ((prevStat & SpecialChars::CJK_CHAR_MASK) == SpecialChars::CJK_FENCE_BEGIN) |
639 | { |
||
640 | run.extraWidth -= halfEM; |
||
641 | run.xoffset -= halfEM; |
||
642 | } |
||
643 | else |
||
644 | { |
||
645 | run.setFlag(ScLayout_CJKFence); |
||
646 | } |
||
647 | break; |
||
22017 | jghali | 648 | } |
649 | } |
||
650 | } |
||
651 | |||
652 | result.glyphs().append(run); |
||
653 | } |
||
654 | hb_buffer_destroy(hbBuffer); |
||
655 | |||
656 | } |
||
657 | |||
658 | m_textMap.clear(); |
||
659 | m_text = ""; |
||
660 | result.needsContext(m_contextNeeded); |
||
661 | return result; |
||
662 | } |