View Issue Details

IDProjectCategoryView StatusLast Update
0016963ScribusText Frames / Story Editorpublic2026-07-15 04:24
Reporterale Assigned ToFahad  
PrioritynormalSeverityminorReproducibilityalways
Status resolvedResolutionfixed 
Product Version1.7.0.svn 
Fixed in Version1.7.4.svn 
Summary0016963: second line is indented on the left, when RTL drop caps is enabled
Descriptionit's not clear to me, if drop caps make sense for RTL languages (as long as we cannot use full words for the drop caps), but when RTL is enabled, the drop caps do not behave correctly.

one solution is to disable the drop caps for RTL, the other to fix the behavior.
TagsNo tags attached.
PatchNo

Relationships

has duplicate 0016961 closedFahad Drop Caps do not work with RTL 
related to 0017243 new Left indent for numbered lists 
related to 0017809 resolvedFahad [PATCH] RTL: wrong bulleted list character offset 

Activities

ale

2023-06-20 06:13

manager   ~0050237

rtl-dropcaps.png (76,298 bytes)   
rtl-dropcaps.png (76,298 bytes)   

ale

2023-06-20 06:16

manager   ~0050238

it looks like the first line is intended twice on the right and the second one once on the left...

medmedin2014

2023-07-04 09:42

reporter   ~0050257

I confirm the bug, in RTL text the second line of the paragraph with drop caps should be moved to left instead of right.
Screenshot_20230704_103938.png (252,736 bytes)   
Screenshot_20230704_103938.png (252,736 bytes)   

medmedin2014

2023-07-04 09:47

reporter   ~0050258

Not only for the second line but for all added lines where cap drops stretches
Screenshot_20230704_104527.png (241,831 bytes)   
Screenshot_20230704_104527.png (241,831 bytes)   

Fahad

2026-06-14 14:36

developer   ~0053832

here is a patch will fix rtl DropCap lines indentation also it provides a better fix for bullet char indentation bug https://bugs.scribus.net/view.php?id=17809

however, I think we should be implement something like libreoffice offer: number of Char & whole word
rtl-dropcap.png (241,377 bytes)   
rtl-dropcap.png (241,377 bytes)   
dropcap-rtl.patch (3,830 bytes)   
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -1763,42 +1763,28 @@ void PageItem_TextFrame::layout()
 				 }
 				 //set left indentation
 				 current.leftIndent = 0.0;
-				 if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode))
+				 // RTL: a line sitting beside an active drop cap must reserve the cap
+				 // band on the visual RIGHT. The indent block is normally skipped once
+				 // maxDX != 0, so force it to run for these follow-lines.
+				 bool rtlDropFollow = (style.direction() == ParagraphStyle::RTL)
+									   && current.hasDropCap && (maxDX > current.colLeft);
+				 if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode || rtlDropFollow))
 				 {
-					 current.leftIndent = style.leftMargin() + autoLeftIndent;
-					 if (itemText.isBlockStart(a))
+					 if (style.direction() == ParagraphStyle::RTL)
 					 {
-						 if (style.direction() == ParagraphStyle::RTL)
-						 {
-							 // use rightIndent to not mess with old behavior
-							 current.rightIndent = style.firstIndent();
-							 // line width should consider RTL indent when it breaks the line.
-							 current.mustLineEnd = current.colRight - current.rightIndent;
-						 }
-						 else
-							 current.leftIndent += style.firstIndent();
-						 if (BulNumMode || DropCmode)
-						 {
-							 if (style.parEffectIndent())
-							 {
-								 double effectWidth = 0.0;
-								 for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-								 {
-									 const auto& glyph = glyphClusters[j];
-									 if (glyph.firstChar() != a)
-										 break;
-									 effectWidth += glyph.width();
-								 }
-								 current.leftIndent -= style.parEffectOffset() + effectWidth;
-								 if (current.leftIndent < 0.0)
-								 {
-									 autoLeftIndent = abs(current.leftIndent);
-									 current.leftIndent = 0.0;
-								 }
-							 }
-						 }
+						 // RTL: logical left margin is on the visual right side
+						 // logical right margin is on the visual left side
+						 current.leftIndent = style.rightMargin();
+						 current.rightIndent = style.leftMargin();
+
+						 if (itemText.isBlockStart(a))
+							 current.rightIndent += style.firstIndent();
+						 // reserve the drop-cap band on the visual right for follow-lines
+						 if (rtlDropFollow)
+							 current.rightIndent += (maxDX - current.colLeft);
+						 // line width should consider RTL indent when it breaks the line.
+						 current.mustLineEnd = current.colRight - current.rightIndent;
 					 }
-					 current.addLeftIndent = false;
 				 }
 			 }
 			 current.recalculateY = true;
@@ -2361,6 +2347,12 @@ void PageItem_TextFrame::layout()
 					 }
 				 }
 				 // set the offset for Drop Cap, Bullet & Number List
 				 current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				 // RTL: the line is reversed at render time, so trailing extraWidth lands
+				 // on the cap's OUTER (right) edge, not between the cap and the text. Shift
+				 // the cap glyph to the right of its advance box so the reserved offset
+				 // falls on the inner (left) side, next to the text.
+				 if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
+					 current.glyphs[currentIndex].xoffset += style.parEffectOffset();
 				 if (DropCmode)
 				 {
 					 DropCmode = false;
@@ -2672,7 +2658,10 @@ void PageItem_TextFrame::layout()
 					 inOverflow = false;
 					 outs = false;
 					 current.startOfCol = false;
-					 current.restartX = current.xPos = qMax(maxDX, current.colLeft);
+					 if (style.direction() == ParagraphStyle::RTL)
+						 current.restartX = current.xPos = current.colLeft;
+					 else
+						 current.restartX = current.xPos = qMax(maxDX, current.colLeft);
 					 lastLineY = current.rowDesc;
 					 if (current.hasDropCap)
 					 {
dropcap-rtl.patch (3,830 bytes)   

cbradney

2026-06-22 18:17

administrator   ~0053873

Last edited: 2026-06-22 18:17

Testing https://bugs.scribus.net/view.php?id=17809... and using this patch LTR and RTL lorem text .. does not seem right. The indent changes from para to para. Also there seems to be code missing now when in LTR mode? The patch removes quite a lot of code
image.png (317,313 bytes)   
image.png (317,313 bytes)   
image-2.png (312,044 bytes)   
image-2.png (312,044 bytes)   

Fahad

2026-06-29 03:52

developer   ~0053910

It took me a long time to get all RTL drop caps works epically with different fonts. The patch adds right-to-left (RTL) support for drop caps in Scribu, fixing several rendering edge cases. It does five things:
1- Vertical scale: sizes the drop cap from its effective ascent (bbox + upward GPOS lift) so calligraphic fonts like Noto Nastaliq Urdu don't clip the frame top (direction-agnostic).
2- Follow-line reserve: adds a dropCapWidth member and, for RTL, reserves the cap width on the right instead of the left; follow-lines start flush at colLeft.
3- Marker offset: shifts the marker by parEffectOffset in RTL so the offset lands on the inner (text) side after render-time reversal (drop caps + bullet/numbered markers).
4- Ink overhang: measures right overhang and left bleed past the advance box and adjusts extraWidth/xoffset/reserve to keep ink inside the column edge.
5- Restart X: RTL follow-lines restart at colLeft rather than qMax(maxDX, colLeft).
rtl-dropcap.patch (6,404 bytes)   
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 75c14e8bf..d2a0c7cc9 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -371,6 +371,7 @@ struct LineControl {
 	int      column { 0 };
 	bool     startOfCol { true };
 	bool     hasDropCap;
+	double   dropCapWidth { 0.0 };
 	bool     afterOverflow { false };
 	bool     addLine { false };
 	bool     recalculateY { false };
@@ -1653,7 +1654,22 @@ void PageItem_TextFrame::layout()
 					if (current.startOfCol && (m_firstLineOffset == FLOPFontAscent))
 						asce = font.ascent(hlcsize10);
 					glyphCluster.setScaleH(glyphCluster.scaleH() / glyphCluster.scaleV());
-					glyphCluster.setScaleV(realAsce / realCharHeight);
+					// Divide by the glyph's *effective* ascent: bare bbox ascent plus any
+					// upward GPOS lift. A negative gl.yoffset lifts the glyph up, and the
+					// renderer applies that lift inside the scaleV matrix, so the rendered
+					// top is (ascent - yoffset) * scaleV. The bare bbox ascent ignores the
+					// lift, so the cap overshoots and clips the frame top for calligraphic
+					// fonts like Noto Nastaliq Urdu. Direction-agnostic; no RTL branch.
+					double capAscent = 0.0, capDescent = 0.0;
+					for (const GlyphLayout& g : glyphCluster.glyphs())
+					{
+						GlyphMetrics gm = font.glyphBBox(g.glyph, charStyle.fontSize() / 10.0);
+						capAscent  = qMax(capAscent,  gm.ascent + qMax(0.0, -g.yoffset)); // +upward GPOS lift
+						capDescent = qMax(capDescent, gm.descent);
+					}
+					if (capAscent <= 0.0)
+						capAscent = realCharHeight;
+					glyphCluster.setScaleV(realAsce / (capAscent + capDescent));
 					glyphCluster.setScaleH(glyphCluster.scaleH() * glyphCluster.scaleV());
 					glyphCluster.xoffset -= 0.5; //drop caps are always to far from column left edge
 				}
@@ -1763,7 +1779,12 @@ void PageItem_TextFrame::layout()
 				}
 				//set left indentation
 				current.leftIndent = 0.0;
-				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode))
+				// RTL: a line beside an active drop cap must reserve the cap width on the
+				// visual right; this block is normally skipped once maxDX != 0, so force
+				// it to run for those follow-lines.
+				bool rtlDropFollow = (style.direction() == ParagraphStyle::RTL)
+									 && current.hasDropCap && (maxDX > current.colLeft);
+				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode || rtlDropFollow))
 				{
 					current.leftIndent = style.leftMargin() + autoLeftIndent;
 					if (itemText.isBlockStart(a))
@@ -1798,6 +1819,13 @@ void PageItem_TextFrame::layout()
 							}
 						}
 					}
+					// RTL drop-cap follow-lines: Constrain the available line width
+					// from the right margin to prevent text overlapping the right-aligned drop cap.
+					if (rtlDropFollow)
+					{
+						current.rightIndent = current.dropCapWidth;
+						current.mustLineEnd = current.colRight - current.rightIndent;
+					}
 					current.addLeftIndent = false;
 				}
 			}
@@ -2376,6 +2404,13 @@ void PageItem_TextFrame::layout()
 				}
 				// set the offset for Drop Cap, Bullet & Number List
 				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				// RTL: the line is reversed at render time, so trailing extraWidth lands on
+				// the marker's OUTER (right) edge, not between the marker and the text. Shift
+				// the marker glyph to the right of its advance box so the reserved offset
+				// falls on the inner (left) side, next to the text. Drop caps and bullet /
+				// numbered-list markers share this offset mechanism.
+				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
 
 				if (DropCmode)
 				{
@@ -2384,6 +2419,34 @@ void PageItem_TextFrame::layout()
 					maxDY = current.yPos;
 					current.hasDropCap = true;
 					maxDX = current.xPos;
+					// Follow-line reserve must be the cap's own band, not maxDX
+					// (maxDX = colLeft + leftIndent + capWidth, so it over-reserves
+					// by leftIndent). width() here already includes parEffectOffset.
+					current.dropCapWidth = current.glyphs[currentIndex].width();
+					if (style.direction() == ParagraphStyle::RTL)
+					{
+						GlyphCluster& cap = current.glyphs[currentIndex];
+						double sizePt = charStyle.fontSize() / 10.0;
+						double pen = 0.0, inkRight = 0.0, inkLeft = 0.0;
+						for (const GlyphLayout& g : cap.glyphs())
+						{
+							double xMax = font.glyphBBox(g.glyph, sizePt).width;          // ink right edge (base pt)
+							QRectF ob = font.glyphOutline(g.glyph, sizePt).boundingRect();
+							double s  = (ob.right() != 0.0) ? xMax / ob.right() : 0.0;     // outline units -> base pt
+							double xMin = ob.left() * s;                                  // ink left edge (base pt)
+							inkRight = qMax(inkRight, pen + xMax);
+							inkLeft  = qMin(inkLeft,  pen + xMin);   // <0 only if ink bleeds left of origin
+							pen += g.xadvance;
+						}
+						double overhang  = qMax(0.0, (inkRight - pen)) * cap.scaleH();    // ink past advance, right
+						double leftBleed = qMax(0.0, -inkLeft)        * cap.scaleH();     // ink left of origin (Naskh bowl)
+						if (overhang > 0.0 || leftBleed > 0.0)
+						{
+							cap.extraWidth       += overhang + leftBleed; // box = full ink WIDTH (xMax - xMin)
+							cap.xoffset          += leftBleed;            // shift right so ink right edge stays at colRight
+							current.dropCapWidth += overhang + leftBleed; // follow-line reserve matches
+						}
+					}
 					double spacing = calculateLineSpacing (style, this);
 					current.yPos -= spacing * (DropLines - 1);
 					if (style.lineSpacingMode() == ParagraphStyle::BaselineGridLineSpacing)
@@ -2672,7 +2735,12 @@ void PageItem_TextFrame::layout()
 					inOverflow = false;
 					outs = false;
 					current.startOfCol = false;
-					current.restartX = current.xPos = qMax(maxDX, current.colLeft);
+					if (style.direction() == ParagraphStyle::RTL)
+						// RTL: drop cap sits on the visual right, so follow-lines start flush
+						// at the column left; the reserve is taken from the right.
+						current.restartX = current.xPos = current.colLeft;
+					else
+						current.restartX = current.xPos = qMax(maxDX, current.colLeft);
 					lastLineY = current.rowDesc;
 					if (current.hasDropCap)
 					{
rtl-dropcap.patch (6,404 bytes)   

cbradney

2026-07-01 19:46

administrator   ~0053918

Looks like my example with the bullets is now fixed - I had to set to LTR and RTL to make it clean though.
Ale's example does not look fixed.
image-3.png (466,569 bytes)   
image-3.png (466,569 bytes)   

Fahad

2026-07-02 08:58

developer   ~0053925

Doing LTR Drop Cap in RTL text frame is extremely rare so I didn't think about it before that. Adding RTL drop cap is very difficult task because the layout engine is built LTR-first — every position (xPos, maxDX, colLeft, colRight) is computed as if the line reads left to right. RTL was hooked up later, only as a visual flip at render time, not in the layout math itself. That caused three problems:
1- style.direction() == RTL is a paragraph box setting, not a script property. An English drop cap in an RTL frame still reads left to right, so it needs its gap on the left — but the code originally assumed RTL frame = RTL letter, and put the gap on the wrong side.
2- Some variables (like maxDX) are shared with unrelated features (bullets, numbering) and get reset in ways that don't suit drop caps.
3- Calligraphic fonts (Amiri, Noto Nastaliq Urdu, DecoType Naskh) have ink that overhangs the glyph's box, sometimes 30%+ — the original scale math ignored this and caused clipping/overhang.

--- My approach ---
Small, isolated changes only — no rewrites, since this function is too tightly coupled to risk breaking stable LTR behavior. Each fix was checked against real runtime numbers before being applied, and reverted immediately if the numbers didn't hold up.

What was fixed so far:

1- Vertical clipping at the frame top (wrong ascent used in scale calculation).
2- Descent collision with lines below, for large-descent fonts.
3- RTL follow-line gap: now based on dropCapWidth (clean value) instead of maxDX (shared/contaminated).
4- Mixed-script drop caps in RTL frames: an RTL frame can contain either an Arabic or an English drop cap, and each needs the gap on a different side. Fixed the gap logic to 5- 5- check the actual drop cap character's reading direction, not just the frame's direction flag — so Arabic caps reserve the gap on the right and English caps reserve it on the left, both correctly, in the same RTL frame.
6- Horizontal overhang/bleed compensation for fonts like Amiri and DecoType Naskh.
A separate, pre-existing bug in justifyLine(): the drop cap's own width was missing from the line's natural-width calculation, which pushed drop caps out of the frame under Force Justify + RTL.

please test
rtl-ltr-dropcap.png (179,698 bytes)   
rtl-ltr-dropcap.png (179,698 bytes)   
rtl-dropcap v2.patch (7,486 bytes)   
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 75c14e8bf..1f2957e69 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -371,6 +371,8 @@ struct LineControl {
 	int      column { 0 };
 	bool     startOfCol { true };
 	bool     hasDropCap;
+	double   dropCapWidth { 0.0 };
+	bool     dropCapGapOnRight { false };
 	bool     afterOverflow { false };
 	bool     addLine { false };
 	bool     recalculateY { false };
@@ -837,8 +839,8 @@ struct LineControl {
 		int startItem = 0;
 		if (glyphs[startItem].hasFlag(ScLayout_DropCap))
 		{
-			startItem++;
-			naturalWidth += glyphs[startItem].width();
+			naturalWidth += glyphs[startItem].width();  // add the drop cap's own width first
+			startItem++;                                // then skip it in the loop below
 		}
 		// distribute whitespace on spaces and glyphs
 		for (int i = startItem; i < glyphsCount; ++i)
@@ -1653,7 +1655,22 @@ void PageItem_TextFrame::layout()
 					if (current.startOfCol && (m_firstLineOffset == FLOPFontAscent))
 						asce = font.ascent(hlcsize10);
 					glyphCluster.setScaleH(glyphCluster.scaleH() / glyphCluster.scaleV());
-					glyphCluster.setScaleV(realAsce / realCharHeight);
+					// Divide by the glyph's *effective* ascent: bare bbox ascent plus any
+					// upward GPOS lift. A negative gl.yoffset lifts the glyph up, and the
+					// renderer applies that lift inside the scaleV matrix, so the rendered
+					// top is (ascent - yoffset) * scaleV. The bare bbox ascent ignores the
+					// lift, so the cap overshoots and clips the frame top for calligraphic
+					// fonts like Noto Nastaliq Urdu. Direction-agnostic; no RTL branch.
+					double capAscent = 0.0, capDescent = 0.0;
+					for (const GlyphLayout& g : glyphCluster.glyphs())
+					{
+						GlyphMetrics gm = font.glyphBBox(g.glyph, charStyle.fontSize() / 10.0);
+						capAscent  = qMax(capAscent,  gm.ascent + qMax(0.0, -g.yoffset)); // +upward GPOS lift
+						capDescent = qMax(capDescent, gm.descent);
+					}
+					if (capAscent <= 0.0)
+						capAscent = realCharHeight;
+					glyphCluster.setScaleV(realAsce / (capAscent + capDescent));
 					glyphCluster.setScaleH(glyphCluster.scaleH() * glyphCluster.scaleV());
 					glyphCluster.xoffset -= 0.5; //drop caps are always to far from column left edge
 				}
@@ -1763,7 +1780,13 @@ void PageItem_TextFrame::layout()
 				}
 				//set left indentation
 				current.leftIndent = 0.0;
-				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode))
+				// RTL: a line beside an active drop cap must reserve the cap width on the
+				// visual right; this block is normally skipped once maxDX != 0, so force
+				// it to run for those follow-lines.
+				bool rtlDropFollow = (style.direction() == ParagraphStyle::RTL)
+									 && current.hasDropCap && !DropCmode
+									 && (maxDX > current.colLeft);
+				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode || rtlDropFollow))
 				{
 					current.leftIndent = style.leftMargin() + autoLeftIndent;
 					if (itemText.isBlockStart(a))
@@ -1798,6 +1821,13 @@ void PageItem_TextFrame::layout()
 							}
 						}
 					}
+					// RTL drop-cap follow-lines: Constrain the available line width
+					// from the right margin to prevent text overlapping the right-aligned drop cap.
+					if (rtlDropFollow && current.dropCapGapOnRight)
+					{
+						current.rightIndent = current.dropCapWidth;
+						current.mustLineEnd = current.colRight - current.rightIndent;
+					}
 					current.addLeftIndent = false;
 				}
 			}
@@ -2376,6 +2406,13 @@ void PageItem_TextFrame::layout()
 				}
 				// set the offset for Drop Cap, Bullet & Number List
 				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				// RTL: the line is reversed at render time, so trailing extraWidth lands on
+				// the marker's OUTER (right) edge, not between the marker and the text. Shift
+				// the marker glyph to the right of its advance box so the reserved offset
+				// falls on the inner (left) side, next to the text. Drop caps and bullet /
+				// numbered-list markers share this offset mechanism.
+				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
 
 				if (DropCmode)
 				{
@@ -2384,6 +2421,39 @@ void PageItem_TextFrame::layout()
 					maxDY = current.yPos;
 					current.hasDropCap = true;
 					maxDX = current.xPos;
+					// Follow-line reserve must be the cap's own band, not maxDX
+					// (maxDX = colLeft + leftIndent + capWidth, so it over-reserves
+					// by leftIndent). width() here already includes parEffectOffset.
+					current.dropCapWidth = current.glyphs[currentIndex].width();
+					// Gap side depends on the cap letter itself, not the frame setting:
+					// an English letter in an RTL frame still reads left-to-right.
+					QChar::Direction capCharDir = itemText.text(a).direction();
+					current.dropCapGapOnRight = (style.direction() == ParagraphStyle::RTL)
+												&& (capCharDir != QChar::DirL);
+					if (style.direction() == ParagraphStyle::RTL)
+					{
+						GlyphCluster& cap = current.glyphs[currentIndex];
+						double sizePt = charStyle.fontSize() / 10.0;
+						double pen = 0.0, inkRight = 0.0, inkLeft = 0.0;
+						for (const GlyphLayout& g : cap.glyphs())
+						{
+							double xMax = font.glyphBBox(g.glyph, sizePt).width;          // ink right edge (base pt)
+							QRectF ob = font.glyphOutline(g.glyph, sizePt).boundingRect();
+							double s  = (ob.right() != 0.0) ? xMax / ob.right() : 0.0;     // outline units -> base pt
+							double xMin = ob.left() * s;                                  // ink left edge (base pt)
+							inkRight = qMax(inkRight, pen + xMax);
+							inkLeft  = qMin(inkLeft,  pen + xMin);   // <0 only if ink bleeds left of origin
+							pen += g.xadvance;
+						}
+						double overhang  = qMax(0.0, (inkRight - pen)) * cap.scaleH();    // ink past advance, right
+						double leftBleed = qMax(0.0, -inkLeft)        * cap.scaleH();     // ink left of origin (Naskh bowl)
+						if (overhang > 0.0 || leftBleed > 0.0)
+						{
+							cap.extraWidth       += overhang + leftBleed; // box = full ink WIDTH (xMax - xMin)
+							cap.xoffset          += leftBleed;            // shift right so ink right edge stays at colRight
+							current.dropCapWidth += overhang + leftBleed; // follow-line reserve matches
+						}
+					}
 					double spacing = calculateLineSpacing (style, this);
 					current.yPos -= spacing * (DropLines - 1);
 					if (style.lineSpacingMode() == ParagraphStyle::BaselineGridLineSpacing)
@@ -2672,7 +2742,14 @@ void PageItem_TextFrame::layout()
 					inOverflow = false;
 					outs = false;
 					current.startOfCol = false;
-					current.restartX = current.xPos = qMax(maxDX, current.colLeft);
+					if (current.hasDropCap && !current.dropCapGapOnRight && style.direction() == ParagraphStyle::RTL)
+						// English-style cap in an RTL frame: gap stays on the left, but only
+						// while the cap is still active — once it ends, fall through below.
+						current.restartX = current.xPos = current.colLeft + current.dropCapWidth;
+					else if (style.direction() == ParagraphStyle::RTL)
+						current.restartX = current.xPos = current.colLeft;
+					else
+						current.restartX = current.xPos = qMax(maxDX, current.colLeft);
 					lastLineY = current.rowDesc;
 					if (current.hasDropCap)
 					{
rtl-dropcap v2.patch (7,486 bytes)   

cbradney

2026-07-02 21:44

administrator   ~0053929

I'm not sure how second paragraphs are supposed to work. Changing the H to a 1 made it weird. Clearly, LTR language in RTL format may produce some weirdness, though we should work through them.
image-4.png (320,451 bytes)   
image-4.png (320,451 bytes)   

Fahad

2026-07-04 10:25

developer   ~0053965

Actually, I spotted the real issue here. In the second line, the gap was still appearing on the right side of the English text. I've managed to fix it now.

Because the caption character is an Arabic numeral (which has weak directionality) and the paragraph is RTL (Right-to-Left), it now correctly aligns to the right as expected. This aligns perfectly with how LibreOffice handles it.
rtl-dropcap-2.patch (7,303 bytes)   
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 75c14e8bf..ad3db6d76 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -371,6 +371,7 @@ struct LineControl {
 	int      column { 0 };
 	bool     startOfCol { true };
 	bool     hasDropCap;
+	double   dropCapWidth { 0.0 };
 	bool     afterOverflow { false };
 	bool     addLine { false };
 	bool     recalculateY { false };
@@ -837,8 +838,8 @@ struct LineControl {
 		int startItem = 0;
 		if (glyphs[startItem].hasFlag(ScLayout_DropCap))
 		{
-			startItem++;
-			naturalWidth += glyphs[startItem].width();
+			naturalWidth += glyphs[startItem].width();  // add the drop cap's own width first
+			startItem++;                                // then skip it in the loop below
 		}
 		// distribute whitespace on spaces and glyphs
 		for (int i = startItem; i < glyphsCount; ++i)
@@ -1653,7 +1654,22 @@ void PageItem_TextFrame::layout()
 					if (current.startOfCol && (m_firstLineOffset == FLOPFontAscent))
 						asce = font.ascent(hlcsize10);
 					glyphCluster.setScaleH(glyphCluster.scaleH() / glyphCluster.scaleV());
-					glyphCluster.setScaleV(realAsce / realCharHeight);
+					// Divide by the glyph's *effective* ascent: bare bbox ascent plus any
+					// upward GPOS lift. A negative gl.yoffset lifts the glyph up, and the
+					// renderer applies that lift inside the scaleV matrix, so the rendered
+					// top is (ascent - yoffset) * scaleV. The bare bbox ascent ignores the
+					// lift, so the cap overshoots and clips the frame top for calligraphic
+					// fonts like Noto Nastaliq Urdu. Direction-agnostic; no RTL branch.
+					double capAscent = 0.0, capDescent = 0.0;
+					for (const GlyphLayout& g : glyphCluster.glyphs())
+					{
+						GlyphMetrics gm = font.glyphBBox(g.glyph, charStyle.fontSize() / 10.0);
+						capAscent  = qMax(capAscent,  gm.ascent + qMax(0.0, -g.yoffset)); // +upward GPOS lift
+						capDescent = qMax(capDescent, gm.descent);
+					}
+					if (capAscent <= 0.0)
+						capAscent = realCharHeight;
+					glyphCluster.setScaleV(realAsce / (capAscent + capDescent));
 					glyphCluster.setScaleH(glyphCluster.scaleH() * glyphCluster.scaleV());
 					glyphCluster.xoffset -= 0.5; //drop caps are always to far from column left edge
 				}
@@ -1763,7 +1779,17 @@ void PageItem_TextFrame::layout()
 				}
 				//set left indentation
 				current.leftIndent = 0.0;
-				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode))
+				QChar capChar = itemText.text(a);
+				bool capIsArabicScript = (capChar.script() == QChar::Script_Arabic)
+									   || (capChar.script() == QChar::Script_Syriac)
+									   || (capChar.script() == QChar::Script_Nko);
+				// RTL: a line beside an active drop cap must reserve the cap width on the
+				// visual right; this block is normally skipped once maxDX != 0, so force
+				// it to run for those follow-lines.
+				bool rtlDropFollow = (style.direction() == ParagraphStyle::RTL)
+									 && current.hasDropCap && !DropCmode && capIsArabicScript
+									 && (maxDX > current.colLeft);
+				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode || rtlDropFollow))
 				{
 					current.leftIndent = style.leftMargin() + autoLeftIndent;
 					if (itemText.isBlockStart(a))
@@ -1798,6 +1824,13 @@ void PageItem_TextFrame::layout()
 							}
 						}
 					}
+					// RTL drop-cap follow-lines: Constrain the available line width
+					// from the right margin to prevent text overlapping the right-aligned drop cap.
+					if (rtlDropFollow)
+					{
+						current.rightIndent = current.dropCapWidth;
+						current.mustLineEnd = current.colRight - current.rightIndent;
+					}
 					current.addLeftIndent = false;
 				}
 			}
@@ -2376,6 +2409,13 @@ void PageItem_TextFrame::layout()
 				}
 				// set the offset for Drop Cap, Bullet & Number List
 				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				// RTL: the line is reversed at render time, so trailing extraWidth lands on
+				// the marker's OUTER (right) edge, not between the marker and the text. Shift
+				// the marker glyph to the right of its advance box so the reserved offset
+				// falls on the inner (left) side, next to the text. Drop caps and bullet /
+				// numbered-list markers share this offset mechanism.
+				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
 
 				if (DropCmode)
 				{
@@ -2384,6 +2424,34 @@ void PageItem_TextFrame::layout()
 					maxDY = current.yPos;
 					current.hasDropCap = true;
 					maxDX = current.xPos;
+					// Follow-line reserve must be the cap's own band, not maxDX
+					// (maxDX = colLeft + leftIndent + capWidth, so it over-reserves
+					// by leftIndent). width() here already includes parEffectOffset.
+					current.dropCapWidth = current.glyphs[currentIndex].width();
+					if (style.direction() == ParagraphStyle::RTL)
+					{
+						GlyphCluster& cap = current.glyphs[currentIndex];
+						double sizePt = charStyle.fontSize() / 10.0;
+						double pen = 0.0, inkRight = 0.0, inkLeft = 0.0;
+						for (const GlyphLayout& g : cap.glyphs())
+						{
+							double xMax = font.glyphBBox(g.glyph, sizePt).width;          // ink right edge (base pt)
+							QRectF ob = font.glyphOutline(g.glyph, sizePt).boundingRect();
+							double s  = (ob.right() != 0.0) ? xMax / ob.right() : 0.0;     // outline units -> base pt
+							double xMin = ob.left() * s;                                  // ink left edge (base pt)
+							inkRight = qMax(inkRight, pen + xMax);
+							inkLeft  = qMin(inkLeft,  pen + xMin);   // <0 only if ink bleeds left of origin
+							pen += g.xadvance;
+						}
+						double overhang  = qMax(0.0, (inkRight - pen)) * cap.scaleH();    // ink past advance, right
+						double leftBleed = qMax(0.0, -inkLeft)        * cap.scaleH();     // ink left of origin (Naskh bowl)
+						if (overhang > 0.0 || leftBleed > 0.0)
+						{
+							cap.extraWidth       += overhang + leftBleed; // box = full ink WIDTH (xMax - xMin)
+							cap.xoffset          += leftBleed;            // shift right so ink right edge stays at colRight
+							current.dropCapWidth += overhang + leftBleed; // follow-line reserve matches
+						}
+					}
 					double spacing = calculateLineSpacing (style, this);
 					current.yPos -= spacing * (DropLines - 1);
 					if (style.lineSpacingMode() == ParagraphStyle::BaselineGridLineSpacing)
@@ -2672,7 +2740,14 @@ void PageItem_TextFrame::layout()
 					inOverflow = false;
 					outs = false;
 					current.startOfCol = false;
-					current.restartX = current.xPos = qMax(maxDX, current.colLeft);
+					if (current.hasDropCap && style.direction() == ParagraphStyle::RTL)
+						// English-style cap in an RTL frame: gap stays on the left, but only
+						// while the cap is still active — once it ends, fall through below.
+						current.restartX = current.xPos = current.colLeft + current.dropCapWidth;
+					else if (style.direction() == ParagraphStyle::RTL)
+						current.restartX = current.xPos = current.colLeft;
+					else
+						current.restartX = current.xPos = qMax(maxDX, current.colLeft);
 					lastLineY = current.rowDesc;
 					if (current.hasDropCap)
 					{
rtl-dropcap-2.patch (7,303 bytes)   

cbradney

2026-07-04 21:05

administrator   ~0053971

Not sure.. now the 3 has changed :)
image-5.png (236,571 bytes)   
image-5.png (236,571 bytes)   

Fahad

2026-07-05 05:07

developer   ~0053972

here we go. Fixed now here how it should be looked like.
rtl-ltr-dropcap2.png (124,423 bytes)   
rtl-ltr-dropcap2.png (124,423 bytes)   
rtl-dropcap v3.patch (7,440 bytes)   
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 75c14e8bf..9f4f080f8 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -371,6 +371,8 @@ struct LineControl {
 	int      column { 0 };
 	bool     startOfCol { true };
 	bool     hasDropCap;
+	double   dropCapWidth { 0.0 };
+	bool     dropCapIsLTR { false };
 	bool     afterOverflow { false };
 	bool     addLine { false };
 	bool     recalculateY { false };
@@ -837,8 +839,8 @@ struct LineControl {
 		int startItem = 0;
 		if (glyphs[startItem].hasFlag(ScLayout_DropCap))
 		{
-			startItem++;
-			naturalWidth += glyphs[startItem].width();
+			naturalWidth += glyphs[startItem].width();  // add the drop cap's own width first
+			startItem++;                                // then skip it in the loop below
 		}
 		// distribute whitespace on spaces and glyphs
 		for (int i = startItem; i < glyphsCount; ++i)
@@ -1653,7 +1655,22 @@ void PageItem_TextFrame::layout()
 					if (current.startOfCol && (m_firstLineOffset == FLOPFontAscent))
 						asce = font.ascent(hlcsize10);
 					glyphCluster.setScaleH(glyphCluster.scaleH() / glyphCluster.scaleV());
-					glyphCluster.setScaleV(realAsce / realCharHeight);
+					// Divide by the glyph's *effective* ascent: bare bbox ascent plus any
+					// upward GPOS lift. A negative gl.yoffset lifts the glyph up, and the
+					// renderer applies that lift inside the scaleV matrix, so the rendered
+					// top is (ascent - yoffset) * scaleV. The bare bbox ascent ignores the
+					// lift, so the cap overshoots and clips the frame top for calligraphic
+					// fonts like Noto Nastaliq Urdu. Direction-agnostic; no RTL branch.
+					double capAscent = 0.0, capDescent = 0.0;
+					for (const GlyphLayout& g : glyphCluster.glyphs())
+					{
+						GlyphMetrics gm = font.glyphBBox(g.glyph, charStyle.fontSize() / 10.0);
+						capAscent  = qMax(capAscent,  gm.ascent + qMax(0.0, -g.yoffset)); // +upward GPOS lift
+						capDescent = qMax(capDescent, gm.descent);
+					}
+					if (capAscent <= 0.0)
+						capAscent = realCharHeight;
+					glyphCluster.setScaleV(realAsce / (capAscent + capDescent));
 					glyphCluster.setScaleH(glyphCluster.scaleH() * glyphCluster.scaleV());
 					glyphCluster.xoffset -= 0.5; //drop caps are always to far from column left edge
 				}
@@ -1763,7 +1780,14 @@ void PageItem_TextFrame::layout()
 				}
 				//set left indentation
 				current.leftIndent = 0.0;
-				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode))
+				// RTL: a line beside an active drop cap must reserve the cap width on the
+				// visual right; this block is normally skipped once maxDX != 0, so force
+				// it to run for those follow-lines.
+				bool rtlDropFollow = (style.direction() == ParagraphStyle::RTL)
+									 && current.hasDropCap && !DropCmode
+									 && !current.dropCapIsLTR
+									 && (maxDX > current.colLeft);
+				if (current.addLeftIndent && ((maxDX == 0) || DropCmode || BulNumMode || rtlDropFollow))
 				{
 					current.leftIndent = style.leftMargin() + autoLeftIndent;
 					if (itemText.isBlockStart(a))
@@ -1798,6 +1822,13 @@ void PageItem_TextFrame::layout()
 							}
 						}
 					}
+					// RTL drop-cap follow-lines: Constrain the available line width
+					// from the right margin to prevent text overlapping the right-aligned drop cap.
+					if (rtlDropFollow)
+					{
+						current.rightIndent = current.dropCapWidth;
+						current.mustLineEnd = current.colRight - current.rightIndent;
+					}
 					current.addLeftIndent = false;
 				}
 			}
@@ -2376,6 +2407,13 @@ void PageItem_TextFrame::layout()
 				}
 				// set the offset for Drop Cap, Bullet & Number List
 				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				// RTL: the line is reversed at render time, so trailing extraWidth lands on
+				// the marker's OUTER (right) edge, not between the marker and the text. Shift
+				// the marker glyph to the right of its advance box so the reserved offset
+				// falls on the inner (left) side, next to the text. Drop caps and bullet /
+				// numbered-list markers share this offset mechanism.
+				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
 
 				if (DropCmode)
 				{
@@ -2384,6 +2422,38 @@ void PageItem_TextFrame::layout()
 					maxDY = current.yPos;
 					current.hasDropCap = true;
 					maxDX = current.xPos;
+					// Follow-line reserve must be the cap's own band, not maxDX
+					// (maxDX = colLeft + leftIndent + capWidth, so it over-reserves
+					// by leftIndent). width() here already includes parEffectOffset.
+					current.dropCapWidth = current.glyphs[currentIndex].width();
+					// Strong-LTR char (e.g. English letter) keeps LTR placement even
+					// in an RTL paragraph. Weak types (Arabic-Indic digits) and
+					// strong-RTL chars (Arabic letters) follow the paragraph's RTL side.
+					current.dropCapIsLTR = (itemText.text(a).direction() == QChar::DirL);
+					if (style.direction() == ParagraphStyle::RTL)
+					{
+						GlyphCluster& cap = current.glyphs[currentIndex];
+						double sizePt = charStyle.fontSize() / 10.0;
+						double pen = 0.0, inkRight = 0.0, inkLeft = 0.0;
+						for (const GlyphLayout& g : cap.glyphs())
+						{
+							double xMax = font.glyphBBox(g.glyph, sizePt).width;          // ink right edge (base pt)
+							QRectF ob = font.glyphOutline(g.glyph, sizePt).boundingRect();
+							double s  = (ob.right() != 0.0) ? xMax / ob.right() : 0.0;     // outline units -> base pt
+							double xMin = ob.left() * s;                                  // ink left edge (base pt)
+							inkRight = qMax(inkRight, pen + xMax);
+							inkLeft  = qMin(inkLeft,  pen + xMin);   // <0 only if ink bleeds left of origin
+							pen += g.xadvance;
+						}
+						double overhang  = qMax(0.0, (inkRight - pen)) * cap.scaleH();    // ink past advance, right
+						double leftBleed = qMax(0.0, -inkLeft)        * cap.scaleH();     // ink left of origin (Naskh bowl)
+						if (overhang > 0.0 || leftBleed > 0.0)
+						{
+							cap.extraWidth       += overhang + leftBleed; // box = full ink WIDTH (xMax - xMin)
+							cap.xoffset          += leftBleed;            // shift right so ink right edge stays at colRight
+							current.dropCapWidth += overhang + leftBleed; // follow-line reserve matches
+						}
+					}
 					double spacing = calculateLineSpacing (style, this);
 					current.yPos -= spacing * (DropLines - 1);
 					if (style.lineSpacingMode() == ParagraphStyle::BaselineGridLineSpacing)
@@ -2672,7 +2742,14 @@ void PageItem_TextFrame::layout()
 					inOverflow = false;
 					outs = false;
 					current.startOfCol = false;
-					current.restartX = current.xPos = qMax(maxDX, current.colLeft);
+					if (current.hasDropCap && style.direction() == ParagraphStyle::RTL && current.dropCapIsLTR)
+						// English-style cap in an RTL frame: gap stays on the left, but only
+						// while the cap is still active — once it ends, fall through below.
+						current.restartX = current.xPos = current.colLeft + current.dropCapWidth;
+					else if (style.direction() == ParagraphStyle::RTL)
+						current.restartX = current.xPos = current.colLeft;
+					else
+						current.restartX = current.xPos = qMax(maxDX, current.colLeft);
 					lastLineY = current.rowDesc;
 					if (current.hasDropCap)
 					{
rtl-dropcap v3.patch (7,440 bytes)   

cbradney

2026-07-05 13:42

administrator   ~0053974

Committed. thank you for working through all the revisions. I tested a few fonts and scenarios and it seems to work well now.

Fahad

2026-07-05 13:58

developer   ~0053976

Finally �️
Thank you for your all great work.

Issue History

Date Modified Username Field Change
2023-06-20 06:12 ale New Issue
2023-06-20 06:13 ale Relationship added related to 0016961
2023-06-20 06:13 ale Note Added: 0050237
2023-06-20 06:13 ale File Added: rtl-dropcaps.png
2023-06-20 06:16 ale Note Added: 0050238
2023-07-04 09:42 medmedin2014 Note Added: 0050257
2023-07-04 09:42 medmedin2014 File Added: Screenshot_20230704_103938.png
2023-07-04 09:47 medmedin2014 Note Added: 0050258
2023-07-04 09:47 medmedin2014 File Added: Screenshot_20230704_104527.png
2024-07-01 13:02 ale Relationship added related to 0017243
2025-04-27 19:16 cbradney Category Story Editor / Text Frames => Text Frames / Story Editor
2026-06-14 14:36 Fahad Note Added: 0053832
2026-06-14 14:36 Fahad File Added: rtl-dropcap.png
2026-06-14 14:36 Fahad File Added: dropcap-rtl.patch
2026-06-14 15:47 ale Relationship added related to 0017809
2026-06-22 18:17 cbradney Note Added: 0053873
2026-06-22 18:17 cbradney File Added: image.png
2026-06-22 18:17 cbradney File Added: image-2.png
2026-06-22 18:17 cbradney Note Edited: 0053873
2026-06-29 03:52 Fahad Note Added: 0053910
2026-06-29 03:52 Fahad File Added: rtl-dropcap.patch
2026-07-01 19:46 cbradney Note Added: 0053918
2026-07-01 19:46 cbradney File Added: image-3.png
2026-07-02 08:58 Fahad Note Added: 0053925
2026-07-02 08:58 Fahad File Added: rtl-ltr-dropcap.png
2026-07-02 08:58 Fahad File Added: rtl-dropcap v2.patch
2026-07-02 21:44 cbradney Note Added: 0053929
2026-07-02 21:44 cbradney File Added: image-4.png
2026-07-04 10:25 Fahad Note Added: 0053965
2026-07-04 10:25 Fahad File Added: rtl-dropcap-2.patch
2026-07-04 21:05 cbradney Note Added: 0053971
2026-07-04 21:05 cbradney File Added: image-5.png
2026-07-05 05:07 Fahad Note Added: 0053972
2026-07-05 05:07 Fahad File Added: rtl-ltr-dropcap2.png
2026-07-05 05:07 Fahad File Added: rtl-dropcap v3.patch
2026-07-05 13:42 cbradney Assigned To => Fahad
2026-07-05 13:42 cbradney Status new => resolved
2026-07-05 13:42 cbradney Resolution open => fixed
2026-07-05 13:42 cbradney Fixed in Version => 1.7.4.svn
2026-07-05 13:42 cbradney Note Added: 0053974
2026-07-05 13:58 Fahad Note Added: 0053976
2026-07-15 04:24 Fahad Relationship replaced has duplicate 0016961