View Issue Details

IDProjectCategoryView StatusLast Update
0000460ScribusText Frames / Story Editorpublic2026-09-23 11:14
Reporterlinuxlingam Assigned To 
PrioritynormalSeveritytweakReproducibilityalways
Status acknowledgedResolutionopen 
PlatformAthlonXPOSFedoraOS VersionCore 1
Product Version1.3.4cvs 
Target VersionFuture Release - Nice to have 
Summary0000460: Drop caps with more than one character
DescriptionTitle was:
drop-cap with # of letters, or first word(s). subsequent paras shift-return for newline, no dropcap


drop-caps should give choice of number of letters to drop-cap, or word(s). like in Openoffice writer. if i want a newline, pressing shift-return gives it, without drop-caps, para-above para-below gaps, etc.

Example of use case from 0007587
Am trying to find an easier way to get the first two characters to act as Drop Caps. Since we are using this wonderful program in Spanish, many of our "first paragraphs" begin with either an upside down question mark or the upside down exclamation mark and the paragraph looks pretty strange with only the upside down question mark as a Drop Cap.
Tagsdropcap
PatchNo

Relationships

has duplicate 0007587 closed Drop caps with more than one character 
has duplicate 0015339 closedjghali Drop Caps for more than one character 
has duplicate 0015760 closedjghali Drop caps applying only single charecter 
has duplicate 0016136 closedjghali Drop Caps work only for single character 
related to 0000461 acknowledged drop-cap position for hanging, jutting, etc 
related to 0010338 closedfschmid [new feature] patch for applying char styles to Drop Cap and on-demand control in PP 
related to 0001337 confirmed Drop caps only drop first char of a paragraph beginning with a quote 

Activities

mhanski

2006-04-13 19:14

developer   ~0009909

Planned in Roadmap Extras http://wiki.scribus.net/index.php/1.3.x_Roadmap_Extras
with no ETA

Changing the status to acknowledged

Kunda

2016-04-11 19:06

updater   ~0040086

merged 0007587
altered summary and ticket details.
Assigned to Future Milestone

enzogian

2018-05-28 08:43

reporter   ~0045277

Yes, this is a very useful resource (effect) and many times you need to use in a big number of articles (if is a big magazine or glossy) .. I am glad to read the feature is in the planned roadmap (?)
thanks a lot

buttar

2025-05-15 06:12

reporter   ~0052537

20+ years! Wow. Scribus has come a long way. Anyway, I appreciate the team's efforts. These are minor things and one can get by without.

nicorikken

2026-06-26 14:50

reporter   ~0053904

I've also encountered this issue with use of punctuation or accents. More concrete in the translation of the Ada & Zangemann book, there are 3 exampled I encountered thus far:

Italian:

> C’era una volta una bambina di nome Ada.

Spanish:

> ¿Y Zangemann? No sé sabe mucho de él.

Hindi (although I'm not sure dropcaps are fitting there):

> कंप्यूटर ने ज़ैंगमैन को बचपन से ही आकर्षित किया था। जब वो छोटा था।

I attached screenshots of the situations. The Hindi one seems the worst to me, although I'm not sure drop caps are fitting for Hindi.

I'm not sure how other DTP software does this, but I could imagine a feature where Scribus will also dropcap any accents and punctuation surrounding the first readable character.
dropcap-issue-hindi.png (32,234 bytes)   
dropcap-issue-hindi.png (32,234 bytes)   
dropcap-issue-italian.png (26,108 bytes)   
dropcap-issue-italian.png (26,108 bytes)   
dropcap-issue-spanish.png (39,493 bytes)   
dropcap-issue-spanish.png (39,493 bytes)   

buttar

2026-06-27 00:58

reporter   ~0053905

I don't think I have ever seen a Hindi, Punjabi, or any other Indian language book with dropcap (only seen in English).

Moreover, this feature would be nice if one could extend dropcap to more than one charavter as it most likely would solve the diacritic marks separating from the letter as you can see the first screenshot (Hindi) by @nicorikken. It is called a bindi (literally means a dot) as you can see was not supposed to be shown separated (with a dottled circle).

However, I am curious how this feature, if implemwnted, would handle half letters (yes there are literally half letters in many Indian languages), for example in the other, more common, spelling of the worrd computer in Hindi: कम्प्यूटर (the second letter is half)

buttar

2026-06-28 02:42

reporter   ~0053908

Also, some diacritic marks are placed on the left of the letter, meaning they comes first and then comes the letter. Tin this case only the mark would be enlarged, leading to messy composition.

ale

2026-07-02 08:11

manager   ~0053924

Nico posted this article about drop caps in different languages: https://barefootliam.blogspot.com/2014/05/drop-caps-other-writing-systems-other.html

buttar

2026-07-02 13:07

reporter   ~0053927

Yes, you can apply DropCap to any language/script, of course. The article talks more about the complications of using (if/when used) DropCap for Indic scripts rather than the prevalence of such a style in them.

What I meant that I have rarely encountered DropCaps (especially in Hindi and Punjabi books/newspapers). Maybe it is not a tradition anymore.

The blog article Nico posted mentions/refers to Indic language Requirements Document (https://www.w3.org/TR/ilreq/). And the examples here are pretty much the only Indic language DropCaps I have seen (for Punjabi and Hindi, I mean. I cannot say for other Indic languages. I don't speak those languages).

Here (at https://www.w3.org/TR/ilreq/), only section 3.2 seems to have a real example of DropCaps (in Hindi). The section 5.2 too talks about DropCaps but it seems they are (digitally) reproduced to discuss the concept.

All in all, I want to say that if one decides to use DropCaps for Indic languages, 1 character DropCap would hardly suffice. It is more about DropCapping a syllable which is often made up of two or sometimes more characters.

Fahad

2026-07-15 04:02

developer   ~0054035

The layout & logic is already there. The remaining parts are GUI & reading and saving. IMHO, implementing what libreoffice has is enough unless there are new ideas to make scribus special.
dropcap-libreoffice.png (19,001 bytes)   
dropcap-libreoffice.png (19,001 bytes)   

buttar

2026-07-19 01:27

reporter   ~0054052

That would suffice, I think.

Fahad

2026-07-19 09:40

developer   ~0054054

There is a bug requesting to apply the Drop Cap only for the first paragraph only. This is logical thing to do in my opinion

nicorikken

2026-07-26 10:58

reporter   ~0054070

@Fahad you mean this issue https://bugs.scribus.net/view.php?id=204 right? To me those issues are clearly separate: this one is about the amount of characters, the other is about doing it only for the first paragraph.

buttar

2026-09-06 08:26

reporter   ~0054426

I gave it a try. Here is what I could manage to achieve. Pls test @a.le, @Fahad, @nicorikken.

#Multi-character Drop Caps

- Adds configurable multi-character drop caps via new `DropCapChars` paragraph-style property.
- Uses grapheme-safe counting so combining marks/conjuncts stay with their base characters.
- Treats the cap prefix as one shaped block for sizing, spacing, RTL handling and justification.
- Adds Vertical Offset (%) in both PP and SM.
- Fixes editing geometry so caret, mouse hit-testing and selection match the enlarged cap.
- Persists new drop-cap settings in the Scribus 1.7.1 format with backward-compatible defaults.
- Keeps ordinary single-character drop caps on the legacy path where possible.

(The next patch (dc2) will preserves multi-character drop-cap counts from IDML DropCapCharacters and XTG charCount during import instead of reducing them to simple on/off drop-cap state.)
dc1-multichar-dropcaps-core-v1.04.patch (53,036 bytes)   
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27816)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -846,10 +846,10 @@
 		*/
 
 		int startItem = 0;
-		if (glyphs[startItem].hasFlag(ScLayout_DropCap))
-		{
-			naturalWidth += glyphs[startItem].width();  // add the drop cap's own width first
-			startItem++;                                // then skip it in the loop below
+		while (startItem < glyphsCount && glyphs[startItem].hasFlag(ScLayout_DropCap))
+		{
+			naturalWidth += glyphs[startItem].width();
+			++startItem;
 		}
 		// distribute whitespace on spaces and glyphs
 		for (int i = startItem; i < glyphsCount; ++i)
@@ -1240,6 +1240,12 @@
 	double desc=0, asce=0, realAsce=0, realDesc = 0, offset = 0;
 	double maxDY=0, maxDX=0;
 	double DropCapDrop = 0;
+	double DropCapChs = 0.0, DropCapChsd = 0.0;
+	double DropCapEffectWidth = 0.0;
+	double DropCapPrefixAscent = 0.0, DropCapPrefixDescent = 0.0;
+	double DropCapScale = 1.0;
+	double DropCapYOffset = 0.0;
+	bool   DropCapSizeSet = false, DropCapUseCommonScale = false;
 	int    DropLines = 0;
 	int    DropLinesCount = 0;
 
@@ -1415,6 +1421,7 @@
 				continue;
 
 			int a = current.glyphs[currentIndex].firstChar();
+			const bool sourceDropCap = current.glyphs[currentIndex].hasFlag(ScLayout_DropCap);
 			bool HasObject = itemText.hasObject(a);
 			PageItem* currentObject = itemText.object(a).getPageItem(m_Doc);
 			QRectF currentObjectBox;
@@ -1449,6 +1456,48 @@
 				{
 					BulNumMode = true;
 				}
+			}
+
+			DropCmode = style.hasDropCap() && sourceDropCap;
+			bool DropCapLast = DropCmode;
+			if (DropCmode && (i + 1 < glyphClusters.count()))
+				DropCapLast = !glyphClusters[i + 1].hasFlag(ScLayout_DropCap);
+
+			const bool firstDropCapCluster = DropCmode && itemText.isBlockStart(a)
+				&& (i == 0 || glyphClusters[i - 1].firstChar() != a);
+			if (firstDropCapCluster)
+			{
+				DropLines = style.dropCapLines();
+				DropCapSizeSet = false;
+				DropCapUseCommonScale = (style.dropCapChars() > 1);
+				DropCapYOffset = 0.0;
+				DropCapEffectWidth = 0.0;
+				DropCapPrefixAscent = DropCapPrefixDescent = 0.0;
+				int dropCapClusterCount = 0;
+				for (int j = i; j < glyphClusters.count() && glyphClusters[j].hasFlag(ScLayout_DropCap); ++j)
+				{
+					const GlyphCluster& capCluster = glyphClusters[j];
+					++dropCapClusterCount;
+					DropCapEffectWidth += capCluster.width();
+
+					const CharStyle& capStyle = capCluster.style();
+					const ScFace& capFont = capStyle.font();
+					if (capCluster.glyphs().count() != 1 || capCluster.firstChar() != capCluster.lastChar())
+						DropCapUseCommonScale = true;
+					for (const GlyphLayout& gl : capCluster.glyphs())
+					{
+						if (qFuzzyIsNull(gl.xadvance))
+							DropCapUseCommonScale = true;
+
+						GlyphMetrics gm = capFont.glyphBBox(gl.glyph, capStyle.fontSize() / 10.0);
+						const double glyphAscent = gm.ascent * capCluster.scaleV() - gl.yoffset;
+						const double glyphDescent = gm.descent * capCluster.scaleV() + gl.yoffset;
+						DropCapPrefixAscent = qMax(DropCapPrefixAscent, qMax(0.0, glyphAscent));
+						DropCapPrefixDescent = qMax(DropCapPrefixDescent, qMax(0.0, glyphDescent));
+					}
+				}
+				if (dropCapClusterCount > 1)
+					DropCapUseCommonScale = true;
 			}
 
 			if (current.isEmpty)
@@ -1472,22 +1521,9 @@
 			style.setLineSpacing (calculateLineSpacing (style, this));
 			FlopBaseline = (current.startOfCol && firstLineOffset() == FLOPBaselineGrid);
 
-			// find out about par gap and dropcap
-			if (a == firstInFrame())
-			{
-				if (itemText.isBlockStart(a))
-				{
-					if (!itemText.isBlockStart(a + 1))
-					{
-						DropCmode = style.hasDropCap();
-						if (DropCmode)
-							DropLines = style.dropCapLines();
-					}
-					else
-						DropCmode = false;
-					current.lineData.isFirstLine = true;
-				}
-			}
+			// find out about paragraph gap and first-line state
+			if (a == firstInFrame() && itemText.isBlockStart(a))
+				current.lineData.isFirstLine = true;
 
 			const ScFace& font = charStyle.font();
 
@@ -1541,16 +1577,6 @@
 					if (!current.afterOverflow && current.recalculateY && !current.startOfCol)
 						current.yPos += style.gapBefore();
 					DropCapDrop = 0;
-					if (!itemText.isBlockStart(a + 1))
-						DropCmode = style.hasDropCap();
-					else
-						DropCmode = false;
-					if (DropCmode && !current.afterOverflow)
-					{
-						DropLines = style.dropCapLines();
-//						DropCapDrop = calculateLineSpacing (style, this) * (DropLines - 1);
-//						qDebug() << QString("dropcapdrop: y=%1+%2").arg(current.yPos).arg(DropCapDrop);
-					}
 					current.lineData.isFirstLine = true;
 				}
 			}
@@ -1574,8 +1600,33 @@
 					realCharAscent = font.ascent(style.charStyle().fontSize() / 10.0);
 				if (current.startOfCol && (m_firstLineOffset == FLOPFontAscent))
 					realCharAscent = font.ascent(hlcsize10);
-				chsd = ((DropCapDrop + realCharAscent) / realCharHeight) * style.charStyle().fontSize();
-				chs  = ((DropCapDrop + realCharAscent) / realCharAscent) * style.charStyle().fontSize();
+				if (DropCapUseCommonScale)
+				{
+					if (!DropCapSizeSet)
+					{
+						const double prefixHeight = DropCapPrefixAscent + DropCapPrefixDescent;
+						DropCapScale = (prefixHeight > 0.0) ? (DropCapDrop + DropCapPrefixAscent) / prefixHeight : 1.0;
+						DropCapSizeSet = true;
+					}
+					chsd = charStyle.fontSize() * DropCapScale;
+					chs = chsd;
+				}
+				else if (!DropCapSizeSet)
+				{
+					// Preserve the legacy single-character sizing exactly.
+					chsd = ((DropCapDrop + realCharAscent) / realCharHeight) * style.charStyle().fontSize();
+					chs  = ((DropCapDrop + realCharAscent) / realCharAscent) * style.charStyle().fontSize();
+					DropCapChsd = chsd;
+					DropCapChs = chs;
+					DropCapSizeSet = true;
+				}
+				else
+				{
+					chsd = DropCapChsd;
+					chs = DropCapChs;
+				}
+				if (firstDropCapCluster)
+					DropCapYOffset = (chsd / 10.0) * style.dropCapOffsetY() / 100.0;
 				current.glyphs[currentIndex].setFlag(ScLayout_DropCap);
 				if (HasObject)
 				{
@@ -1601,10 +1652,17 @@
 
 			if (!current.glyphs[currentIndex].hasFlag(ScLayout_StartOfLine))
 			{
-				double tracking = charStyle.fontSize() * charStyle.tracking() / 10000.0;
-				current.glyphs[currentIndex].xoffset += tracking;
-				if (current.glyphs[currentIndex].width() != 0)
-					current.glyphs[currentIndex].extraWidth += tracking;
+				// A zero-advance cluster inside a shaped drop-cap band is normally an
+				// attached mark. Tracking must not move it away from its HarfBuzz anchor.
+				const bool attachedDropCapCluster = DropCmode && DropCapUseCommonScale
+					&& qFuzzyIsNull(current.glyphs[currentIndex].width());
+				if (!attachedDropCapCluster)
+				{
+					double tracking = charStyle.fontSize() * charStyle.tracking() / 10000.0;
+					current.glyphs[currentIndex].xoffset += tracking;
+					if (current.glyphs[currentIndex].width() != 0)
+						current.glyphs[currentIndex].extraWidth += tracking;
+				}
 			}
 
 //			glyphs->yadvance = 0;
@@ -1641,6 +1699,26 @@
 					glyphCluster.setScaleV(realAsce / itemHeight);
 					glyphCluster.setScaleH(glyphCluster.scaleH() * glyphCluster.scaleV());
 					wide *= glyphCluster.scaleH();
+				}
+				else if (DropCapUseCommonScale)
+				{
+					// Preserve the HarfBuzz geometry of the complete shaped prefix. The
+					// advances define horizontal placement (including zero-advance marks),
+					// while GPOS offsets grow by the same factor as the glyph outlines.
+					for (GlyphLayout& gl : glyphCluster.glyphs())
+					{
+						gl.xoffset *= DropCapScale;
+						gl.yoffset *= DropCapScale;
+					}
+					glyphCluster.setScaleH(glyphCluster.scaleH() * DropCapScale);
+					glyphCluster.setScaleV(glyphCluster.scaleV() * DropCapScale);
+					wide = glyphCluster.width();
+					realAsce = (DropCapPrefixAscent + DropCapPrefixDescent) * DropCapScale;
+					asce = DropCapPrefixAscent;
+					if (current.startOfCol && (m_firstLineOffset == FLOPFontAscent))
+						asce = font.ascent(hlcsize10);
+					if (firstDropCapCluster)
+						glyphCluster.xoffset -= 0.5; // apply the legacy edge correction once per cap band
 				}
 				else
 				{
@@ -1827,13 +1905,16 @@
 					{
 						if (style.parEffectIndent())
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+							double effectWidth = DropCmode ? DropCapEffectWidth : 0.0;
+							if (!DropCmode)
 							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
+								for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+								{
+									const auto & glyph = glyphClusters[j];
+									if (glyph.firstChar() != a)
+										break;
+									effectWidth += glyph.width();
+								}
 							}
 
 							if (style.direction() == ParagraphStyle::RTL)
@@ -2184,10 +2265,16 @@
 			if (DropCmode)
 			{
 				double yoffset = 0.0;
-				const QList<GlyphLayout>& glyphs = current.glyphs[currentIndex].glyphs();
-				for (const GlyphLayout& gl : glyphs)
-					yoffset = qMax(yoffset, font.glyphBBox(gl.glyph, chsd / 10.0).descent);
+				if (DropCapUseCommonScale)
+					yoffset = DropCapPrefixDescent * DropCapScale;
+				else
+				{
+					const QList<GlyphLayout>& glyphs = current.glyphs[currentIndex].glyphs();
+					for (const GlyphLayout& gl : glyphs)
+						yoffset = qMax(yoffset, font.glyphBBox(gl.glyph, chsd / 10.0).descent);
+				}
 				current.glyphs[currentIndex].yoffset -= yoffset;
+				current.glyphs[currentIndex].yoffset += DropCapYOffset;
 			}
 			// remember x pos
 			double breakPos = current.xPos;
@@ -2398,10 +2485,11 @@
 			}
 
 			// hyphenation
-			if ((current.glyphs[currentIndex].hasFlag(ScLayout_HyphenationPossible)
+			if (!(DropCmode && !DropCapLast)
+				&& (current.glyphs[currentIndex].hasFlag(ScLayout_HyphenationPossible)
 				  || itemText.text(a) == '-'
 				  || itemText.text(a) == SpecialChars::SHYPHEN)
-				 && (!outs) && ((i == 0) || !itemText.text(glyphClusters[i - 1].lastChar()).isSpace()) )
+				&& (!outs) && ((i == 0) || !itemText.text(glyphClusters[i - 1].lastChar()).isSpace()) )
 			{
 				breakPos = current.xPos;
 				if (itemText.text(a) != '-')
@@ -2449,68 +2537,94 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
-				// for bulleted lists, make sure offset is applied only after last bullet char
-				// for numbered lists, make sure that offset is applied only after the suffix
-				// loop over previous current.glyphs and set their extraWidth to 0.0
-				for (int j = 0; j < current.glyphs.size(); j++)
-				{
-					if (j != currentIndex)
-					{
-						GlyphCluster& currentGlyph = current.glyphs[j];
-						current.xPos -= currentGlyph.extraWidth;
-						currentGlyph.extraWidth = 0.0;
-					}
-				}
-				// 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)
+				const bool finalizeDropCap = DropCmode && DropCapLast;
+				const bool applyEffectOffset = BulNumMode || finalizeDropCap;
+				if (applyEffectOffset)
+				{
+					current.xPos += style.parEffectOffset();
+					// Bullet/number markers can span multiple clusters for one source
+					// character; retain their existing last-cluster offset behavior.
+					if (BulNumMode)
+					{
+						for (int j = 0; j < current.glyphs.size(); j++)
+						{
+							if (j != currentIndex)
+							{
+								GlyphCluster& currentGlyph = current.glyphs[j];
+								current.xPos -= currentGlyph.extraWidth;
+								currentGlyph.extraWidth = 0.0;
+							}
+						}
+					}
+
+					// A multi-character drop cap gets one offset, after its final cluster.
+					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.
+					if (style.direction() == ParagraphStyle::RTL)
+						current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+				}
+
+				if (finalizeDropCap)
 				{
 					DropCmode = false;
 					DropLinesCount = 0;
 					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);
+
+					int firstDropIndex = currentIndex;
+					for (int j = 0; j <= currentIndex; ++j)
+					{
+						if (current.glyphs[j].hasFlag(ScLayout_DropCap))
+						{
+							firstDropIndex = j;
+							break;
+						}
+					}
+
+					// Strong-LTR first character keeps LTR placement even in an RTL paragraph.
+					// Weak types and strong-RTL characters follow the paragraph's RTL side.
+					current.dropCapIsLTR = (itemText.text(current.glyphs[firstDropIndex].firstChar()).direction() == QChar::DirL);
 					if (style.direction() == ParagraphStyle::RTL)
 					{
-						GlyphCluster& cap = current.glyphs[currentIndex];
-						double sizePt = charStyle.fontSize() / 10.0;
+						// The first logical cluster forms the outer edge of the RTL cap block.
+						// Extend its box for ink overhang exactly as for the legacy one-char cap.
+						GlyphCluster& cap = current.glyphs[firstDropIndex];
+						const CharStyle& capStyle = itemText.charStyle(cap.firstChar());
+						const ScFace& capFont = capStyle.font();
+						double sizePt = capStyle.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)
+							double xMax = capFont.glyphBBox(g.glyph, sizePt).width;
+							QRectF ob = capFont.glyphOutline(g.glyph, sizePt).boundingRect();
+							double outlineScale = (ob.right() != 0.0) ? xMax / ob.right() : 0.0;
+							double xMin = ob.left() * outlineScale;
 							inkRight = qMax(inkRight, pen + xMax);
-							inkLeft  = qMin(inkLeft,  pen + xMin);   // <0 only if ink bleeds left of origin
+							inkLeft = qMin(inkLeft, pen + xMin);
 							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)
+						double overhang = qMax(0.0, inkRight - pen) * cap.scaleH();
+						double leftBleed = qMax(0.0, -inkLeft) * cap.scaleH();
 						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
+							cap.extraWidth += overhang + leftBleed;
+							cap.xoffset += leftBleed;
 						}
 					}
+
+					// Reserve the complete multi-character cap band (including its one offset)
+					// for following lines.
+					current.dropCapWidth = 0.0;
+					for (int j = firstDropIndex; j <= currentIndex; ++j)
+					{
+						if (current.glyphs[j].hasFlag(ScLayout_DropCap))
+							current.dropCapWidth += current.glyphs[j].width();
+					}
+
 					double spacing = calculateLineSpacing (style, this);
 					current.yPos -= spacing * (DropLines - 1);
 					if (style.lineSpacingMode() == ParagraphStyle::BaselineGridLineSpacing)
@@ -2605,7 +2719,7 @@
 					// for now, such chars is only spaces but may change in the future
 					// e.g. https://www.w3.org/TR/css-text-3/#hanging
 					QChar ch = itemText.text(a);
-					if (ch.isSpace() && SpecialChars::isBreakingSpace(ch))
+					if (!(DropCmode && !DropCapLast) && ch.isSpace() && SpecialChars::isBreakingSpace(ch))
 						current.rememberBreak(i, breakPos, style.rightMargin());
 
 					if (current.breakIndex >= 0)
@@ -2754,9 +2868,10 @@
 					{
 						if (current.glyphs[0].hasFlag(ScLayout_DropCap))
 						{
-							// put line back to top
+							// Put the line back to the top and shift the complete cap prefix.
 							current.lineData.y -= DropCapDrop;
-							current.glyphs[0].yoffset += DropCapDrop;
+							for (int j = 0; j < current.glyphs.count() && current.glyphs[j].hasFlag(ScLayout_DropCap); ++j)
+								current.glyphs[j].yoffset += DropCapDrop;
 						}
 						current.fillInTabLeaders();
 						//if right margin is set we temporally save line, not append it
@@ -2882,7 +2997,7 @@
 				if (shapedText.haveMoreText(i + 1, glyphClusters))
 				{
 					const GlyphCluster& nextCluster = glyphClusters[i + 1];
-					if (nextCluster.hasFlag(ScLayout_LineBoundary))
+					if (nextCluster.hasFlag(ScLayout_LineBoundary) && !nextCluster.hasFlag(ScLayout_DropCap))
 					{
 						// #16100: why preventing possible line break when there are two
 						// consecutive line break opportunities? This is bad for CJK. /
@@ -2895,7 +3010,7 @@
 						}
 					}
 				}
-				if (HasObject)
+				if (HasObject && !(DropCmode && !DropCapLast))
 				{
 //					qDebug() << "rememberBreak object @" << i;
 					current.rememberBreak(i, breakPos, style.rightMargin());
@@ -3018,9 +3133,10 @@
 			}
 			if (current.glyphs[0].hasFlag(ScLayout_DropCap))
 			{
-				// put line back to top
+				// Put the line back to the top and shift the complete cap prefix.
 				current.lineData.y -= DropCapDrop;
-				current.glyphs[0].yoffset += DropCapDrop;
+				for (int j = 0; j < current.glyphs.count() && current.glyphs[j].hasFlag(ScLayout_DropCap); ++j)
+					current.glyphs[j].yoffset += DropCapDrop;
 			}
 			current.fillInTabLeaders();
 			current.startOfCol = false;
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27816)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -2520,6 +2520,8 @@
 			pstyle.resetGapAfter();
 		if (pstyle.dropCapLines() < 0)
 			pstyle.resetDropCapLines();
+		if (pstyle.dropCapChars() < 1)
+			pstyle.resetDropCapChars();
 		if (pstyle.parEffectOffset() <= NOVALUE)
 			pstyle.resetParEffectOffset();
 		fixLegacyCharStyle(pstyle.charStyle());
@@ -3654,6 +3656,10 @@
 		newStyle.setDropCapLines(attrs.valueAsInt("DROPLIN"));
 	else if (attrs.hasAttribute("DropCapLines"))
 		newStyle.setDropCapLines(attrs.valueAsInt("DropCapLines"));
+	if (attrs.hasAttribute("DropCapChars"))
+		newStyle.setDropCapChars(qMax(1, attrs.valueAsInt("DropCapChars")));
+	if (attrs.hasAttribute("DropCapOffsetY"))
+		newStyle.setDropCapOffsetY(attrs.valueAsDouble("DropCapOffsetY"));
 
 	// static const QString DROPDIST("DROPDIST");
 	// if (attrs.hasAttribute(DROPDIST))
@@ -6697,6 +6703,10 @@
 	else
 		if (attrs.hasAttribute("DropCapLines"))
 			pstyle.setDropCapLines(attrs.valueAsInt("DropCapLines"));
+	if (attrs.hasAttribute("DropCapChars"))
+		pstyle.setDropCapChars(qMax(1, attrs.valueAsInt("DropCapChars")));
+	if (attrs.hasAttribute("DropCapOffsetY"))
+		pstyle.setDropCapOffsetY(attrs.valueAsDouble("DropCapOffsetY"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROPDIST"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("DROPDIST"));
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27816)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -852,6 +852,10 @@
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
 		docu.writeAttribute("DropCapLines", style.dropCapLines());
+	if (!style.isInhDropCapChars())
+		docu.writeAttribute("DropCapChars", style.dropCapChars());
+	if (!style.isInhDropCapOffsetY())
+		docu.writeAttribute("DropCapOffsetY", style.dropCapOffsetY());
 	if (!style.isInhHasBullet())
 		docu.writeAttribute("Bullet", static_cast<int>(style.hasBullet()));
 	if (!style.isInhBulletStr())
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27816)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -39,6 +39,8 @@
 ATTRDEF(bool, keepTogether, KeepTogether, false)
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
+ATTRDEF(int, dropCapChars, DropCapChars, 1)
+ATTRDEF(double, dropCapOffsetY, DropCapOffsetY, 0.0)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
 ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
Index: scribus/text/boxes.h
===================================================================
--- scribus/text/boxes.h	(revision 27816)
+++ scribus/text/boxes.h	(working copy)
@@ -215,6 +215,8 @@
 	void removeBox(int i) override;
 
 protected:
+	QRectF dropCapBounds() const;
+	int dropCapPointToPosition(const QPointF& coord, const StoryText& story) const;
 	virtual void drawBackGround(TextLayoutPainter *p) const;
 	virtual void update();
 };
@@ -253,7 +255,9 @@
 	void render(TextLayoutPainter *p) const override;
 	void render(ScreenPainter *p, ITextContext *ctx) const override;
 	void drawSelection(ScreenPainter *p, ITextContext *ctx) const override;
-
+	void drawSelection(ScreenPainter *p, ITextContext *ctx, double top, double bottom) const;
+
+	QRectF visualBounds() const;
 	GlyphCluster glyphRun() const { return m_glyphRun; }
 
 	const CharStyle& style() const { return m_glyphRun.style(); }
Index: scribus/text/boxes.cpp
===================================================================
--- scribus/text/boxes.cpp	(revision 27816)
+++ scribus/text/boxes.cpp	(working copy)
@@ -6,6 +6,7 @@
  */
 
 #include <cmath>
+#include <limits>
 #include <unicode/brkiter.h>
 
 #include "pageitem.h"
@@ -155,7 +156,11 @@
 
 int LineBox::pointToPosition(const QPointF& coord, const StoryText &story) const
 {
-	int position = GroupBox::pointToPosition(coord, story);
+	int position = dropCapPointToPosition(coord, story);
+	if (position >= 0)
+		return position;
+
+	position = GroupBox::pointToPosition(coord, story);
 	if (position < 0 && containsPoint(coord))
 	{
 		const ParagraphStyle& style = story.paragraphStyle(firstChar());
@@ -193,16 +198,98 @@
 		if (box->containsPos(pos))
 		{
 			double xPos = x() + box->positionToPoint(pos, story).x1();
-			result = QLineF(xPos, y(), xPos, y() + height());
+			const GlyphBox* glyphBox = dynamic_cast<const GlyphBox*>(box);
+			if (glyphBox && glyphBox->glyphRun().hasFlag(ScLayout_DropCap))
+			{
+				const QRectF capRect = dropCapBounds();
+				if (!capRect.isEmpty())
+					result = QLineF(xPos, capRect.top(), xPos, capRect.bottom());
+				else
+					result = QLineF(xPos, y(), xPos, y() + height());
+			}
+			else
+				result = QLineF(xPos, y(), xPos, y() + height());
 			break;
 		}
 	}
 	return result;
 }
 
+QRectF LineBox::dropCapBounds() const
+{
+	QRectF bounds;
+	bool haveBounds = false;
+	for (const Box* box : boxes())
+	{
+		const GlyphBox* glyphBox = dynamic_cast<const GlyphBox*>(box);
+		if (!glyphBox || !glyphBox->glyphRun().hasFlag(ScLayout_DropCap))
+			continue;
+
+		QRectF glyphBounds = glyphBox->visualBounds();
+		if (glyphBounds.isEmpty())
+			continue;
+		glyphBounds.translate(x(), y() + ascent());
+		if (!haveBounds)
+		{
+			bounds = glyphBounds;
+			haveBounds = true;
+		}
+		else
+			bounds = bounds.united(glyphBounds);
+	}
+	return bounds;
+}
+
+int LineBox::dropCapPointToPosition(const QPointF& coord, const StoryText& story) const
+{
+	const QRectF capRect = dropCapBounds();
+	if (capRect.isEmpty() || !capRect.contains(coord))
+		return -1;
+
+	int capFirst = INT_MAX;
+	int capLast = INT_MIN;
+	for (const Box* box : boxes())
+	{
+		const GlyphBox* glyphBox = dynamic_cast<const GlyphBox*>(box);
+		if (!glyphBox || !glyphBox->glyphRun().hasFlag(ScLayout_DropCap))
+			continue;
+		capFirst = qMin(capFirst, glyphBox->firstChar());
+		capLast = qMax(capLast, glyphBox->lastChar());
+	}
+	if (capFirst == INT_MAX || capLast < capFirst)
+		return -1;
+
+	int bestPosition = -1;
+	double bestDistance = std::numeric_limits<double>::max();
+	auto considerPosition = [&](int pos)
+	{
+		const QLineF caret = positionToPoint(pos, story);
+		if (caret.isNull())
+			return;
+		const double distance = qAbs(coord.x() - caret.x1());
+		if (distance < bestDistance)
+		{
+			bestDistance = distance;
+			bestPosition = pos;
+		}
+	};
+
+	considerPosition(capFirst);
+	BreakIterator* it = StoryText::getGraphemeIterator();
+	QString text(story.text(capFirst, capLast - capFirst + 1));
+	UnicodeString unicodeStr((const UChar*) text.utf16());
+	it->setText(unicodeStr);
+	for (int boundary = it->first(); (boundary = it->next()) != BreakIterator::DONE;)
+		considerPosition(capFirst + boundary);
+
+	return bestPosition;
+}
+
 bool LineBox::containsPoint(const QPointF& coord) const
 {
-	return QRectF(0, m_y, m_width, height()).contains(coord);
+	if (QRectF(0, m_y, m_width, height()).contains(coord))
+		return true;
+	return dropCapBounds().contains(coord);
 }
 
 void LineBox::render(TextLayoutPainter *p) const
@@ -240,8 +327,17 @@
 {
 	p->save();
 	p->translate(0, ascent());
+	const QRectF capRect = dropCapBounds();
+	const double capTop = capRect.top() - y() - ascent();
+	const double capBottom = capRect.bottom() - y() - ascent();
 	for (const Box *box : boxes())
-		box->drawSelection(p, ctx);
+	{
+		const GlyphBox* glyphBox = dynamic_cast<const GlyphBox*>(box);
+		if (glyphBox && glyphBox->glyphRun().hasFlag(ScLayout_DropCap) && !capRect.isEmpty())
+			glyphBox->drawSelection(p, ctx, capTop, capBottom);
+		else
+			box->drawSelection(p, ctx);
+	}
 	p->restore();
 }
 
@@ -423,8 +519,13 @@
 		*/
 
 	int startItem = 0;
-	if (dynamic_cast<GlyphBox*>(m_boxes[startItem])->glyphs.hasFlag(ScLayout_DropCap))
-		startItem++;
+	while (startItem < m_boxes.count())
+	{
+		GlyphBox* box = dynamic_cast<GlyphBox*>(m_boxes[startItem]);
+		if (!box || !box->glyphs.hasFlag(ScLayout_DropCap))
+			break;
+		++startItem;
+	}
 
 	// distribute whitespace on spaces and glyphs
 	for (int i = startItem; i < m_boxes.count(); ++i)
@@ -524,10 +625,11 @@
 			p->setSelected(true);
 			qreal firstX = positionToPoint(selectionFirst, item->itemText).x1();
 			qreal lastX = positionToPoint(selectionLast + 1, item->itemText).x1();
+			QRectF visualRect = m_glyphRun.hasFlag(ScLayout_DropCap) ? visualBounds() : QRectF(x(), y(), width(), height());
 			if (m_glyphRun.hasFlag(ScLayout_RightToLeft))
-				p->clip(QRectF(lastX, y(), firstX - lastX, height()));
-			else
-				p->clip(QRectF(firstX, y(), lastX - firstX, height()));
+				p->clip(QRectF(lastX, visualRect.top(), firstX - lastX, visualRect.height()));
+			else
+				p->clip(QRectF(firstX, visualRect.top(), lastX - firstX, visualRect.height()));
 			render(p);
 			p->restoreState();
 		}
@@ -540,6 +642,11 @@
 }
 
 void GlyphBox::drawSelection(ScreenPainter *p, ITextContext *ctx) const
+{
+	drawSelection(p, ctx, -ascent(), -descent());
+}
+
+void GlyphBox::drawSelection(ScreenPainter *p, ITextContext *ctx, double top, double bottom) const
 {
 	const PageItem* item = ctx->getFrame();
 	bool s = p->selected();
@@ -558,7 +665,10 @@
 		}
 	}
 
-	QRectF rect(x(), -ascent(), width(), height());
+	QRectF visualRect = m_glyphRun.hasFlag(ScLayout_DropCap) ? visualBounds() : QRectF(x(), top, width(), bottom - top);
+	const double left = qMin(x(), visualRect.left());
+	const double right = qMax(x() + width(), visualRect.right());
+	QRectF rect(left, top, right - left, bottom - top);
 	p->setSelected(true);
 	p->setStrokeWidth(0);
 
@@ -574,15 +684,58 @@
 			qreal firstX = positionToPoint(selectionFirst, item->itemText).x1();
 			qreal lastX = positionToPoint(selectionLast + 1, item->itemText).x1();
 			if (m_glyphRun.hasFlag(ScLayout_RightToLeft))
-				rect = QRectF(lastX, -ascent(), firstX - lastX, height());
-			else
-				rect = QRectF(firstX, -ascent(), lastX - firstX, height());
-			p->drawRect(rect);
+				rect = QRectF(lastX, top, firstX - lastX, bottom - top);
+			else
+				rect = QRectF(firstX, top, lastX - firstX, bottom - top);
+			p->drawRect(rect.normalized());
 		}
 	}
 
 	p->setSelected(s);
 	p->setStrokeWidth(sw);
+}
+
+QRectF GlyphBox::visualBounds() const
+{
+	if (!m_glyphRun.hasFlag(ScLayout_DropCap))
+		return QRectF(x(), -ascent(), width(), height());
+
+	const CharStyle& charStyle = style();
+	const ScFace& font = charStyle.font();
+	const double fontSize = charStyle.fontSize() / 10.0;
+	const double scaleH = m_glyphRun.scaleH();
+	const double scaleV = m_glyphRun.scaleV();
+	const double styleYOffset = -fontSize * (charStyle.baselineOffset() / 1000.0);
+	double advance = 0.0;
+	QRectF bounds;
+	bool haveBounds = false;
+
+	for (const GlyphLayout& gl : m_glyphRun.glyphs())
+	{
+		const GlyphMetrics gm = font.glyphBBox(gl.glyph, fontSize);
+		const double glyphX = x() + m_glyphRun.xoffset + advance * scaleH + gl.xoffset;
+		const double glyphY = y() + m_glyphRun.yoffset + styleYOffset + gl.yoffset;
+		QRectF glyphRect(glyphX, glyphY - gm.ascent * scaleV,
+			gm.width * scaleH, (gm.ascent + gm.descent) * scaleV);
+		glyphRect = glyphRect.normalized();
+		if (!haveBounds)
+		{
+			bounds = glyphRect;
+			haveBounds = true;
+		}
+		else
+			bounds = bounds.united(glyphRect);
+		advance += gl.xadvance;
+	}
+
+	if (!haveBounds)
+		return QRectF(x(), -ascent(), width(), height());
+
+	// Include the cluster's logical advance so the interactive region remains
+	// continuous even when a mark has ink but zero advance (or vice versa).
+	bounds.setLeft(qMin(bounds.left(), qMin(x(), x() + width())));
+	bounds.setRight(qMax(bounds.right(), qMax(x(), x() + width())));
+	return bounds.normalized();
 }
 
 void GlyphBox::render(TextLayoutPainter *p) const
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp	(revision 27816)
+++ scribus/text/textshaper.cpp	(working copy)
@@ -19,6 +19,54 @@
 
 using namespace icu;
 
+namespace
+{
+
+int paragraphEffectLength(const ITextSource& story, int start, int graphemeCount)
+{
+	if (graphemeCount <= 0 || start < 0 || start >= story.length())
+		return 0;
+
+	const QChar firstChar = story.text(start);
+	if (firstChar == SpecialChars::PARSEP)
+		return 0;
+	if (SpecialChars::isBreak(firstChar, true))
+		return 0;
+
+	int end = start;
+	while (end < story.length() && !SpecialChars::isBreak(story.text(end), true))
+		++end;
+	if (end <= start)
+		return 0;
+
+	const QString text = story.text(start, end - start);
+	BreakIterator* charIt = StoryText::getGraphemeIterator();
+	if (!charIt)
+	{
+		int length = qMin(graphemeCount, text.length());
+		if (length > 0 && length < text.length() && text.at(length - 1).isHighSurrogate() && text.at(length).isLowSurrogate())
+			++length;
+		return length;
+	}
+
+	icu::UnicodeString unicodeStr(true, (const UChar*) text.utf16(), text.length());
+	charIt->setText(unicodeStr);
+	int32_t boundary = charIt->first();
+	for (int count = 0; count < graphemeCount; ++count)
+	{
+		const int32_t next = charIt->next();
+		if (next == BreakIterator::DONE)
+		{
+			boundary = text.length();
+			break;
+		}
+		boundary = next;
+	}
+	return qMax(1, static_cast<int>(boundary));
+}
+
+}
+
 TextShaper::TextShaper(ITextContext* context, ITextSource &story, int firstChar, bool singlePar)
 	: m_context(context),
 	m_story(story),
@@ -205,33 +253,55 @@
 		
 		str.replace(SpecialChars::SHYPHEN, SpecialChars::ZWNJ);
 
-		//set style for paragraph effects
+		// Set the character style for paragraph effects. Drop caps may cover a
+		// grapheme-safe prefix; ScLayout_DropCap marks only the automatic range so
+		// a later count/style change can clean up characters that leave the prefix.
 		if (m_story.isBlockStart(i) && (m_context != nullptr) && (m_context->getDoc() != nullptr))
 		{
 			const ScribusDoc* doc = m_context->getDoc();
 			const ParagraphStyle& style = m_story.paragraphStyle(i);
+			const QString& curParent(style.hasParent() ? style.parent() : style.name());
+			const QString baseParent = doc->paragraphStyle(curParent).charStyle().name();
+			const int dropCapLength = style.hasDropCap() ? paragraphEffectLength(m_story, i, style.dropCapChars()) : 0;
+
+			// Remove automatically applied drop-cap styling outside the new prefix.
+			for (int pos = i + dropCapLength; pos < m_story.length() && m_story.hasFlag(pos, ScLayout_DropCap); ++pos)
+			{
+				CharStyle charStyle = m_story.charStyle(pos);
+				const QString oldParent = charStyle.parent();
+				if (oldParent != baseParent && doc->charStyles().contains(oldParent))
+					charStyle.eraseCharStyle(doc->charStyle(oldParent));
+				charStyle.setParent(baseParent);
+				m_story.setCharStyle(pos, 1, charStyle);
+				m_story.clearFlag(pos, ScLayout_DropCap);
+			}
+
 			if (style.hasDropCap() || style.hasBullet() || style.hasNum())
 			{
-				CharStyle charStyle = (m_story.text(i) != SpecialChars::PARSEP) ? m_story.charStyle(i) : style.charStyle();
-				const QString& curParent(style.hasParent() ? style.parent() : style.name());
-				CharStyle newStyle(charStyle);
-				if (style.peCharStyleName().isEmpty())
-					newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
-				else if (charStyle.name() != style.peCharStyleName())
-					newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
-				charStyle.setStyle(newStyle);
-				m_story.setCharStyle(i, 1, charStyle);
+				const int effectLength = style.hasDropCap() ? dropCapLength : 1;
+				for (int pos = i; pos < i + effectLength && pos < m_story.length(); ++pos)
+				{
+					CharStyle charStyle = (m_story.text(pos) != SpecialChars::PARSEP) ? m_story.charStyle(pos) : style.charStyle();
+					CharStyle newStyle(charStyle);
+					if (style.peCharStyleName().isEmpty())
+						newStyle.setParent(baseParent);
+					else if (charStyle.name() != style.peCharStyleName())
+						newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
+					charStyle.setStyle(newStyle);
+					m_story.setCharStyle(pos, 1, charStyle);
+					if (style.hasDropCap())
+						m_story.setFlag(pos, ScLayout_DropCap);
+				}
 			}
 			else if (!style.peCharStyleName().isEmpty())
 			{
-				//par effect is cleared but is set dcCharStyleName = clear drop cap char style
+				// Paragraph effect is cleared but its character style is still selected.
 				CharStyle charStyle = (m_story.text(i) != SpecialChars::PARSEP) ? m_story.charStyle(i) : style.charStyle();
 				if (charStyle.parent() == style.peCharStyleName())
 				{
-					const QString& curParent(style.hasParent() ? style.parent() : style.name());
 					if (doc->charStyles().contains(style.peCharStyleName()))
 						charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
-					charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
+					charStyle.setParent(baseParent);
 					m_story.setCharStyle(i, 1, charStyle);
 				}
 			}
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27816)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -27,6 +27,8 @@
 
 	languageChange();
 	dropCapLines->setValue(2);
+	dropCapChars->setValue(1);
+	dropCapOffsetY->setValue(0.0);
 
 	if (m_doc)
 		peCharStyleCombo->updateStyleList();
@@ -42,6 +44,10 @@
 	numSuffix->setMaximumWidth(QFontMetrics(numSuffix->font()).averageCharWidth() * 7);
 	dropCapLines->setMinimum(2);
 	dropCapLines->setMaximum(99);
+	dropCapChars->setMinimum(1);
+	dropCapChars->setMaximum(99);
+	dropCapOffsetY->setMinimum(-100.0);
+	dropCapOffsetY->setMaximum(100.0);
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
@@ -117,6 +123,8 @@
 	IconManager& iconManager = IconManager::instance();
 	bulletCharTableButton->setIcon(iconManager.loadIcon("tool-insert-table"));
 	peCharStyleLabel->setPixmap(iconManager.loadPixmap("character-style"));
+	dropCapOffsetYLabel->setPixmap(iconManager.loadPixmap("character-offset-baseline"));
+	dropCapOffsetYLabel->setContentsMargins(4, 0, 0, 0);
 	peOffsetLabel->setPixmap(iconManager.loadPixmap("paragraph-list-offset"));
 	peLabel->setPixmap(iconManager.loadPixmap("paragraph-effects-style"));
 }
@@ -222,6 +230,8 @@
 
 	QSignalBlocker blocker1(peCombo);
 	QSignalBlocker blocker2(dropCapLines);
+	QSignalBlocker blockerDropCapChars(dropCapChars);
+	QSignalBlocker blockerDropCapOffsetY(dropCapOffsetY);
 	QSignalBlocker blocker3(bulletStrEdit);
 	QSignalBlocker blocker4(numComboBox);
 	QSignalBlocker blocker5(numLevelSpin);
@@ -252,6 +262,8 @@
 
 	QString numName = numComboBox->currentText();
 	dropCapLines->setValue(newPStyle.dropCapLines());
+	dropCapChars->setValue(newPStyle.dropCapChars());
+	dropCapOffsetY->setValue(newPStyle.dropCapOffsetY());
 	bulletStrEdit->setEditText(newPStyle.bulletStr());
 	numName = newPStyle.numName();
 	if (numName.isEmpty())
@@ -280,6 +292,8 @@
 {
 	connect(peCombo, SIGNAL(activated(int)), this, SLOT(handleParEffectUse()), Qt::UniqueConnection);
 	connect(dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(handleDropCapLines(int)), Qt::UniqueConnection);
+	connect(dropCapChars, SIGNAL(valueChanged(int)), this, SLOT(handleDropCapChars(int)), Qt::UniqueConnection);
+	connect(dropCapOffsetY, SIGNAL(valueChanged(double)), this, SLOT(handleDropCapOffsetY(double)), Qt::UniqueConnection);
 	connect(bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(handleBulletStr(QString)), Qt::UniqueConnection);
 	connect(numComboBox, SIGNAL(textActivated(QString)), this, SLOT(handleNumName(QString)), Qt::UniqueConnection);
 	connect(numLevelSpin, SIGNAL(valueChanged(int)), this, SLOT(handleNumLevel(int)), Qt::UniqueConnection);
@@ -296,6 +310,8 @@
 {
 	disconnect(peCombo, SIGNAL(activated(int)), this, SLOT(handleParEffectUse()));
 	disconnect(dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(handleDropCapLines(int)));
+	disconnect(dropCapChars, SIGNAL(valueChanged(int)), this, SLOT(handleDropCapChars(int)));
+	disconnect(dropCapOffsetY, SIGNAL(valueChanged(double)), this, SLOT(handleDropCapOffsetY(double)));
 	disconnect(bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(handleBulletStr(QString)));
 	disconnect(numComboBox, SIGNAL(textActivated(QString)), this, SLOT(handleNumName(QString)));
 	disconnect(numLevelSpin, SIGNAL(valueChanged(int)), this, SLOT(handleNumLevel(int)));
@@ -366,6 +382,8 @@
 	if (peCombo->currentIndex() == 1)
 	{
 		newStyle.setDropCapLines(dropCapLines->value());
+		newStyle.setDropCapChars(dropCapChars->value());
+		newStyle.setDropCapOffsetY(dropCapOffsetY->value());
 		newStyle.setHasDropCap(true);
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
@@ -433,6 +451,24 @@
 	handleChanges(m_item, newStyle);
 }
 
+void PropertyWidget_ParEffect::handleDropCapChars(int dcChars)
+{
+	if (!m_doc || !m_item)
+		return;
+	ParagraphStyle newStyle;
+	newStyle.setDropCapChars(dcChars);
+	handleChanges(m_item, newStyle);
+}
+
+void PropertyWidget_ParEffect::handleDropCapOffsetY(double offset)
+{
+	if (!m_doc || !m_item)
+		return;
+	ParagraphStyle newStyle;
+	newStyle.setDropCapOffsetY(offset);
+	handleChanges(m_item, newStyle);
+}
+
 void PropertyWidget_ParEffect::handleNumName(const QString& numName)
 {
 	if (!m_doc || !m_item)
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27816)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -59,6 +59,8 @@
 	void handleParEffectUse();
 	void handleBulletStr(QString);
 	void handleDropCapLines(int);
+	void handleDropCapChars(int);
+	void handleDropCapOffsetY(double);
 	void handleNumName(const QString&);
 	void handleNumFormat(int);
 	void handleNumLevel(int);
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27816)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -119,6 +119,58 @@
         </widget>
        </item>
        <item row="0" column="2">
+        <widget class="QLabel" name="dropCapCharsLabel">
+         <property name="toolTip">
+          <string>Number of characters used by the drop cap</string>
+         </property>
+         <property name="text">
+          <string>Characters:</string>
+         </property>
+         <property name="buddy">
+          <cstring>dropCapChars</cstring>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="3">
+        <widget class="QSpinBox" name="dropCapChars">
+         <property name="toolTip">
+          <string>Drop Cap Characters</string>
+         </property>
+         <property name="minimum">
+          <number>1</number>
+         </property>
+         <property name="maximum">
+          <number>99</number>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="4">
+        <widget class="QLabel" name="dropCapOffsetYLabel">
+         <property name="toolTip">
+          <string>Drop Cap Vertical Offset</string>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="5">
+        <widget class="ScrSpinBox" name="dropCapOffsetY">
+         <property name="toolTip">
+          <string>Drop Cap Vertical Offset</string>
+         </property>
+         <property name="suffix">
+          <string> %</string>
+         </property>
+         <property name="minimum">
+          <double>-100.000000000000000</double>
+         </property>
+         <property name="maximum">
+          <double>100.000000000000000</double>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="6">
         <spacer name="horizontalSpacer">
          <property name="orientation">
           <enum>Qt::Orientation::Horizontal</enum>
@@ -560,6 +612,8 @@
   <tabstop>peIndent</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
+  <tabstop>dropCapChars</tabstop>
+  <tabstop>dropCapOffsetY</tabstop>
   <tabstop>bulletStrEdit</tabstop>
   <tabstop>bulletCharTableButton</tabstop>
  </tabstops>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27816)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -72,6 +72,10 @@
 	fillNumRestartCombo();
 	dropCapLines->setMinimum(2);
 	dropCapLines->setMaximum(99);
+	dropCapChars->setMinimum(1);
+	dropCapChars->setMaximum(99);
+	dropCapOffsetY->setMinimum(-100.0);
+	dropCapOffsetY->setMaximum(100.0);
 
 	fillPECombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
@@ -118,6 +122,8 @@
 	consecutiveHyphenLabel->setPixmap(iconManager.loadPixmap("hyphen-consecutive"));
 
 	bulletCharTableButton->setIcon(iconManager.loadIcon("character-table"));
+	dropCapOffsetYLabel->setPixmap(iconManager.loadPixmap("character-offset-baseline"));
+	dropCapOffsetYLabel->setContentsMargins(4, 0, 0, 0);
 	distFromTextLabel->setPixmap(iconManager.loadPixmap("paragraph-list-offset"));
 	parEffectCharStyleComboLabel->setPixmap(iconManager.loadPixmap("character-style"));
 	peLabel->setPixmap(iconManager.loadPixmap("paragraph-effects-style"));
@@ -307,6 +313,10 @@
 		parEffectIndentBox->setParentValue(parent->parEffectIndent());
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
+		dropCapChars->setValue(pstyle->dropCapChars(), pstyle->isInhDropCapChars());
+		dropCapChars->setParentValue(parent->dropCapChars());
+		dropCapOffsetY->setValue(pstyle->dropCapOffsetY(), pstyle->isInhDropCapOffsetY());
+		dropCapOffsetY->setParentValue(parent->dropCapOffsetY());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, !pstyle->isInhBulletStr());
 
@@ -361,6 +371,8 @@
 		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
 		dropCapLines->setValue(pstyle->dropCapLines());
+		dropCapChars->setValue(pstyle->dropCapChars());
+		dropCapOffsetY->setValue(pstyle->dropCapOffsetY());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
 		QString numName = pstyle->numName();
@@ -1061,6 +1073,36 @@
 		dropCapLines->clear();
 	else
 		dropCapLines->setValue(lines);
+
+	int chars = -1;
+	for (int i = 0; i < pstyles.count(); ++i)
+	{
+		if (chars > -1 && pstyles[i]->dropCapChars() != chars)
+		{
+			chars = -1;
+			break;
+		}
+		chars = pstyles[i]->dropCapChars();
+	}
+	if (chars == -1)
+		dropCapChars->clear();
+	else
+		dropCapChars->setValue(chars);
+
+	double offsetY = pstyles[0]->dropCapOffsetY();
+	bool sameOffset = true;
+	for (int i = 1; i < pstyles.count(); ++i)
+	{
+		if (!isEqual(pstyles[i]->dropCapOffsetY(), offsetY))
+		{
+			sameOffset = false;
+			break;
+		}
+	}
+	if (!sameOffset)
+		dropCapOffsetY->clear();
+	else
+		dropCapOffsetY->setValue(offsetY);
 }
 
 void SMPStyleWidget::showBullet(const QList<ParagraphStyle *> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27816)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -1116,6 +1116,55 @@
                </property>
                <property name="maximum">
                 <number>20</number>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QLabel" name="dropCapsCharLabel">
+               <property name="text">
+                <string>Characters:</string>
+               </property>
+               <property name="buddy">
+                <cstring>dropCapChars</cstring>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="SMSpinBox" name="dropCapChars">
+               <property name="toolTip">
+                <string>Drop Cap Characters</string>
+               </property>
+               <property name="minimum">
+                <number>1</number>
+               </property>
+               <property name="maximum">
+                <number>99</number>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QLabel" name="dropCapOffsetYLabel">
+               <property name="toolTip">
+                <string>Drop Cap Vertical Offset</string>
+               </property>
+               <property name="text">
+                <string/>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="SMScrSpinBox" name="dropCapOffsetY">
+               <property name="toolTip">
+                <string>Drop Cap Vertical Offset</string>
+               </property>
+               <property name="suffix">
+                <string> %</string>
+               </property>
+               <property name="minimum">
+                <double>-100.000000000000000</double>
+               </property>
+               <property name="maximum">
+                <double>100.000000000000000</double>
                </property>
               </widget>
              </item>
@@ -1791,6 +1840,8 @@
   <tabstop>peCombo</tabstop>
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
+  <tabstop>dropCapChars</tabstop>
+  <tabstop>dropCapOffsetY</tabstop>
   <tabstop>parEffectOffset</tabstop>
   <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27816)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -510,6 +510,8 @@
 	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)));
+	connect(m_pwidget->dropCapChars, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapChars(int)));
+	connect(m_pwidget->dropCapOffsetY, SIGNAL(valueChanged(double)), this, SLOT(slotDropCapOffsetY()));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
 	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
@@ -605,6 +607,8 @@
 	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)));
+	disconnect(m_pwidget->dropCapChars, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapChars(int)));
+	disconnect(m_pwidget->dropCapOffsetY, SIGNAL(valueChanged(double)), this, SLOT(slotDropCapOffsetY()));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
 	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
 	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
@@ -934,6 +938,35 @@
 	slotSelectionDirty();
 }
 
+void SMParagraphStyle::slotDropCapChars(int chars)
+{
+	if (m_pwidget->dropCapChars->useParentValue())
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->resetDropCapChars();
+	else
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->setDropCapChars(chars);
+
+	slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotDropCapOffsetY()
+{
+	if (m_pwidget->dropCapOffsetY->useParentValue())
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->resetDropCapOffsetY();
+	else
+	{
+		double a, b, value;
+		int c;
+		m_pwidget->dropCapOffsetY->getValues(&a, &b, &c, &value);
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->setDropCapOffsetY(value);
+	}
+
+	slotSelectionDirty();
+}
+
 void SMParagraphStyle::slotParEffectOffset()
 {
 	if (m_pwidget->parEffectOffset->useParentValue())
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27816)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -76,6 +76,8 @@
 	void slotParentParaEffects();
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
+	void slotDropCapChars(int chars);
+	void slotDropCapOffsetY();
 	void slotParEffectOffset();
 	void slotParEffectIndent(bool);
 	void slotParEffectCharStyle(int);

Fahad

2026-09-13 09:02

developer   ~0054464

@qirat I have built your patch. for functionality side it works fine. Unfortunately, I didn't have time to review from code side One note, why not implement Drop Cap only for the first paragraph?

buttar

2026-09-14 05:47

reporter   ~0054468

Thank you @Fahad. Plz review the code side too at your earliest convenience. :)

buttar

2026-09-19 00:50

reporter   ~0054479

@Fahad, do you think the "dropcap for first para only" is best handled by the Next Paragraph feature (not in Scribus yet)? Or, do you have another way in mind?

Fahad

2026-09-20 08:20

developer   ~0054480

@buttar I think the logic of DropCap should be done once for the first paragraph only by keep track of it. whenever, there is a new paragraph check the tracker and the the paragraph style.

buttar

2026-09-23 11:14

reporter   ~0054489

@Fahad, @ale do you think that the "dropcap for first para only" should be handled by/included in the patch for this ticket?

Issue History

Date Modified Username Field Change
2004-04-08 20:50 linuxlingam New Issue
2005-02-20 21:25 plinnell Relationship added related to 0000461
2006-04-11 13:26 mhanski Product Version 1.1.6 => 1.3.4cvs
2006-04-11 13:26 mhanski Build cvs1.2 =>
2006-04-13 19:14 mhanski Note Added: 0009909
2006-04-13 19:14 mhanski Status new => acknowledged
2015-09-17 20:08 Kunda Category Story Editor / Text Frames => Story Ed/Txt Frames
2015-09-17 20:12 Kunda Category Story Ed/Txt Frames => Story Editor / Text Frames
2016-01-23 13:18 Kunda Tag Attached: dropcap
2016-04-11 19:01 Kunda Relationship added has duplicate 0007587
2016-04-11 19:06 Kunda Patch => No
2016-04-11 19:06 Kunda Note Added: 0040086
2016-04-11 19:06 Kunda Target Version => Future Release - Nice to have
2016-04-11 19:06 Kunda Summary drop-cap with # of letters, or first word(s). subsequent paras shift-return for newline, no dropcap => Drop caps with more than one character (1 of them for example is a quotation mark)
2016-04-11 19:06 Kunda Description Updated
2016-04-11 19:11 Kunda Relationship added related to 0010338
2016-04-14 20:51 Kunda Relationship added related to 0001337
2018-05-27 19:53 jghali Relationship added has duplicate 0015339
2018-05-28 08:43 enzogian Note Added: 0045277
2019-08-07 23:53 jghali Relationship added has duplicate 0015760
2019-08-07 23:54 jghali Summary Drop caps with more than one character (1 of them for example is a quotation mark) => Drop caps with more than one character
2020-06-15 13:02 jghali Relationship added has duplicate 0016136
2025-04-27 19:16 cbradney Category Story Editor / Text Frames => Text Frames / Story Editor
2025-05-15 06:12 buttar Note Added: 0052537
2026-06-26 14:50 nicorikken Note Added: 0053904
2026-06-26 14:50 nicorikken File Added: dropcap-issue-hindi.png
2026-06-26 14:50 nicorikken File Added: dropcap-issue-italian.png
2026-06-26 14:50 nicorikken File Added: dropcap-issue-spanish.png
2026-06-27 00:58 buttar Note Added: 0053905
2026-06-28 02:42 buttar Note Added: 0053908
2026-07-02 08:11 ale Note Added: 0053924
2026-07-02 13:07 buttar Note Added: 0053927
2026-07-15 04:02 Fahad Note Added: 0054035
2026-07-15 04:02 Fahad File Added: dropcap-libreoffice.png
2026-07-19 01:27 buttar Note Added: 0054052
2026-07-19 09:40 Fahad Note Added: 0054054
2026-07-26 10:58 nicorikken Note Added: 0054070
2026-09-06 08:26 buttar Note Added: 0054426
2026-09-06 08:26 buttar File Added: dc1-multichar-dropcaps-core-v1.04.patch
2026-09-13 09:02 Fahad Note Added: 0054464
2026-09-14 05:47 buttar Note Added: 0054468
2026-09-19 00:50 buttar Note Added: 0054479
2026-09-20 08:20 Fahad Note Added: 0054480
2026-09-23 11:14 buttar Note Added: 0054489