View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0017867 | Scribus | Typography | public | 2026-07-01 15:30 | 2026-08-08 11:57 |
| Reporter | qirat | Assigned To | |||
| Priority | normal | Severity | feature | Reproducibility | N/A |
| Status | new | Resolution | open | ||
| Platform | Linux | OS | Fedora Workstation | OS Version | 44 |
| Product Version | 1.7.4.svn | ||||
| Summary | 0017867: [Feature Request] Script-specific font preferences to prevent missing glyphs in mixed-script text | ||||
| Description | Add an option to assign default fonts for specific scripts (e.g., Latin, Devanagari, CJK) individually. When pasting mixed-script text, the app should automatically render each character using the user-defined font for that script. This will help to display complex characters correctly (especially for mixed-script text) and eliminates "missing glyph" boxes without requiring manual font changes. | ||||
| Steps To Reproduce | Maybe two font preferences categories as per script like in LibreOffice (Edit Para Style > Font): Western, Complex/Asian | ||||
| Tags | No tags attached. | ||||
| Patch | No | ||||
|
|
I'm not sure that the suggested behavior is a good one for Scribus. I think that the starting point should be: what is the correct behavior when pasting text into Scribus? The case of the scripts that are only correctly rendered in fewer fonts should be compatible with the general behavior. First, one should take into consideration the characteristics of the source text formatting: - Pasting pure text with no formatting - Pasting text with direct formatting (but no style information) - Pasting text with named styles (and possibly also direct formatting on top of it). (I guess that, for the specific case, we also need to find out if the snipped being pasted contains information about the language and/or script of the text) (Probably, the user should also have a way to decide how to pick one of the mode, and overrule the automatic detection. If possible, without being presented a pop up on each paste.) On the other side, we have the state of the text frame where the paste action occurs: - Does the user want to use the styles that have already been applied to the frame (or to the cursor position in the frame) - Does the user want to use the direct formatting at the cursor position (included the font currently active?) - Does the user want some style replacement to happen? Should only part of the direct formatting be taken into account? (As an example, copying into a frame with the correct language some text from a web page that does not have the language correctly set should retain the frame's language.) All this being taken into account, I guess that Scribus should get a switch (per document and/or global) to enable the advanced "international" features. (Because the "western" are lazy and most of them will never be faced with the need to choose the binding direction or having to define different fonts per script...). And then, the style definition should have the option to define different character styles (font, size...) for each script. Also, I'm not sure if looking at the language (as defined in Scribus) is enough to detect the script to be used: we might need to extend that too... As a person who has never written anything in any non Latin script languages (except having fun with Egyptian hieroglyphs in secondary school...), I will need somebody (like you qirat) who checks, if what I'm writing makes sense : - ) |
|
|
Thanks, this makes sense. Just to clarify: I did not mean this as paste behaviour only. I used pasting mixed-script text only as an example which I now think was a poor one given the scope of the feature I am talking about. So, one can already define a character style for a specific script, but that still needs manual application to each instance. What I wanted to suggest is an option inside Paragraph Styles for script-based fallback fonts. Maybe it does not need many fallback slots. In most cases, the text will use two scripts at max (Latin plus one other script). So it could go something like: * Main script font: Source Serif * Second script fallback font: Gurmukhi — Noto Serif Gurmukhi Then, if a paragraph contains English + Punjabi, Scribus would automatically use the main font for Latin and the fallback font for Gurmukhi _inside the same paragraph_. For users working with more scripts, character styles can still be used manually for the less common cases in the text. |
|
|
[PATCH] Hello, everyone. Inspired with LO (which has much more than I understand), I took a chance at it (using AI) and here is a "nicely" working prototype as per my testing (with Punjabi, English and Hindi, and some others I don't speak). FEATURE(S) - set fallback fonts for 2 more scripts (alongside the main one from "Character Style" tab) - makes possible to just change keyboard mid typing and keep typing second script - pasted mixed-script text is also handled well See clips and screenshots below. fallback-fonts-v4.03-4.patch (63,353 bytes)
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp (revision 27767)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp (working copy)
@@ -3203,6 +3203,30 @@
if (attrs.hasAttribute(HyphenConsecutiveLines))
newStyle.setHyphenConsecutiveLines(attrs.valueAsInt(HyphenConsecutiveLines));
+ static const QString FallbackFontScript1("FallbackFontScript1");
+ if (attrs.hasAttribute(FallbackFontScript1))
+ newStyle.setFallbackFontScript1(attrs.valueAsString(FallbackFontScript1));
+
+ static const QString FallbackFont1("FallbackFont1");
+ if (attrs.hasAttribute(FallbackFont1))
+ newStyle.setFallbackFont1(attrs.valueAsString(FallbackFont1));
+
+ static const QString FallbackFontSize1("FallbackFontSize1");
+ if (attrs.hasAttribute(FallbackFontSize1))
+ newStyle.setFallbackFontSize1(attrs.valueAsDouble(FallbackFontSize1));
+
+ static const QString FallbackFontScript2("FallbackFontScript2");
+ if (attrs.hasAttribute(FallbackFontScript2))
+ newStyle.setFallbackFontScript2(attrs.valueAsString(FallbackFontScript2));
+
+ static const QString FallbackFont2("FallbackFont2");
+ if (attrs.hasAttribute(FallbackFont2))
+ newStyle.setFallbackFont2(attrs.valueAsString(FallbackFont2));
+
+ static const QString FallbackFontSize2("FallbackFontSize2");
+ if (attrs.hasAttribute(FallbackFontSize2))
+ newStyle.setFallbackFontSize2(attrs.valueAsDouble(FallbackFontSize2));
+
static const QString HyphenationMode("HyphenationMode");
if (attrs.hasAttribute(HyphenationMode))
newStyle.setHyphenationMode(attrs.valueAsInt(HyphenationMode));
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp (revision 27767)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp (working copy)
@@ -876,6 +876,18 @@
docu.writeAttribute("OpticalMargins", style.opticalMargins());
if ( ! style.isInhHyphenConsecutiveLines())
docu.writeAttribute("HyphenConsecutiveLines", style.hyphenConsecutiveLines());
+ if (!style.isInhFallbackFontScript1())
+ docu.writeAttribute("FallbackFontScript1", style.fallbackFontScript1());
+ if (!style.isInhFallbackFont1())
+ docu.writeAttribute("FallbackFont1", style.fallbackFont1());
+ if (!style.isInhFallbackFontSize1())
+ docu.writeAttribute("FallbackFontSize1", style.fallbackFontSize1());
+ if (!style.isInhFallbackFontScript2())
+ docu.writeAttribute("FallbackFontScript2", style.fallbackFontScript2());
+ if (!style.isInhFallbackFont2())
+ docu.writeAttribute("FallbackFont2", style.fallbackFont2());
+ if (!style.isInhFallbackFontSize2())
+ docu.writeAttribute("FallbackFontSize2", style.fallbackFontSize2());
if ( ! style.isInhHyphenationMode())
docu.writeAttribute("HyphenationMode", style.hyphenationMode());
if ( ! style.isInhMinWordTracking())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp (revision 27767)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp (working copy)
@@ -3215,6 +3215,30 @@
if (attrs.hasAttribute(HyphenConsecutiveLines))
newStyle.setHyphenConsecutiveLines(attrs.valueAsInt(HyphenConsecutiveLines));
+ static const QString FallbackFontScript1("FallbackFontScript1");
+ if (attrs.hasAttribute(FallbackFontScript1))
+ newStyle.setFallbackFontScript1(attrs.valueAsString(FallbackFontScript1));
+
+ static const QString FallbackFont1("FallbackFont1");
+ if (attrs.hasAttribute(FallbackFont1))
+ newStyle.setFallbackFont1(attrs.valueAsString(FallbackFont1));
+
+ static const QString FallbackFontSize1("FallbackFontSize1");
+ if (attrs.hasAttribute(FallbackFontSize1))
+ newStyle.setFallbackFontSize1(attrs.valueAsDouble(FallbackFontSize1));
+
+ static const QString FallbackFontScript2("FallbackFontScript2");
+ if (attrs.hasAttribute(FallbackFontScript2))
+ newStyle.setFallbackFontScript2(attrs.valueAsString(FallbackFontScript2));
+
+ static const QString FallbackFont2("FallbackFont2");
+ if (attrs.hasAttribute(FallbackFont2))
+ newStyle.setFallbackFont2(attrs.valueAsString(FallbackFont2));
+
+ static const QString FallbackFontSize2("FallbackFontSize2");
+ if (attrs.hasAttribute(FallbackFontSize2))
+ newStyle.setFallbackFontSize2(attrs.valueAsDouble(FallbackFontSize2));
+
static const QString HyphenationMode("HyphenationMode");
if (attrs.hasAttribute(HyphenationMode))
newStyle.setHyphenationMode(attrs.valueAsInt(HyphenationMode));
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp (revision 27767)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp (working copy)
@@ -880,6 +880,18 @@
docu.writeAttribute("OpticalMarginSetId", style.opticalMarginSetId());
if (!style.isInhHyphenConsecutiveLines())
docu.writeAttribute("HyphenConsecutiveLines", style.hyphenConsecutiveLines());
+ if (!style.isInhFallbackFontScript1())
+ docu.writeAttribute("FallbackFontScript1", style.fallbackFontScript1());
+ if (!style.isInhFallbackFont1())
+ docu.writeAttribute("FallbackFont1", style.fallbackFont1());
+ if (!style.isInhFallbackFontSize1())
+ docu.writeAttribute("FallbackFontSize1", style.fallbackFontSize1());
+ if (!style.isInhFallbackFontScript2())
+ docu.writeAttribute("FallbackFontScript2", style.fallbackFontScript2());
+ if (!style.isInhFallbackFont2())
+ docu.writeAttribute("FallbackFont2", style.fallbackFont2());
+ if (!style.isInhFallbackFontSize2())
+ docu.writeAttribute("FallbackFontSize2", style.fallbackFontSize2());
if (!style.isInhHyphenationMode())
docu.writeAttribute("HyphenationMode", style.hyphenationMode());
if (!style.isInhMinWordTracking())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp (revision 27767)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp (working copy)
@@ -3725,6 +3725,30 @@
if (attrs.hasAttribute(HyphenConsecutiveLines))
newStyle.setHyphenConsecutiveLines(attrs.valueAsInt(HyphenConsecutiveLines));
+ static const QString FallbackFontScript1("FallbackFontScript1");
+ if (attrs.hasAttribute(FallbackFontScript1))
+ newStyle.setFallbackFontScript1(attrs.valueAsString(FallbackFontScript1));
+
+ static const QString FallbackFont1("FallbackFont1");
+ if (attrs.hasAttribute(FallbackFont1))
+ newStyle.setFallbackFont1(attrs.valueAsString(FallbackFont1));
+
+ static const QString FallbackFontSize1("FallbackFontSize1");
+ if (attrs.hasAttribute(FallbackFontSize1))
+ newStyle.setFallbackFontSize1(attrs.valueAsDouble(FallbackFontSize1));
+
+ static const QString FallbackFontScript2("FallbackFontScript2");
+ if (attrs.hasAttribute(FallbackFontScript2))
+ newStyle.setFallbackFontScript2(attrs.valueAsString(FallbackFontScript2));
+
+ static const QString FallbackFont2("FallbackFont2");
+ if (attrs.hasAttribute(FallbackFont2))
+ newStyle.setFallbackFont2(attrs.valueAsString(FallbackFont2));
+
+ static const QString FallbackFontSize2("FallbackFontSize2");
+ if (attrs.hasAttribute(FallbackFontSize2))
+ newStyle.setFallbackFontSize2(attrs.valueAsDouble(FallbackFontSize2));
+
static const QString HyphenationMode("HyphenationMode");
if (attrs.hasAttribute(HyphenationMode))
newStyle.setHyphenationMode(attrs.valueAsInt(HyphenationMode));
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp (revision 27767)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp (working copy)
@@ -882,6 +882,18 @@
docu.writeAttribute("OpticalMarginSetId", style.opticalMarginSetId());
if (!style.isInhHyphenConsecutiveLines())
docu.writeAttribute("HyphenConsecutiveLines", style.hyphenConsecutiveLines());
+ if (!style.isInhFallbackFontScript1())
+ docu.writeAttribute("FallbackFontScript1", style.fallbackFontScript1());
+ if (!style.isInhFallbackFont1())
+ docu.writeAttribute("FallbackFont1", style.fallbackFont1());
+ if (!style.isInhFallbackFontSize1())
+ docu.writeAttribute("FallbackFontSize1", style.fallbackFontSize1());
+ if (!style.isInhFallbackFontScript2())
+ docu.writeAttribute("FallbackFontScript2", style.fallbackFontScript2());
+ if (!style.isInhFallbackFont2())
+ docu.writeAttribute("FallbackFont2", style.fallbackFont2());
+ if (!style.isInhFallbackFontSize2())
+ docu.writeAttribute("FallbackFontSize2", style.fallbackFontSize2());
if (!style.isInhHyphenationMode())
docu.writeAttribute("HyphenationMode", style.hyphenationMode());
if (!style.isInhMinWordTracking())
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx (revision 27767)
+++ scribus/styles/paragraphstyle.attrdefs.cxx (working copy)
@@ -58,3 +58,19 @@
ATTRDEF(double, backgroundShade, BackgroundShade, 100)
ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
ATTRDEF(QString, opticalMarginSetId, OpticalMarginSetId, OpticalMarginLookup::instance().defaultSetId())
+// Fallback fonts used per-script when the paragraph's main/character font
+// cannot render a given character (e.g. mixed Latin + CJK/Complex text).
+// Each slot stores a *specific* script identifier (e.g. "Gurmukhi",
+// "Devanagari", "Han"; see ParagraphStyle::fallbackScripts()) rather than
+// a broad script category ("Indic", "CJK", ...), so that two slots can be
+// assigned to two different scripts of the same broad category without
+// colliding. The defaults below reflect that: Slot 1 defaults to Gurmukhi
+// and Slot 2 to Devanagari, two different scripts of the same "Indic"
+// category. FontSize is a percentage of the character's own point size
+// (100 = same size as the surrounding text).
+ATTRDEF(QString, fallbackFontScript1, FallbackFontScript1, "Gurmukhi")
+ATTRDEF(QString, fallbackFont1, FallbackFont1, "")
+ATTRDEF(double, fallbackFontSize1, FallbackFontSize1, 100)
+ATTRDEF(QString, fallbackFontScript2, FallbackFontScript2, "Devanagari")
+ATTRDEF(QString, fallbackFont2, FallbackFont2, "")
+ATTRDEF(double, fallbackFontSize2, FallbackFontSize2, 100)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp (revision 27767)
+++ scribus/styles/paragraphstyle.cpp (working copy)
@@ -14,6 +14,8 @@
***************************************************************************/
#include <QDebug>
+#include <unicode/uscript.h>
+
#include "commonstrings.h"
#include "styles/style.h"
#include "paragraphstyle.h"
@@ -22,6 +24,90 @@
#include "desaxe/simple_actions.h"
#include "util_math.h"
+namespace {
+
+// Describes one entry offered in the "Fallback Fonts" UI: a specific,
+// selectable script (identified by a stable, untranslated technical id)
+// together with the broader category it is grouped under. Categories
+// exist purely for UI grouping and for keying a generic default font
+// per category; matching a character against a fallback font slot is
+// always done using the specific script id, never the category, so
+// that e.g. Gurmukhi and Devanagari (both "Indic") can be assigned to
+// two different slots without colliding.
+struct ScriptInfo
+{
+ UScriptCode code;
+ const char* id;
+ const char* category;
+};
+
+// Within each region block (see fallbackScriptRegion() in smpstylewidget.cpp),
+// entries are kept in alphabetical order by their display name, so that the
+// grouped script combo boxes in the UI are alphabetical purely by iterating
+// this table in order, with no extra sorting step required there.
+const ScriptInfo scriptTable[] = {
+ { USCRIPT_CYRILLIC, "Cyrillic", "Cyrillic" },
+ { USCRIPT_GREEK, "Greek", "Greek" },
+ { USCRIPT_LATIN, "Latin", "Latin" },
+ { USCRIPT_ARABIC, "Arabic", "Arabic" },
+ { USCRIPT_HEBREW, "Hebrew", "Hebrew" },
+ { USCRIPT_SYRIAC, "Syriac", "Arabic" },
+ { USCRIPT_BOPOMOFO, "Bopomofo", "CJK" },
+ { USCRIPT_HAN, "Han", "CJK" },
+ { USCRIPT_HANGUL, "Hangul", "CJK" },
+ { USCRIPT_HIRAGANA, "Hiragana", "CJK" },
+ { USCRIPT_KATAKANA, "Katakana", "CJK" },
+ { USCRIPT_BENGALI, "Bengali", "Indic" },
+ { USCRIPT_DEVANAGARI, "Devanagari", "Indic" },
+ { USCRIPT_GUJARATI, "Gujarati", "Indic" },
+ { USCRIPT_GURMUKHI, "Gurmukhi", "Indic" },
+ { USCRIPT_KANNADA, "Kannada", "Indic" },
+ { USCRIPT_MALAYALAM, "Malayalam", "Indic" },
+ { USCRIPT_ORIYA, "Oriya", "Indic" },
+ { USCRIPT_SINHALA, "Sinhala", "Indic" },
+ { USCRIPT_TAMIL, "Tamil", "Indic" },
+ { USCRIPT_TELUGU, "Telugu", "Indic" },
+ { USCRIPT_KHMER, "Khmer", "SoutheastAsian" },
+ { USCRIPT_LAO, "Lao", "SoutheastAsian" },
+ { USCRIPT_MYANMAR, "Myanmar", "SoutheastAsian" },
+ { USCRIPT_NEW_TAI_LUE, "NewTaiLue", "SoutheastAsian" },
+ { USCRIPT_TAI_LE, "TaiLe", "SoutheastAsian" },
+ { USCRIPT_TAI_VIET, "TaiViet", "SoutheastAsian" },
+ { USCRIPT_THAI, "Thai", "SoutheastAsian" },
+};
+const int scriptTableSize = static_cast<int>(sizeof(scriptTable) / sizeof(scriptTable[0]));
+
+// Catch-all entry used for scripts that are not explicitly listed above
+// but do carry script-specific shaping needs (e.g. Armenian, Georgian,
+// Ethiopic, ...). Script-neutral codes (punctuation, digits, combining
+// marks, ...) are handled separately and never reach this catch-all.
+const char* otherScriptId = "Other";
+const char* otherCategory = "Other";
+
+// Looks up the table entry for an ICU script code, if any.
+const ScriptInfo* findScript(UScriptCode sc)
+{
+ for (int i = 0; i < scriptTableSize; ++i)
+ {
+ if (scriptTable[i].code == sc)
+ return &scriptTable[i];
+ }
+ return nullptr;
+}
+
+// Looks up the table entry for a stored script identifier, if any.
+const ScriptInfo* findScript(const QString& scriptId)
+{
+ for (int i = 0; i < scriptTableSize; ++i)
+ {
+ if (scriptId == QLatin1String(scriptTable[i].id))
+ return &scriptTable[i];
+ }
+ return nullptr;
+}
+
+} // namespace
+
bool ParagraphStyle::TabRecord::operator==(const TabRecord& other) const
{
return isequiv(tabPosition, other.tabPosition) && tabType == other.tabType && tabFillChar == other.tabFillChar;
@@ -256,6 +342,97 @@
}
+QStringList ParagraphStyle::fallbackScripts()
+{
+ // Identifiers only (technical, not translated); the UI is responsible
+ // for mapping these to translated, user-facing labels.
+ static QStringList scripts;
+ if (scripts.isEmpty())
+ {
+ for (int i = 0; i < scriptTableSize; ++i)
+ scripts << QString::fromLatin1(scriptTable[i].id);
+ scripts << QString::fromLatin1(otherScriptId);
+ }
+ return scripts;
+}
+
+QStringList ParagraphStyle::fallbackScriptCategories()
+{
+ static const QStringList categories = {
+ QStringLiteral("Latin"),
+ QStringLiteral("Cyrillic"),
+ QStringLiteral("Greek"),
+ QStringLiteral("Hebrew"),
+ QStringLiteral("Arabic"),
+ QStringLiteral("CJK"),
+ QStringLiteral("Indic"),
+ QStringLiteral("SoutheastAsian"),
+ QStringLiteral("Other")
+ };
+ return categories;
+}
+
+QString ParagraphStyle::categoryForScript(const QString& scriptId)
+{
+ if (scriptId == QLatin1String(otherScriptId))
+ return QString::fromLatin1(otherCategory);
+ const ScriptInfo* info = findScript(scriptId);
+ if (info)
+ return QString::fromLatin1(info->category);
+ return QString();
+}
+
+QString ParagraphStyle::scriptIdForChar(QChar ch)
+{
+ UErrorCode err = U_ZERO_ERROR;
+ UScriptCode sc = uscript_getScript(static_cast<UChar32>(ch.unicode()), &err);
+ if (U_FAILURE(err))
+ return QString();
+ if (sc == USCRIPT_COMMON || sc == USCRIPT_INHERITED || sc == USCRIPT_UNKNOWN)
+ return QString();
+ const ScriptInfo* info = findScript(sc);
+ if (info)
+ return QString::fromLatin1(info->id);
+ return QString::fromLatin1(otherScriptId);
+}
+
+QString ParagraphStyle::scriptGroupForChar(QChar ch)
+{
+ UErrorCode err = U_ZERO_ERROR;
+ UScriptCode sc = uscript_getScript(static_cast<UChar32>(ch.unicode()), &err);
+ if (U_FAILURE(err))
+ return QString();
+ if (sc == USCRIPT_COMMON || sc == USCRIPT_INHERITED || sc == USCRIPT_UNKNOWN)
+ return QString();
+ const ScriptInfo* info = findScript(sc);
+ if (info)
+ return QString::fromLatin1(info->category);
+ return QString::fromLatin1(otherCategory);
+}
+
+ParagraphStyle::FallbackFontInfo ParagraphStyle::fallbackFontForChar(QChar ch) const
+{
+ FallbackFontInfo result;
+
+ QString scriptId = scriptIdForChar(ch);
+ if (scriptId.isEmpty())
+ return result;
+
+ if (!fallbackFont1().isEmpty() && scriptId == fallbackFontScript1())
+ {
+ result.fontName = fallbackFont1();
+ result.sizePercent = fallbackFontSize1();
+ return result;
+ }
+ if (!fallbackFont2().isEmpty() && scriptId == fallbackFontScript2())
+ {
+ result.fontName = fallbackFont2();
+ result.sizePercent = fallbackFontSize2();
+ return result;
+ }
+ return result;
+}
+
static QString toXMLString(ParagraphStyle::AlignmentType val)
{
return QString::number(static_cast<int>(val));
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h (revision 27767)
+++ scribus/styles/paragraphstyle.h (working copy)
@@ -168,7 +168,52 @@
#include "paragraphstyle.attrdefs.cxx"
#undef ATTRDEF
-
+ /** Fallback fonts for mixed-script text. */
+
+ /** Result of a fallback-font lookup for a given character: the font
+ * to use and its size, expressed as a percentage of the character's
+ * own point size (100 = same size). fontName is empty if no
+ * fallback is configured/needed for that character.
+ */
+ struct FallbackFontInfo
+ {
+ QString fontName;
+ double sizePercent = 100.0;
+ };
+
+ /** Returns the list of specific script identifiers ("Latin", "Han",
+ * "Gurmukhi", "Devanagari", ...) that can be assigned to a fallback
+ * font slot, in display order. Each identifier belongs to exactly
+ * one broad category, see ParagraphStyle::categoryForScript().
+ */
+ static QStringList fallbackScripts();
+ /** Returns the broad script-category identifiers ("Latin", "CJK",
+ * "Indic", ...) used to group the fallbackScripts() list, and to key
+ * a generic default/starting font per category in the UI.
+ */
+ static QStringList fallbackScriptCategories();
+ /** Returns the broad category ("Latin", "CJK", "Indic", ...) that
+ * script identifier \a scriptId belongs to, or an empty string if
+ * \a scriptId is not a known identifier.
+ */
+ static QString categoryForScript(const QString& scriptId);
+ /** Returns the specific script identifier (e.g. "Gurmukhi") that
+ * character \a ch belongs to, or an empty string if the character's
+ * script is common/inherited and does not require a specific
+ * fallback (punctuation, digits, combining marks, ...).
+ */
+ static QString scriptIdForChar(QChar ch);
+ /** Returns the broad category (e.g. "Indic") that character \a ch
+ * belongs to, or an empty string; see scriptIdForChar().
+ */
+ static QString scriptGroupForChar(QChar ch);
+ /** Returns the fallback font (and its size, as a percentage of the
+ * character's own point size) configured for the script that \a ch
+ * belongs to, or an empty-named FallbackFontInfo if none is
+ * configured or applicable.
+ */
+ FallbackFontInfo fallbackFontForChar(QChar ch) const;
+
private:
// member declarations:
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp (revision 27767)
+++ scribus/text/textshaper.cpp (working copy)
@@ -12,6 +12,7 @@
#include "glyphcluster.h"
#include "pageitem.h"
+#include "scfonts.h"
#include "scribusdoc.h"
#include "storytext.h"
#include "styles/paragraphstyle.h"
@@ -167,6 +168,60 @@
return newRuns;
}
+void TextShaper::applyFallbackFont(int pos, const QString& str)
+{
+ if (str.isEmpty())
+ return;
+ if (m_context == nullptr || m_context->getDoc() == nullptr)
+ return;
+
+ const ScribusDoc* doc = m_context->getDoc();
+ if (doc->AllFonts == nullptr)
+ return;
+
+ // Only look at the first "real" character of this position; control
+ // characters, spaces and object placeholders never need a fallback
+ // and are cheap to skip before doing any script lookup.
+ QChar ch = str.at(0);
+ if (ch.isSpace() || ch.category() == QChar::Other_Control)
+ return;
+ if (ch == SpecialChars::OBJECT || ch == SpecialChars::PARSEP ||
+ ch == SpecialChars::LINEBREAK || ch == SpecialChars::FRAMEBREAK ||
+ ch == SpecialChars::COLBREAK || ch == SpecialChars::ZWNBSPACE)
+ return;
+
+ const CharStyle& charStyle = m_story.charStyle(pos);
+ const ScFace& primaryFont = charStyle.font();
+ if (primaryFont.canRender(ch))
+ return; // primary font already covers this character, nothing to do
+
+ const ParagraphStyle& parStyle = m_story.paragraphStyle(pos);
+ ParagraphStyle::FallbackFontInfo fallback = parStyle.fallbackFontForChar(ch);
+ if (fallback.fontName.isEmpty() || fallback.fontName == primaryFont.scName())
+ return;
+
+ if (!doc->AllFonts->contains(fallback.fontName))
+ return;
+ const ScFace& fallbackFace = (*doc->AllFonts)[fallback.fontName];
+ if (!fallbackFace.usable() || !fallbackFace.canRender(ch))
+ return; // fallback font can't help either; leave as-is (shows as missing glyph)
+
+ // Persist the substitution (font + size) onto the character style at
+ // this position, the same way automatic paragraph-effect styling (drop
+ // caps, bullets, numbering) is applied above. This keeps shaping and
+ // later rendering/PDF export in sync, since both read the font/size
+ // from the stored CharStyle rather than from a value private to the
+ // shaper.
+ CharStyle newStyle(charStyle);
+ newStyle.setFont(fallbackFace);
+ if (fallback.sizePercent != 100.0)
+ {
+ double newSize = charStyle.fontSize() * (fallback.sizePercent / 100.0);
+ newStyle.setFontSize(qMax(1.0, newSize));
+ }
+ m_story.setCharStyle(pos, 1, newStyle);
+}
+
void TextShaper::buildText(int fromPos, int toPos, QVector<int>& smallCaps)
{
m_text.clear();
@@ -237,6 +292,8 @@
}
}
+ applyFallbackFont(i, str);
+
const CharStyle &style = m_story.charStyle(i);
int effects = style.effects() & ScStyle_UserStyles;
bool hasSmallCap = false;
Index: scribus/text/textshaper.h
===================================================================
--- scribus/text/textshaper.h (revision 27767)
+++ scribus/text/textshaper.h (working copy)
@@ -55,6 +55,16 @@
void buildText(int fromPos, int toPos, QVector<int>& smallCaps);
+ /// Automatic fallback-font substitution for mixed-script text: if the
+ /// character style's own font cannot render the character(s) at
+ /// position \a pos, and the paragraph style at that position has a
+ /// fallback font configured for the character's specific script,
+ /// persist a character style override with that fallback font (and
+ /// its configured size) so that both shaping and later
+ /// rendering/export use a consistent, glyph-capable font instead of
+ /// showing a missing-glyph box.
+ void applyFallbackFont(int pos, const QString& str);
+
QList<TextRun> itemizeBiDi(int fromPos) const;
QList<TextRun> itemizeScripts(const QList<TextRun> &runs) const;
QList<TextRun> itemizeStyles(const QList<TextRun> &runs) const;
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp (revision 27767)
+++ scribus/ui/smpstylewidget.cpp (working copy)
@@ -6,11 +6,19 @@
*/
#include <QEvent>
+#include <QFont>
+#include <QIcon>
+#include <QPainter>
+#include <QPixmap>
+#include <QStandardItemModel>
#include "iconmanager.h"
#include "numeration.h"
#include "smpstylewidget.h"
+#include "prefsmanager.h"
+#include "prefscontext.h"
+#include "prefsfile.h"
#include "scribus.h"
#include "scribusapp.h"
#include "units.h"
@@ -81,6 +89,18 @@
minGlyphExtSpin->setSuffix(unitGetSuffixFromIndex(SC_PERCENT));
maxGlyphExtSpin->setSuffix(unitGetSuffixFromIndex(SC_PERCENT));
+ fallbackFontSizeSpin1->setSuffix(unitGetSuffixFromIndex(SC_PERCENT));
+ fallbackFontSizeSpin1->setRange(10.0, 400.0);
+ fallbackFontSizeSpin2->setSuffix(unitGetSuffixFromIndex(SC_PERCENT));
+ fallbackFontSizeSpin2->setRange(10.0, 400.0);
+ fillFallbackScriptCombo(fallbackScriptCombo1);
+ fillFallbackScriptCombo(fallbackScriptCombo2);
+
+ // Dock the fallback font size next to the style combo, inside the
+ // font combo's own row, so style and size share a single row.
+ fallbackFontCombo1->setGuestWidget(fallbackFontSizeLabel1);
+ fallbackFontCombo2->setGuestWidget(fallbackFontSizeLabel2);
+
connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
if (m_Doc)
@@ -123,6 +143,11 @@
peLabel->setPixmap(iconManager.loadPixmap("paragraph-effects-style"));
parentParEffectsButton->setIcon(iconManager.loadIcon("reset-style-changes"));
backgroundColor->setPixmap(iconManager.loadPixmap("color-fill"));
+
+ fallbackScriptLabel1->setPixmap(iconManager.loadPixmap("paragraph-language"));
+ fallbackScriptLabel2->setPixmap(iconManager.loadPixmap("paragraph-language"));
+ fallbackFontSizeLabel1->setPixmap(iconManager.loadPixmap("font-size"));
+ fallbackFontSizeLabel2->setPixmap(iconManager.loadPixmap("font-size"));
}
void SMPStyleWidget::languageChange()
@@ -181,6 +206,12 @@
cpage->setDoc(m_Doc);
backgroundColor->colorButton->setDoc(m_Doc);
+
+ if (m_Doc)
+ {
+ fallbackFontCombo1->rebuildList(m_Doc);
+ fallbackFontCombo2->rebuildList(m_Doc);
+ }
}
void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles, QList<CharStyle> &cstyles, int unitIndex, const QString &defLang)
@@ -422,6 +453,12 @@
cpage->parentLabel->setText( tr("Based On:"));
cpage->show(&pstyle->charStyle(), cstyles, defLang, unitIndex);
+ {
+ QList<ParagraphStyle*> singleStyle;
+ singleStyle << pstyle;
+ showFallbackFonts(singleStyle);
+ }
+
parentCombo->clear();
parentCombo->addItem( pstyle->isDefaultStyle()? tr("A default style cannot be assigned a parent style") : "");
if (!pstyle->isDefaultStyle())
@@ -467,6 +504,7 @@
showMinGlyphExt(pstyles);
showMaxGlyphExt(pstyles);
showConsecutiveLines(pstyles);
+ showFallbackFonts(pstyles);
showTabs(pstyles, unitIndex);
showCStyle(pstyles, cstyles, defLang, unitIndex);
showParent(pstyles);
@@ -746,6 +784,58 @@
}
+void SMPStyleWidget::showFallbackFonts(const QList<ParagraphStyle*> &pstyles)
+{
+ if (pstyles.isEmpty())
+ {
+ qDebug() << "Warning showFallbackFonts called with an empty list of styles";
+ return;
+ }
+
+ QSignalBlocker sbScript1(fallbackScriptCombo1);
+ QSignalBlocker sbFont1(fallbackFontCombo1);
+ QSignalBlocker sbSize1(fallbackFontSizeSpin1);
+ QSignalBlocker sbScript2(fallbackScriptCombo2);
+ QSignalBlocker sbFont2(fallbackFontCombo2);
+ QSignalBlocker sbSize2(fallbackFontSizeSpin2);
+
+ QString script1 = pstyles[0]->fallbackFontScript1();
+ QString font1 = pstyles[0]->fallbackFont1();
+ double size1 = pstyles[0]->fallbackFontSize1();
+ QString script2 = pstyles[0]->fallbackFontScript2();
+ QString font2 = pstyles[0]->fallbackFont2();
+ double size2 = pstyles[0]->fallbackFontSize2();
+ bool mixedSize1 = false, mixedSize2 = false;
+ for (int i = 0; i < pstyles.count(); ++i)
+ {
+ if (script1 != pstyles[i]->fallbackFontScript1())
+ script1.clear();
+ if (font1 != pstyles[i]->fallbackFont1())
+ font1.clear();
+ if (!isEqual(size1, pstyles[i]->fallbackFontSize1()))
+ mixedSize1 = true;
+ if (script2 != pstyles[i]->fallbackFontScript2())
+ script2.clear();
+ if (font2 != pstyles[i]->fallbackFont2())
+ font2.clear();
+ if (!isEqual(size2, pstyles[i]->fallbackFontSize2()))
+ mixedSize2 = true;
+ }
+
+ // Index 0 of each combo is always a disabled region heading (see
+ // fillFallbackScriptCombo()), so an unmatched/unset script falls
+ // back to index 1, the first selectable script entry.
+ int idx1 = fallbackScriptCombo1->findData(script1);
+ fallbackScriptCombo1->setCurrentIndex(idx1 >= 1 ? idx1 : 1);
+ fallbackFontCombo1->setCurrentFont(font1);
+ fallbackFontSizeSpin1->setValue(mixedSize1 ? 100.0 : size1);
+
+ int idx2 = fallbackScriptCombo2->findData(script2);
+ fallbackScriptCombo2->setCurrentIndex(idx2 >= 1 ? idx2 : 1);
+ fallbackFontCombo2->setCurrentFont(font2);
+ fallbackFontSizeSpin2->setValue(mixedSize2 ? 100.0 : size2);
+}
+
void SMPStyleWidget::showTabs(const QList<ParagraphStyle*> &pstyles, int unitIndex)
{
@@ -1000,6 +1090,292 @@
peCombo->setCurrentIndex(currIndex);
}
+static QIcon fallbackScriptGlyphIcon(const QString& scriptId, const QColor& textColor)
+{
+ // Renders a small representative glyph for the script into an icon, so
+ // the script combo gets an icon column and lines up visually with the
+ // font-family combo next to it. This only needs a glyph to exist in
+ // *some* font on the system (Qt's font-matching will substitute one if
+ // the application font lacks it); worst case a given entry falls back
+ // to the ".notdef" box glyph, which is still a harmless, distinct icon.
+ static const QMap<QString, QString> glyphs {
+ { QStringLiteral("Latin"), QStringLiteral("A") },
+ { QStringLiteral("Cyrillic"), QStringLiteral("\u0416") },
+ { QStringLiteral("Greek"), QStringLiteral("\u03A9") },
+ { QStringLiteral("Hebrew"), QStringLiteral("\u05D0") },
+ { QStringLiteral("Arabic"), QStringLiteral("\u0628") },
+ { QStringLiteral("Syriac"), QStringLiteral("\u0710") },
+ { QStringLiteral("Han"), QStringLiteral("\u6F22") },
+ { QStringLiteral("Hiragana"), QStringLiteral("\u3042") },
+ { QStringLiteral("Katakana"), QStringLiteral("\u30A2") },
+ { QStringLiteral("Hangul"), QStringLiteral("\uD55C") },
+ { QStringLiteral("Bopomofo"), QStringLiteral("\u3105") },
+ { QStringLiteral("Devanagari"), QStringLiteral("\u0905") },
+ { QStringLiteral("Bengali"), QStringLiteral("\u0985") },
+ { QStringLiteral("Gurmukhi"), QStringLiteral("\u0A05") },
+ { QStringLiteral("Gujarati"), QStringLiteral("\u0A85") },
+ { QStringLiteral("Oriya"), QStringLiteral("\u0B05") },
+ { QStringLiteral("Tamil"), QStringLiteral("\u0B85") },
+ { QStringLiteral("Telugu"), QStringLiteral("\u0C05") },
+ { QStringLiteral("Kannada"), QStringLiteral("\u0C85") },
+ { QStringLiteral("Malayalam"), QStringLiteral("\u0D05") },
+ { QStringLiteral("Sinhala"), QStringLiteral("\u0D85") },
+ { QStringLiteral("Thai"), QStringLiteral("\u0E01") },
+ { QStringLiteral("Lao"), QStringLiteral("\u0E81") },
+ { QStringLiteral("Myanmar"), QStringLiteral("\u1000") },
+ { QStringLiteral("Khmer"), QStringLiteral("\u1780") },
+ { QStringLiteral("TaiViet"), QStringLiteral("\uAA80") },
+ { QStringLiteral("NewTaiLue"), QStringLiteral("\u1980") },
+ { QStringLiteral("TaiLe"), QStringLiteral("\u1950") },
+ };
+
+ const QString glyph = glyphs.value(scriptId, QStringLiteral("?"));
+
+ const int size = 16;
+ QPixmap pixmap(size, size);
+ pixmap.fill(Qt::transparent);
+
+ QPainter painter(&pixmap);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setRenderHint(QPainter::TextAntialiasing);
+ QFont font = painter.font();
+ font.setPixelSize(size - 4);
+ painter.setFont(font);
+ painter.setPen(textColor);
+ painter.drawText(pixmap.rect(), Qt::AlignCenter, glyph);
+ painter.end();
+
+ return QIcon(pixmap);
+}
+
+void SMPStyleWidget::fillFallbackScriptCombo(QComboBox* combo)
+{
+ if (!combo)
+ return;
+ QSignalBlocker sb(combo);
+ int currIndex = combo->currentIndex();
+ combo->clear();
+
+ // Group the (already category-ordered, and alphabetically-ordered
+ // within each category) script list one level further, into the
+ // broad geographic regions users actually think in terms of, with
+ // an explicit, non-selectable heading for each region. This is
+ // purely a display grouping; the underlying category identifiers
+ // returned by ParagraphStyle::categoryForScript() are unchanged and
+ // still used to key remembered default fonts. Scripts within each
+ // region appear alphabetically because ParagraphStyle::fallbackScripts()
+ // already returns them in that order (see scriptTable in
+ // paragraphstyle.cpp) — no further sorting is needed here.
+ const QStringList scripts = ParagraphStyle::fallbackScripts();
+ QStandardItemModel* model = qobject_cast<QStandardItemModel*>(combo->model());
+ QString lastRegion;
+ bool first = true;
+ for (const QString& scriptId : scripts)
+ {
+ QString category = ParagraphStyle::categoryForScript(scriptId);
+ QString region = fallbackScriptRegion(category);
+ if (region != lastRegion)
+ {
+ if (!first)
+ combo->insertSeparator(combo->count());
+ combo->addItem(region);
+ if (model)
+ {
+ QStandardItem* headerItem = model->item(combo->count() - 1);
+ if (headerItem)
+ {
+ headerItem->setFlags(headerItem->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled));
+ QFont headerFont = headerItem->font();
+ headerFont.setBold(true);
+ headerItem->setFont(headerFont);
+ }
+ }
+ lastRegion = region;
+ }
+ QString label = fallbackScriptLabel(scriptId);
+ combo->addItem(fallbackScriptGlyphIcon(scriptId, combo->palette().color(QPalette::Text)), label, scriptId);
+ if (model)
+ model->item(combo->count() - 1)->setData(fallbackScriptTooltip(scriptId), Qt::ToolTipRole);
+ first = false;
+ }
+ combo->setCurrentIndex(currIndex >= 1 && currIndex < combo->count() ? currIndex : 1);
+}
+
+QString SMPStyleWidget::fallbackScriptRegion(const QString& category) const
+{
+ // Maps the technical, per-script category (see
+ // ParagraphStyle::categoryForScript()) to the broad geographic
+ // region shown as a group heading in the script combo boxes. This
+ // grouping is UI-only: it has no bearing on the category keys used
+ // to remember a default fallback font.
+ if (category == QLatin1String("Latin") || category == QLatin1String("Cyrillic") || category == QLatin1String("Greek"))
+ return tr("Europe");
+ if (category == QLatin1String("Hebrew") || category == QLatin1String("Arabic"))
+ return tr("Middle East & North Africa");
+ if (category == QLatin1String("CJK"))
+ return tr("East Asia");
+ if (category == QLatin1String("Indic"))
+ return tr("South Asia");
+ if (category == QLatin1String("SoutheastAsian"))
+ return tr("Southeast Asia");
+ return tr("Other Scripts");
+}
+
+QString SMPStyleWidget::fallbackScriptLabel(const QString& scriptId) const
+{
+ // User-facing, translated labels for the technical script identifiers
+ // stored in the paragraph style (see ParagraphStyle::fallbackScripts()).
+ // Deliberately just the script name, with no language names or other
+ // annotations tacked on: which languages a script is used for is shown
+ // in the combo entry's tooltip instead (see fallbackScriptTooltip()).
+ if (scriptId == QLatin1String("Latin"))
+ return tr("Latin");
+ if (scriptId == QLatin1String("Cyrillic"))
+ return tr("Cyrillic");
+ if (scriptId == QLatin1String("Greek"))
+ return tr("Greek");
+ if (scriptId == QLatin1String("Hebrew"))
+ return tr("Hebrew");
+ if (scriptId == QLatin1String("Arabic"))
+ return tr("Arabic");
+ if (scriptId == QLatin1String("Syriac"))
+ return tr("Syriac");
+ if (scriptId == QLatin1String("Han"))
+ return tr("Han");
+ if (scriptId == QLatin1String("Hiragana"))
+ return tr("Hiragana");
+ if (scriptId == QLatin1String("Katakana"))
+ return tr("Katakana");
+ if (scriptId == QLatin1String("Hangul"))
+ return tr("Hangul");
+ if (scriptId == QLatin1String("Bopomofo"))
+ return tr("Bopomofo");
+ if (scriptId == QLatin1String("Devanagari"))
+ return tr("Devanagari");
+ if (scriptId == QLatin1String("Bengali"))
+ return tr("Bengali");
+ if (scriptId == QLatin1String("Gurmukhi"))
+ return tr("Gurmukhi");
+ if (scriptId == QLatin1String("Gujarati"))
+ return tr("Gujarati");
+ if (scriptId == QLatin1String("Oriya"))
+ return tr("Oriya");
+ if (scriptId == QLatin1String("Tamil"))
+ return tr("Tamil");
+ if (scriptId == QLatin1String("Telugu"))
+ return tr("Telugu");
+ if (scriptId == QLatin1String("Kannada"))
+ return tr("Kannada");
+ if (scriptId == QLatin1String("Malayalam"))
+ return tr("Malayalam");
+ if (scriptId == QLatin1String("Sinhala"))
+ return tr("Sinhala");
+ if (scriptId == QLatin1String("Thai"))
+ return tr("Thai");
+ if (scriptId == QLatin1String("Lao"))
+ return tr("Lao");
+ if (scriptId == QLatin1String("Myanmar"))
+ return tr("Myanmar");
+ if (scriptId == QLatin1String("Khmer"))
+ return tr("Khmer");
+ if (scriptId == QLatin1String("TaiViet"))
+ return tr("Tai Viet");
+ if (scriptId == QLatin1String("NewTaiLue"))
+ return tr("New Tai Lue");
+ if (scriptId == QLatin1String("TaiLe"))
+ return tr("Tai Le");
+ return tr("Other");
+}
+
+QString SMPStyleWidget::fallbackScriptTooltip(const QString& scriptId) const
+{
+ // Tooltip shown for each entry in the fallback script combos. For
+ // scripts shared by several languages, this names the languages the
+ // script is used for (since the script name alone doesn't say who
+ // uses it). For scripts that already unambiguously imply a single
+ // language from their name (e.g. Hiragana, Bopomofo), naming that
+ // language again would be redundant, so the tooltip instead gives a
+ // one-sentence description of what the script itself is.
+ if (scriptId == QLatin1String("Latin"))
+ return tr("Used for English and most Western European languages; usually needs no fallback font.");
+ if (scriptId == QLatin1String("Cyrillic"))
+ return tr("Used for Russian, Ukrainian, Bulgarian, Serbian, and other Slavic and Central Asian languages.");
+ if (scriptId == QLatin1String("Greek"))
+ return tr("Used for the Greek language.");
+ if (scriptId == QLatin1String("Hebrew"))
+ return tr("Used for Hebrew and Yiddish.");
+ if (scriptId == QLatin1String("Arabic"))
+ return tr("Used for Arabic, Persian, Urdu, and several other languages.");
+ if (scriptId == QLatin1String("Syriac"))
+ return tr("Used for the Syriac language.");
+ if (scriptId == QLatin1String("Han"))
+ return tr("Chinese characters (Hanzi/Kanji/Hanja), used in written Chinese, Japanese, and Korean.");
+ if (scriptId == QLatin1String("Hiragana"))
+ return tr("A Japanese syllabary used for native words and grammatical elements.");
+ if (scriptId == QLatin1String("Katakana"))
+ return tr("A Japanese syllabary used mainly for foreign loanwords and emphasis.");
+ if (scriptId == QLatin1String("Hangul"))
+ return tr("The Korean alphabet.");
+ if (scriptId == QLatin1String("Bopomofo"))
+ return tr("A phonetic notation system used mainly in Taiwan to teach Mandarin pronunciation.");
+ if (scriptId == QLatin1String("Devanagari"))
+ return tr("Used for Hindi, Marathi, Nepali, Sanskrit, and several other South Asian languages.");
+ if (scriptId == QLatin1String("Bengali"))
+ return tr("Used for Bengali and Assamese.");
+ if (scriptId == QLatin1String("Gurmukhi"))
+ return tr("Used for the Punjabi language.");
+ if (scriptId == QLatin1String("Gujarati"))
+ return tr("Used for the Gujarati language.");
+ if (scriptId == QLatin1String("Oriya"))
+ return tr("Used for the Odia (Oriya) language.");
+ if (scriptId == QLatin1String("Tamil"))
+ return tr("Used for the Tamil language.");
+ if (scriptId == QLatin1String("Telugu"))
+ return tr("Used for the Telugu language.");
+ if (scriptId == QLatin1String("Kannada"))
+ return tr("Used for the Kannada language.");
+ if (scriptId == QLatin1String("Malayalam"))
+ return tr("Used for the Malayalam language.");
+ if (scriptId == QLatin1String("Sinhala"))
+ return tr("Used for the Sinhala language.");
+ if (scriptId == QLatin1String("Thai"))
+ return tr("Used for the Thai language.");
+ if (scriptId == QLatin1String("Lao"))
+ return tr("Used for the Lao language.");
+ if (scriptId == QLatin1String("Myanmar"))
+ return tr("Used for Burmese and other languages of Myanmar.");
+ if (scriptId == QLatin1String("Khmer"))
+ return tr("Used for the Khmer (Cambodian) language.");
+ if (scriptId == QLatin1String("TaiViet"))
+ return tr("Used for Tai Dam and related Tai languages of Vietnam and Laos.");
+ if (scriptId == QLatin1String("NewTaiLue"))
+ return tr("Used for the Tai Lue language.");
+ if (scriptId == QLatin1String("TaiLe"))
+ return tr("Used for the Tai Le language.");
+ return tr("Scripts not covered by the sections above, e.g. Armenian, Georgian, or Ethiopic.");
+}
+
+QString SMPStyleWidget::fallbackCategoryDefaultFont(const QString& category) const
+{
+ // A per-category "starting font" remembered across documents, purely
+ // as a UI convenience: when the user assigns a fallback slot to a
+ // script for the first time, this pre-fills a sensible starting
+ // point instead of leaving the font combo empty. Never written to
+ // the .sla file: it is a user preference, not a document property.
+ if (category.isEmpty())
+ return QString();
+ PrefsContext* ctx = PrefsManager::instance().prefsFile->getContext("fallbackFontDefaults");
+ return ctx->get(category, QString());
+}
+
+void SMPStyleWidget::rememberFallbackCategoryDefaultFont(const QString& category, const QString& font)
+{
+ if (category.isEmpty() || font.isEmpty())
+ return;
+ PrefsContext* ctx = PrefsManager::instance().prefsFile->getContext("fallbackFontDefaults");
+ ctx->set(category, font);
+}
+
void SMPStyleWidget::setParagraphEffect(int index)
{
QSignalBlocker sigPECombo(peCombo);
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h (revision 27767)
+++ scribus/ui/smpstylewidget.h (working copy)
@@ -52,6 +52,12 @@
void fillNumerationsCombo();
void fillNumRestartCombo();
void fillPECombo();
+ void fillFallbackScriptCombo(QComboBox* combo);
+ QString fallbackScriptLabel(const QString& scriptId) const;
+ QString fallbackScriptTooltip(const QString& scriptId) const;
+ QString fallbackScriptRegion(const QString& category) const;
+ QString fallbackCategoryDefaultFont(const QString& category) const;
+ void rememberFallbackCategoryDefaultFont(const QString& category, const QString& font);
void setParagraphEffect(int);
void showColors(const QList<ParagraphStyle*> &cstyles);
void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
@@ -69,6 +75,7 @@
void showTabs(const QList<ParagraphStyle*> &pstyles, int unitIndex);
void showCStyle(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, const QString &defLang, int unitIndex);
void showParent(const QList<ParagraphStyle*> &pstyles);
+ void showFallbackFonts(const QList<ParagraphStyle*> &pstyles);
void openEnhanced();
void closeEnhanced(bool show = false);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui (revision 27767)
+++ scribus/ui/smpstylewidget.ui (working copy)
@@ -1694,6 +1694,345 @@
</item>
</layout>
</widget>
+ <widget class="QWidget" name="tabFallbackFonts">
+ <attribute name="title">
+ <string>Fallback Fonts</string>
+ </attribute>
+ <layout class="QVBoxLayout">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="fallbackFontsInfoLabel">
+ <property name="text">
+ <string>For mixed-script text, choose up to two specific scripts and a fallback font (and size) for each.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="fallbackFontGroup1">
+ <property name="title">
+ <string>Fallback Font 1</string>
+ </property>
+ <layout class="QVBoxLayout" name="fallbackFontLayout1">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="fallbackScriptRow1">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="FormWidget" name="fallbackScriptLabel1">
+ <property name="toolTip">
+ <string>Script that this fallback font applies to</string>
+ </property>
+ <property name="labelVisibility">
+ <bool>false</bool>
+ </property>
+ <layout class="QHBoxLayout" name="fallbackScriptLabelLayout1">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QComboBox" name="fallbackScriptCombo1">
+ <property name="toolTip">
+ <string>Script that this fallback font applies to</string>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
+ </property>
+ <property name="minimumContentsLength">
+ <number>20</number>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="fallbackScriptSpacer1">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="fallbackFontRow1" stretch="0,0,1">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="SMFontComboH" name="fallbackFontCombo1" native="true"/>
+ </item>
+ <item>
+ <widget class="FormWidget" name="fallbackFontSizeLabel1">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="label" stdset="0">
+ <string>Size</string>
+ </property>
+ <layout class="QHBoxLayout" name="fallbackFontSizeLabelLayout1">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QDoubleSpinBox" name="fallbackFontSizeSpin1">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Fallback font size, as a percentage of the surrounding text's own size</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+ </property>
+ <property name="decimals">
+ <number>0</number>
+ </property>
+ <property name="minimum">
+ <double>10.000000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>400.000000000000000</double>
+ </property>
+ <property name="value">
+ <double>100.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="fallbackFontSizeSpacer1">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="fallbackFontGroup2">
+ <property name="title">
+ <string>Fallback Font 2</string>
+ </property>
+ <layout class="QVBoxLayout" name="fallbackFontLayout2">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="fallbackScriptRow2">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="FormWidget" name="fallbackScriptLabel2">
+ <property name="toolTip">
+ <string>Script that this fallback font applies to</string>
+ </property>
+ <property name="labelVisibility">
+ <bool>false</bool>
+ </property>
+ <layout class="QHBoxLayout" name="fallbackScriptLabelLayout2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QComboBox" name="fallbackScriptCombo2">
+ <property name="toolTip">
+ <string>Script that this fallback font applies to</string>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
+ </property>
+ <property name="minimumContentsLength">
+ <number>20</number>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="fallbackScriptSpacer2">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="fallbackFontRow2" stretch="0,0,1">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="SMFontComboH" name="fallbackFontCombo2" native="true"/>
+ </item>
+ <item>
+ <widget class="FormWidget" name="fallbackFontSizeLabel2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="label" stdset="0">
+ <string>Size</string>
+ </property>
+ <layout class="QHBoxLayout" name="fallbackFontSizeLabelLayout2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QDoubleSpinBox" name="fallbackFontSizeSpin2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Fallback font size, as a percentage of the surrounding text's own size</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+ </property>
+ <property name="decimals">
+ <number>0</number>
+ </property>
+ <property name="minimum">
+ <double>10.000000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>400.000000000000000</double>
+ </property>
+ <property name="value">
+ <double>100.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="fallbackFontSizeSpacer2">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="fallbackFontsSpacer">
+ <property name="orientation">
+ <enum>Qt::Orientation::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
</layout>
@@ -1755,6 +2094,12 @@
<container>1</container>
</customwidget>
<customwidget>
+ <class>SMFontComboH</class>
+ <extends>QWidget</extends>
+ <header>ui/smfontcomboh.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
<class>SMDirectionSelect</class>
<extends>QWidget</extends>
<header location="global">ui/smdirectionselect.h</header>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp (revision 27767)
+++ scribus/ui/smtextstyles.cpp (working copy)
@@ -507,6 +507,13 @@
connect(m_pwidget->maxConsecutiveCountSpinBox, SIGNAL(valueChanged(int)),this,SLOT(slotConsecutiveLines()));
+ connect(m_pwidget->fallbackScriptCombo1, SIGNAL(activated(int)), this, SLOT(slotFallbackScript1(int)));
+ connect(m_pwidget->fallbackFontCombo1, SIGNAL(fontSelected(QString)), this, SLOT(slotFallbackFont1(QString)));
+ connect(m_pwidget->fallbackFontSizeSpin1, SIGNAL(valueChanged(double)), this, SLOT(slotFallbackFontSize1(double)));
+ connect(m_pwidget->fallbackScriptCombo2, SIGNAL(activated(int)), this, SLOT(slotFallbackScript2(int)));
+ connect(m_pwidget->fallbackFontCombo2, SIGNAL(fontSelected(QString)), this, SLOT(slotFallbackFont2(QString)));
+ connect(m_pwidget->fallbackFontSizeSpin2, SIGNAL(valueChanged(double)), this, SLOT(slotFallbackFontSize2(double)));
+
connect(m_pwidget, SIGNAL(useParentParaEffects()), this, SLOT(slotParentParaEffects()));
connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
@@ -602,6 +609,13 @@
disconnect(m_pwidget->maxConsecutiveCountSpinBox, SIGNAL(valueChanged(int)),this,SLOT(slotConsecutiveLines()));
+ disconnect(m_pwidget->fallbackScriptCombo1, SIGNAL(activated(int)), this, SLOT(slotFallbackScript1(int)));
+ disconnect(m_pwidget->fallbackFontCombo1, SIGNAL(fontSelected(QString)), this, SLOT(slotFallbackFont1(QString)));
+ disconnect(m_pwidget->fallbackFontSizeSpin1, SIGNAL(valueChanged(double)), this, SLOT(slotFallbackFontSize1(double)));
+ disconnect(m_pwidget->fallbackScriptCombo2, SIGNAL(activated(int)), this, SLOT(slotFallbackScript2(int)));
+ disconnect(m_pwidget->fallbackFontCombo2, SIGNAL(fontSelected(QString)), this, SLOT(slotFallbackFont2(QString)));
+ disconnect(m_pwidget->fallbackFontSizeSpin2, SIGNAL(valueChanged(double)), this, SLOT(slotFallbackFontSize2(double)));
+
disconnect(m_pwidget, SIGNAL(useParentParaEffects()), this, SLOT(slotParentParaEffects()));
disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
@@ -859,6 +873,88 @@
slotSelectionDirty();
}
+
+void SMParagraphStyle::slotFallbackScript1(int index)
+{
+ QString scriptId = m_pwidget->fallbackScriptCombo1->itemData(index).toString();
+ for (int i = 0; i < m_selection.count(); ++i)
+ m_selection[i]->setFallbackFontScript1(scriptId);
+
+ // Convenience: if no font has been chosen for this slot yet, prefill
+ // with the remembered default font for the script's category.
+ if (m_pwidget->fallbackFontCombo1->currentFont().isEmpty())
+ {
+ QString category = ParagraphStyle::categoryForScript(scriptId);
+ QString defaultFont = m_pwidget->fallbackCategoryDefaultFont(category);
+ if (!defaultFont.isEmpty())
+ {
+ m_pwidget->fallbackFontCombo1->setCurrentFont(defaultFont);
+ slotFallbackFont1(defaultFont);
+ return;
+ }
+ }
+
+ slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotFallbackFont1(const QString &font)
+{
+ for (int i = 0; i < m_selection.count(); ++i)
+ m_selection[i]->setFallbackFont1(font);
+
+ QString scriptId = m_pwidget->fallbackScriptCombo1->currentData().toString();
+ m_pwidget->rememberFallbackCategoryDefaultFont(ParagraphStyle::categoryForScript(scriptId), font);
+
+ slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotFallbackFontSize1(double size)
+{
+ for (int i = 0; i < m_selection.count(); ++i)
+ m_selection[i]->setFallbackFontSize1(size);
+
+ slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotFallbackScript2(int index)
+{
+ QString scriptId = m_pwidget->fallbackScriptCombo2->itemData(index).toString();
+ for (int i = 0; i < m_selection.count(); ++i)
+ m_selection[i]->setFallbackFontScript2(scriptId);
+
+ if (m_pwidget->fallbackFontCombo2->currentFont().isEmpty())
+ {
+ QString category = ParagraphStyle::categoryForScript(scriptId);
+ QString defaultFont = m_pwidget->fallbackCategoryDefaultFont(category);
+ if (!defaultFont.isEmpty())
+ {
+ m_pwidget->fallbackFontCombo2->setCurrentFont(defaultFont);
+ slotFallbackFont2(defaultFont);
+ return;
+ }
+ }
+
+ slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotFallbackFont2(const QString &font)
+{
+ for (int i = 0; i < m_selection.count(); ++i)
+ m_selection[i]->setFallbackFont2(font);
+
+ QString scriptId = m_pwidget->fallbackScriptCombo2->currentData().toString();
+ m_pwidget->rememberFallbackCategoryDefaultFont(ParagraphStyle::categoryForScript(scriptId), font);
+
+ slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotFallbackFontSize2(double size)
+{
+ for (int i = 0; i < m_selection.count(); ++i)
+ m_selection[i]->setFallbackFontSize2(size);
+
+ slotSelectionDirty();
+}
// void SMParagraphStyle::slotDropCap(bool isOn)
// {
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h (revision 27767)
+++ scribus/ui/smtextstyles.h (working copy)
@@ -101,6 +101,12 @@
void slotMinGlyphExt();
void slotMaxGlyphExt();
void slotConsecutiveLines();
+ void slotFallbackScript1(int index);
+ void slotFallbackFont1(const QString &font);
+ void slotFallbackFontSize1(double size);
+ void slotFallbackScript2(int index);
+ void slotFallbackFont2(const QString &font);
+ void slotFallbackFontSize2(double size);
void handleKeepLinesStart();
void handleKeepLinesEnd();
void handleKeepTogether();
|
|
|
|
|
|
IMPLEMENTATION - added as a separate tab "Fallback Fonts" - defaults scripts are set just for fun (for fallback fonts) - but no default fonts are set for these scripts to avoid complications - the comments in the code should explain even more - should it be in Character Style tab instead, with only 1 Fallback Font slot (1 main and 1 fallback one, 2 in total)? |
|
|
KNOWN ISSUE(S) - Gurmukhi and Devanagari after English run is typed fine (no issues) - but English after Gurmukhi and Devanagari run is not. -- it can be solved by resetting the para style for the specific word, or for the entire text-frame. See clips below. |
|
|
MORE TESTING Only a few scripts seems to have issue with the others like Eng/Latin has after Gurmukhi. Many others I tested work fine (just change keyboard and start typing). Screenshot below. However, I just randomly added keyboard for those languages and typed, I don't know these languages. My aim was to test the "display" of scripts. See screenshot below. |
|
|
|
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-07-01 15:30 | qirat | New Issue | |
| 2026-07-02 07:15 | ale | Note Added: 0053923 | |
| 2026-07-02 12:15 | qirat | Note Added: 0053926 | |
| 2026-08-08 09:57 | qirat | Note Added: 0054173 | |
| 2026-08-08 09:57 | qirat | File Added: fallback-fonts-v4.03-4.patch | |
| 2026-08-08 09:58 | qirat | Note Added: 0054174 | |
| 2026-08-08 09:58 | qirat | File Added: ff-two-scripts-in-sequence.mp4 | |
| 2026-08-08 09:58 | qirat | File Added: fallback-fonts-tab.png | |
| 2026-08-08 09:58 | qirat | File Added: scripts-list.png | |
| 2026-08-08 09:59 | qirat | Note Added: 0054175 | |
| 2026-08-08 10:00 | qirat | Note Added: 0054176 | |
| 2026-08-08 10:00 | qirat | File Added: ff-known-issue(s).mp4 | |
| 2026-08-08 10:02 | qirat | Note Added: 0054177 | |
| 2026-08-08 11:57 | qirat | Note Added: 0054179 | |
| 2026-08-08 11:57 | qirat | File Added: scripts-in-sequence-more-testing.png |