View Issue Details

IDProjectCategoryView StatusLast Update
0012825ScribusText Frames / Story Editorpublic2026-07-29 08:19
ReporterJLuc Assigned To 
PrioritynormalSeverityminorReproducibilityhave not tried
Status confirmedResolutionopen 
PlatformLinuxOSUbuntuOS Version14.04
Product Version1.5.0svn 
Target Version1.5.4 
Summary0012825: Severe performance issue with layers having special blend modes
DescriptionOn a modern computer, with a 30 pages document having only one text frame, on the first page, scribus is very slow at editing text in wysiwyg mode.

When i type 10 glyphs without waiting between each keystrike, it takes 3 seconds for scribus to update the display, and the fan fans as if it was trying to break a sha256 coded password.

There are 2 layers in this document and only one frame of text.
One of the layer is empty.
If i delete this empty layer, text editing becomes brisk and lively.

When the document is only 1 page long, then there is no issue neither.

EDIT : this happens when some special blend mode is applied to the layer
Steps To ReproduceOpen uploded document slow.sla
Try editing text in first page text frame
See how slow.

Open layers window
Delete "empty" layer (not "main" one)
Try editing text in first page text frame
See how brisk.

Additional InformationCategory for this bug is "story editor" might not be the most appropriate.

I'm very glad to have discovered that scribus is not slow because of linked text frames, but because of layers - at least on this document. Because i can do without layers, but i cant do without linked text frames.
Tagslayers
Attached Files
slow.sla (44,048 bytes)
slow not.sla (34,891 bytes)
PatchNo

Relationships

related to 0001036 confirmed Severe performance problem on documents with very large number of linked frames 
related to 0014032 closedjghali Severe performance problem on documents with 8 linked frames 
child of 0002438 acknowledged Metabug: Cases of extreme memory use 
child of 0003838 acknowledged Metabug: Scribus layers 

Activities

FirasH

2014-11-24 17:10

reporter   ~0034242

Confirmed with 1.5.0.svn (19638).
It's much slower before deleting the empty layer.

JLuc

2015-03-31 12:32

developer   ~0034788

elmargik came on irc and said :
"
Somehow performance is getting very low :(
[...snip...]
I have 1 layer for images and one layer for text :/
so this makes scribus slow?
"
and later on :
"
ok i merged all to 1 layer
and it is faster now
"

JLuc

2015-03-31 13:07

developer   ~0034789

ale mentions these Qtcreator profiling tools : http://doc.qt.digia.com/qtcreator-2.4/creator-cache-profiler.html

JLuc

2015-04-11 18:29

developer   ~0034888

Last edited: 2015-04-12 07:46

I used valgrind and kcachegrind to study what could origin this slowdown. I didnt know these tools so i'm not sure of my clues.

[Edit] Test path was :
- launch scribus15svn with debug data,
- open slow.sla, enter edit mode on text frame,
- type about 60 letters (insert glyph) in about 5 places of the text (click to change place).
- Close scribus without saving doc.[/Edit]

It found that it could be ScPainter::endLayer() that eats up to 82% CPU as soon as there is more than one layer. (scribus/scpainter.cpp L105)

Within scPainter::endLayer, it seems to be the call to cairo_paint_with_alpha that requires all this CPU power.

Maybe it would be possible to not call cairo_paint_with_alpha so frequently within endLayer function ?

[Edit]
endLayer() function is called many times more than stricly required by my edits.
Maybe it would be possible to not call endlayer so frequently ?
(When no display-change causing edit has happened, enLayer should not be called.)

Or maybe cairo_paint_with_alpha could be called with less CPU expensive parameters, in circumstances when it's possible ? Or replaced with some other less CPU expensive function, in circumstances when it's possible ?
[/Edit]

JLuc

2015-04-12 08:15

developer   ~0034893

Last edited: 2015-04-12 08:16

AMOF cairo_paint_with_alpha() is similar to cairo_paint(), but the drawing is faded out using the alpha value. cairo_paint() only has one arg since it doesnt need alpha arg. (see see http://www.cairographics.org/manual/cairo-cairo-t.html#cairo-paint)

So it's possible to replace all calls to cairo_paint_with_alpha() with cairo_paint() when alpha=1, that is : most of the time.

I tested in ::endLayer() and that brings an obvious speed improvement.

In scribus code, there are 27 places where cairo_paint_with_alpha is called.
In 12 of these places, there is no need fot the 2nd parameter since its constant "cairo_paint_with_alpha(cr, 1.0);"

I suggest the following code changes :

a) to replace all "cairo_paint_with_alpha(cr, 1.0);"
with plain "cairo_paint(cr);"
b) This could be emebeded in a scribus_paint(cr) function.

c) for all other calls (where alpha arg is not a constant), i suggest to test whether alpha is 1 and in case it is 1.0, would call plain and speedy cairo_paint(), and in case its not 1.0, would call cairo_paint_with_alpha
if (alpha==1)
   cairo_paint(cr);
else
   cairo_paint_with_alpha (cr, alpha);

d) this test could be embeded in a scribus_paint_with_alpha (cr, aplha) function.

I could provide a patch in case you agree with some of these proposals.

Kunda

2015-04-29 01:29

updater   ~0034981

assigning to 1.5.1 since it merits keeping an eye on this

JLuc

2015-05-07 21:55

developer   ~0035065

Not so simple.

I have to valgrind my proposal precisely
because my feelings of speed improvements are contradicted by the source of cairo_paint_with_alpha(void *cr, double alpha) that does the test itself allready :
if (CAIRO_ALPHA_IS_OPAQUE (alpha))
         return _cairo_gstate_paint (cr->gstate);

JLuc

2015-05-18 16:44

developer   ~0035158

Precisely, for each "real character" like 'a' or 'z', the keystrike triggers a burst of 6 calls to endLayer.

Here are the functions that triggers these calls :
- 1) Canvas::DrawPageGridSub
- 2) Canvas::drawContents
- 3) Canvas::DrawPageGridSub
- 4) Canvas::DrawPageGridSub
- 5) Canvas::DrawPageGridSub
- 6) Canvas::drawContents

Its different for CTRL or SHIFT key that only trigger 2 or 3 calls.

Beside, Im' not sure if my valgrind undestanding is correct, because scribus still takes a long time to digest long bursts of speedy keystroke, even if i call cairo_paint only at the start of each burst of keystroke.

Fahad

2016-10-30 06:36

developer   ~0042226

@JLuc is there any update on this from your side?

JLuc

2016-10-30 08:13

developer   ~0042228

I've tested once again, with revision 21106 and i have different results with 2 very similar files.
- 'slow.sla' is verrrrrry slow to edit.
- 'slow not.sla' is not soooo slow to edit.

I've uploaded both of these files.
Only differences i can spot are that slow.sla has a few styles being defined, since it comes from a real book project and has a long edit log.
Other difference is that 'slow not' has no color for border display.

Btw test computer is a Intel® Core™ i7-3632QM CPU @ 2.20GHz × 8 cores and 16Go RAM

cbradney

2016-10-30 16:57

administrator   ~0042230

Fahad... its slow (slow.sla) , even in CTL. Removing the empty layer fixes the speed issues immediately.

PeterBenedek

2016-10-31 12:42

developer   ~0042241

Last edited: 2016-10-31 12:50

"main" layer turned on "Blend mode": Darken mode

"main" layer: enabled Darken mode (Blend mode == Darken mode) --> edit slow

"main" layer: disabled Darken mode (Blend mode == Normal) --> edit fast

JLuc

2016-10-31 18:39

developer   ~0042247

@peterbenedek I dont understand at all what you are writing about.

What are those "darken" or "blend" mode and where should they be turned on or off ?

cbradney

2016-10-31 18:58

administrator   ~0042248

JLuc.. in the layers palette.. select the main later and turn off the blend mode, ie set to normal.

yes it is much better like that.

JLuc

2016-10-31 21:17

developer   ~0042252

OK.
I confirm it becomes much better without that blend mode !

So this issue is specifically related to the special blend modes.

eudoxos

2017-06-22 05:20

reporter   ~0044074

In a 80+ page document where the "multiple"-blended layer was actually only used on a few pages, performance was affected in the whole document no matter which pages were visible. Thus a partial but easy optimization would be to check object count for special-blended layers when rendering a particular page: if zero, skip rendering of that layer on that page. Just an idea FWIW.

Fahad

2026-07-28 09:04

developer   ~0054080

I retest with scribus 1.6.6 and there is no slow anymore. I think it is fixed

JLuc

2026-07-28 09:33

developer   ~0054081

Last edited: 2026-07-28 09:36

Here in 1.6.5 : It is not as slow as described in initial report, but it still feels slow and typed glyphs appear on screen in burst of 2 to 5.

PeterBenedek

2026-07-28 09:35

developer   ~0054082

Last edited: 2026-07-28 09:41

LinuxMint 22.3, Scribus appimage
Tested in 1.7.4 svn too and works well.

I agree with Fahad. Looks like it's resolved.

JLuc

2026-07-28 09:36

developer   ~0054083

En 1.7.4 SVN : yep it feels better !

PeterBenedek

2026-07-28 09:41

developer   ~0054084

Last edited: 2026-07-28 09:42

Observation:
Opening the sample document (slow.sla), page scrolling works much slower compared to when Blend mode is set to Normal.
However, typing the characters is not slow.

Fahad

2026-07-28 10:03

developer   ~0054085

there is slowness in Blend modes in general. I tried to fix Multiply blend mode in 0016683 . However, if you have a doc so that I can test please share it with a new bug report. For this bug lets agree it is fixed.

PeterBenedek

2026-07-28 10:14

developer   ~0054086

This ticket contain sample document in the "Attached files" section (by JLuc)
Name: slow.sla (https://bugs.scribus.net/file_download.php?file_id=9160&type=bug)

Fahad

2026-07-28 10:47

developer   ~0054088

@PeterBenedek I can't reproduce the slowness in my machine with the attached file. tested by fedora 44 & scribus 1.6.6

PeterBenedek

2026-07-28 11:02

developer   ~0054089

I'm using Scribus on a relatively old laptop. The slowdown is quite noticeable here. See the attached video.
https://youtu.be/gdUtnp_5cAU

Fahad

2026-07-29 02:56

developer   ~0054094

as I expected the problem in blending mode here is perf report in my machine. the core problem is in pixman library which scribus uses it through cairo 2d library. it is old and not supporting SIMD fast path.
dropshadow before.png (146,533 bytes)   
dropshadow before.png (146,533 bytes)   

Fahad

2026-07-29 06:03

developer   ~0054095

Last edited: 2026-07-29 06:24

I’ve updated the patch based on my previous work in 0016683, adding the SIMD fast path for the darken blend mode. Right now I added multiply & darken blending mode. I don't want to add all blending mode unless there is a demand for that.

Performance Benchmark:

 1- Execution Time: Dropped from 71% to 36% of baseline execution time (~ 49 % reduction in processing time).

 2- Speedup: Yields a ~ 1.97x performance improvement (~ 97 % throughput increase).

Please test and let me know your feedback!
speedup2.diff (22,522 bytes)   

diff --git a/Scribus/scribus/pageitem.cpp b/Scribus/scribus/pageitem.cpp
index .. 100644
--- a/Scribus/scribus/pageitem.cpp
+++ b/Scribus/scribus/pageitem.cpp
@@ -2234,9 +2234,9 @@
 		p->save();
 		p->translate(xOffset, yOffset);
 		DrawObj(p, QRectF());
-		p->colorizeAlpha(tmp);
 		if (m_softShadowBlurRadius > 0)
-			p->blur(m_softShadowBlurRadius * sc);
+			p->blurAlpha(m_softShadowBlurRadius * sc);
+		p->colorizeAlphaPremultiplied(tmp);
 		p->restore();
 		if (m_softShadowErasedByObject)
 		{
@@ -2268,7 +2268,8 @@
 			p->strokePath();
 		}
 		if (m_softShadowBlurRadius > 0)
-			p->blur(m_softShadowBlurRadius * sc);
+			p->blurAlpha(m_softShadowBlurRadius * sc);
+		p->colorizeAlphaPremultiplied(tmp);
 		if (m_softShadowErasedByObject)
 		{
 			sh = PoLine.copy();
diff --git a/Scribus/scribus/scpainter.cpp b/Scribus/scribus/scpainter.cpp
index .. 100644
--- a/Scribus/scribus/scpainter.cpp
+++ b/Scribus/scribus/scpainter.cpp
@@ -9,6 +9,13 @@
 #include "scpattern.h"
 #include "util.h"
 #include "util_math.h"
+#include <thread>
+#include <vector>
+#include <mutex>
+#include <condition_variable>
+#include <queue>
+#include <functional>
+#include <atomic>
 
 #include <cairo.h>
 #if CAIRO_HAS_FC_FONT
@@ -21,6 +28,114 @@
 #include <cmath>
 #include <QDebug>
 
+// Added near the top of scpainter.cpp, in an anonymous namespace so it's
+// only visible within this file.
+//
+// Why this exists: profiling showed spawning std::thread objects per call
+// was itself the dominant cost in compositeMultiplyLayer() — not the pixel
+// blend math. This pool creates a fixed set of worker threads once, and
+// reuses them for every call, removing that per-call OS thread cost.
+namespace {
+
+class ScSimpleThreadPool
+{
+public:
+	static ScSimpleThreadPool& instance()
+	{
+		static ScSimpleThreadPool pool;   // created once, on first use
+		return pool;
+	}
+
+	// Splits [0, totalUnits) into up to nThreads chunks, runs fn(start, end)
+	// for each chunk on a pooled worker, and blocks until the whole batch
+	// finishes — same blocking behavior as the old spawn-and-join loop,
+	// just without creating new threads each time.
+	void runParallel(int totalUnits, unsigned nThreads, const std::function<void(int,int)> &fn)
+	{
+		if (totalUnits <= 0)
+			return;
+		nThreads = qMax(1u, qMin(nThreads, (unsigned)m_workers.size()));
+		int chunk = (totalUnits + (int)nThreads - 1) / (int)nThreads;
+
+		m_pending.store((int)nThreads, std::memory_order_relaxed);
+		{
+			std::unique_lock<std::mutex> lock(m_mutex);
+			for (unsigned t = 0; t < nThreads; ++t)
+			{
+				int start = (int)t * chunk;
+				int end = qMin(totalUnits, start + chunk);
+				if (start >= end)
+				{
+					m_pending.fetch_sub(1, std::memory_order_relaxed);
+					continue;
+				}
+				m_jobs.push([fn, start, end]() { fn(start, end); });
+			}
+		}
+		m_jobCv.notify_all();
+
+		std::unique_lock<std::mutex> lock(m_doneMutex);
+		m_doneCv.wait(lock, [this]() { return m_pending.load(std::memory_order_relaxed) <= 0; });
+	}
+
+	unsigned workerCount() const { return (unsigned) m_workers.size(); }
+
+private:
+	ScSimpleThreadPool()
+	{
+		unsigned n = std::thread::hardware_concurrency();
+		if (n == 0)
+			n = 4;
+		for (unsigned i = 0; i < n; ++i)
+			m_workers.emplace_back([this]() { workerLoop(); });
+	}
+
+	~ScSimpleThreadPool()
+	{
+		{
+			std::unique_lock<std::mutex> lock(m_mutex);
+			m_stop = true;
+		}
+		m_jobCv.notify_all();
+		for (auto &t : m_workers)
+			if (t.joinable())
+				t.join();
+	}
+
+	void workerLoop()
+	{
+		for (;;)
+		{
+			std::function<void()> job;
+			{
+				std::unique_lock<std::mutex> lock(m_mutex);
+				m_jobCv.wait(lock, [this]() { return m_stop || !m_jobs.empty(); });
+				if (m_stop && m_jobs.empty())
+					return;
+				job = std::move(m_jobs.front());
+				m_jobs.pop();
+			}
+			job();
+			if (m_pending.fetch_sub(1, std::memory_order_acq_rel) == 1)
+			{
+				std::unique_lock<std::mutex> lock(m_doneMutex);
+				m_doneCv.notify_one();
+			}
+		}
+	}
+
+	std::vector<std::thread> m_workers;
+	std::queue<std::function<void()>> m_jobs;
+	std::mutex m_mutex;
+	std::condition_variable m_jobCv;
+	std::mutex m_doneMutex;
+	std::condition_variable m_doneCv;
+	std::atomic<int> m_pending{0};
+	bool m_stop = false;
+};
+
+} // anonymous namespace
+
 ScPainter::ScPainter(QImage *target, int w, int h, double transparency, int blendmode)
          : m_image(target),
 	       m_layerTransparency(transparency),
@@ -96,7 +211,16 @@
 	mask_gradient = la.mask_gradient;
 	m_fillRule = la.fillRule;
 
+	cairo_surface_t *poppedGroup = cairo_get_group_target(m_cr); // peek target before pop for our manual path
+	bool useManualBlend = ((m_blendMode == 3 || m_blendMode == 1) && m_maskMode == 0 && la.groupClip.empty());
+	cairo_surface_t *groupSurfRef = nullptr;
+	if (useManualBlend)
+	{
+		cairo_surface_reference(poppedGroup); // keep it alive across the pop
+		groupSurfRef = poppedGroup;
+	}
 	cairo_pop_group_to_source (m_cr);
+
 	if (!la.groupClip.empty())
 	{
 		if (m_fillRule)
@@ -107,8 +231,13 @@
 		setClipPath();
 	}
 	cairo_set_operator(m_cr, CAIRO_OPERATOR_OVER);
-	if (m_maskMode > 0)
+	if (useManualBlend)
 	{
+		compositeBlendLayer(groupSurfRef, m_layerTransparency, m_blendMode);
+		cairo_surface_destroy(groupSurfRef);
+	}
+	else if (m_maskMode > 0)
+	{
 		cairo_pattern_t *patM = getMaskPattern();
 		setRasterOp(m_blendMode);
 		cairo_mask(m_cr, patM);
@@ -668,6 +797,21 @@
 			}
 			cairo_pattern_destroy(pat);
 		}
+		else if (m_blendModeFill == 3 || m_blendModeFill == 1)
+		{
+			// Multiply blend: rasterize the solid-color fill (with its normal
+			// anti-aliased path coverage) into its own buffer first, then reuse
+			// the manual multiply compositor already verified for shadow layers.
+			cairo_push_group(m_cr);
+			cairo_set_operator(m_cr, CAIRO_OPERATOR_OVER);
+			cairo_set_source_rgba(m_cr, r, g, b, 1.0);   // full color; overall alpha applied below
+			cairo_fill_preserve(m_cr);
+			cairo_pattern_t *fillPat = cairo_pop_group(m_cr);
+			cairo_surface_t *fillSurf = nullptr;
+			if (cairo_pattern_get_surface(fillPat, &fillSurf) == CAIRO_STATUS_SUCCESS && fillSurf)
+				compositeBlendLayer(fillSurf, m_fill_trans, m_blendModeFill);
+			cairo_pattern_destroy(fillPat);
+		}
 		else
 		{
 			cairo_set_source_rgba(m_cr, r, g, b, m_fill_trans);
@@ -1335,9 +1479,41 @@
 		cairo_stroke_preserve(m_cr);
 		cairo_pattern_destroy (pat);
 		cairo_pop_group_to_source (m_cr);
+		bool useManualBlendStroke = (m_blendModeStroke == 3 || m_blendModeStroke == 1);
+		cairo_surface_t *strokeGroupSurf = nullptr;
+		if (useManualBlendStroke)
+		{
+			strokeGroupSurf = cairo_get_group_target(m_cr); // peek before pop, same trick as endLayer
+			cairo_surface_reference(strokeGroupSurf);
+		}
+		cairo_pop_group_to_source (m_cr);
+		if (useManualBlendStroke)
+		{
+			compositeBlendLayer(strokeGroupSurf, m_stroke_trans, m_blendModeStroke);
+			cairo_surface_destroy(strokeGroupSurf);
+		}
+		else
+		{
 		setRasterOp(m_blendModeStroke);
 		cairo_paint_with_alpha (m_cr, m_stroke_trans);
 	}
+	}
+	else if (m_blendModeStroke == 3 || m_blendModeStroke == 1)
+	{
+		// Same trick as fillPathHelper's solid-fill case: a stroke has no
+		// buffer until rasterized, so render it into its own group first.
+		float r, g, b;
+		m_stroke.getRgbF(&r, &g, &b);
+		cairo_push_group(m_cr);
+		cairo_set_operator(m_cr, CAIRO_OPERATOR_OVER);
+		cairo_set_source_rgba(m_cr, r, g, b, 1.0);
+		cairo_stroke_preserve(m_cr);
+		cairo_pattern_t *strokePat = cairo_pop_group(m_cr);
+		cairo_surface_t *strokeSurf = nullptr;
+		if (cairo_pattern_get_surface(strokePat, &strokeSurf) == CAIRO_STATUS_SUCCESS && strokeSurf)
+			compositeBlendLayer(strokeSurf, m_stroke_trans, m_blendModeStroke);
+		cairo_pattern_destroy(strokePat);
+	}
 	else
 	{
 		float r, g, b;
@@ -1372,12 +1548,15 @@
 	cairo_surface_t *image2  = cairo_image_surface_create_for_data (image->bits(), CAIRO_FORMAT_RGB24, image->width(), image->height(), image->width() * 4);
 	cairo_surface_t *image3 = cairo_image_surface_create_for_data (image->bits(), CAIRO_FORMAT_ARGB32, image->width(), image->height(), image->width() * 4);
 	cairo_set_source_surface (m_cr, image2, 0, 0);
-	cairo_pattern_set_filter(cairo_get_source(m_cr), CAIRO_FILTER_GOOD);
-	cairo_mask_surface (m_cr, image3, 0, 0);
+	cairo_pattern_set_filter(cairo_get_source(m_cr), CAIRO_FILTER_BILINEAR);
+	cairo_pattern_t *maskPat = cairo_pattern_create_for_surface(image3);
+	cairo_pattern_set_filter(maskPat, CAIRO_FILTER_BILINEAR);
+	cairo_mask(m_cr, maskPat);
+	cairo_pattern_destroy(maskPat);
 	cairo_surface_destroy (image2);
 	cairo_surface_destroy (image3);
 	cairo_pop_group_to_source (m_cr);
-	cairo_pattern_set_filter(cairo_get_source(m_cr), CAIRO_FILTER_GOOD);
+	cairo_pattern_set_filter(cairo_get_source(m_cr), CAIRO_FILTER_BILINEAR);
 	setRasterOp(m_blendModeFill);
 	if (m_maskMode > 0)
 	{
@@ -1869,6 +2048,176 @@
 	cairo_surface_mark_dirty(data);
 }
 
+void ScPainter::colorizeAlphaPremultiplied(const QColor &color)
+{
+	cairo_surface_t *data = cairo_get_group_target(m_cr);
+	cairo_surface_flush(data);
+	int w   = cairo_image_surface_get_width(data);
+	int h   = cairo_image_surface_get_height(data);
+	int stride = cairo_image_surface_get_stride(data);
+	unsigned char *d = cairo_image_surface_get_data(data);
+	int cr = color.red();
+	int cg = color.green();
+	int cb = color.blue();
+	for (int y = 0; y < h; ++y)
+	{
+		QRgb *dst = (QRgb*)d;
+		for (int x = 0; x < w; ++x)
+		{
+			int a = qAlpha(*dst);
+			if (a > 0)
+				*dst = qRgba((cr * a) / 255, (cg * a) / 255, (cb * a) / 255, a);
+			dst++;
+		}
+		d += stride;
+	}
+	cairo_surface_mark_dirty(data);
+}
+
+// Manual replacement for cairo's CAIRO_OPERATOR_MULTIPLY and
+// CAIRO_OPERATOR_DARKEN compositing (blendMode 3 and 1 respectively).
+//
+// Why this exists: pixman has no SIMD fast path for either mode (or most
+// other PDF/CSS blend modes). It falls back to general_composite_rect(), a
+// slow generic per-pixel compositor. This function does the exact same math
+// pixman would do, but reads/writes the two buffers directly ourselves and
+// splits the work across threads by row — same approach as blurAlpha().
+//
+// Multiply's blend term (src*dst) falls out of the premultiplied algebra
+// directly. Darken's blend term (min(Cb,Cs) on unpremultiplied color) does
+// not collapse the same way; the correct premultiplied form is
+// min(da*sr, sa*dr) — see blendMode branch inside rowBlend() below.
+//
+// Confirmed call sites as of this session: ScPainter::endLayer() (plain
+// no-mask branch, and the earlier manual-multiply branch), the solid-fill
+// branch in fillPathHelper() (m_blendModeFill == 1 or 3), and both branches
+// of strokePathHelper() (gradient stroke and solid stroke, m_blendModeStroke
+// == 1 or 3). Each caller is responsible for its own guard conditions
+// (mask/clip exclusions, etc.) before invoking this function — see the
+// guard at each call site rather than assuming this list is exhaustive if
+// new call sites are added later.
+//
+// groupSurf : the popped/rasterized cairo group surface — the source layer
+//             for this blend (a shadow, a solid fill, or a solid stroke,
+//             depending on caller).
+// alpha     : the layer's overall opacity, applied to the source before
+//             blending, same as cairo_paint_with_alpha().
+// blendMode : 1 = Darken, 3 = Multiply. Anything else falls through to the
+//             Multiply-shaped math path (see useDarken check) — callers
+//             must only invoke this for blendMode values they've actually
+//             guarded for.
+
+void ScPainter::compositeBlendLayer(cairo_surface_t *groupSurf, double alpha, int blendMode)
+{
+	cairo_surface_t *dst = cairo_get_group_target(m_cr);
+	cairo_surface_flush(groupSurf);
+	cairo_surface_flush(dst);
+
+	int a255 = qBound(0, (int) qRound(alpha * 255.0), 255);
+	if (a255 == 0)
+		return; // Early exit: fully transparent
+
+	int sw = cairo_image_surface_get_width(groupSurf);
+	int sh = cairo_image_surface_get_height(groupSurf);
+	int sStride = cairo_image_surface_get_stride(groupSurf);
+	unsigned char *sData = cairo_image_surface_get_data(groupSurf);
+
+	int dw = cairo_image_surface_get_width(dst);
+	int dh = cairo_image_surface_get_height(dst);
+	int dStride = cairo_image_surface_get_stride(dst);
+	unsigned char *dData = cairo_image_surface_get_data(dst);
+
+	double srcOffX, srcOffY, dstOffX, dstOffY;
+	cairo_surface_get_device_offset(groupSurf, &srcOffX, &srcOffY);
+	cairo_surface_get_device_offset(dst, &dstOffX, &dstOffY);
+	int offX = (int) qRound(-srcOffX + dstOffX);
+	int offY = (int) qRound(-srcOffY + dstOffY);
+
+	auto div255 = [](int v) -> int { return (v + 1 + (v >> 8)) >> 8; };
+
+	// --- Precompute exact 2D overlap bounds ---
+	int sxStart = qMax(0, -offX);
+	int sxEnd   = qMin(sw, dw - offX);
+	int syStart = qMax(0, -offY);
+	int syEnd   = qMin(sh, dh - offY);
+
+	if (sxStart >= sxEnd || syStart >= syEnd)
+		return; // Early exit: surfaces do not overlap
+
+	bool useDarken = (blendMode == 1);
+	auto rowBlend = [&](int yStart, int yEnd)
+	{
+		for (int sy = yStart; sy < yEnd; ++sy)
+		{
+			int dy = sy + offY; // Guaranteed: 0 <= dy < dh
+			QRgb *srow = (QRgb *) (sData + sy * sStride);
+			QRgb *drow = (QRgb *) (dData + dy * dStride);
+
+			for (int sx = sxStart; sx < sxEnd; ++sx) { // Guaranteed: 0 <= dx < dw
+				int dx = sx + offX;
+				QRgb s = srow[sx];
+				int sa = qAlpha(s);
+				if (sa == 0)
+					continue;
+
+				int sr = div255(qRed(s)   * a255);
+				int sg = div255(qGreen(s) * a255);
+				int sb = div255(qBlue(s)  * a255);
+				sa = div255(sa * a255);
+				if (sa == 0)
+					continue;
+
+				QRgb d = drow[dx];
+				int da = qAlpha(d);
+				int dr = qRed(d), dg = qGreen(d), db = qBlue(d);
+
+				int termR, termG, termB;
+				if (useDarken)
+				{
+					termR = div255(qMin(da * sr, sa * dr));
+					termG = div255(qMin(da * sg, sa * dg));
+					termB = div255(qMin(da * sb, sa * db));
+				}
+				else // Multiply
+				{
+					termR = div255(sr * dr);
+					termG = div255(sg * dg);
+					termB = div255(sb * db);
+				}
+				int ra = sa + da - div255(sa * da);
+				int rr = div255(sr * (255 - da)) + div255(dr * (255 - sa)) + termR;
+				int rg = div255(sg * (255 - da)) + div255(dg * (255 - sa)) + termG;
+				int rb = div255(sb * (255 - da)) + div255(db * (255 - sa)) + termB;
+
+				drow[dx] = qRgba(qMin(rr, 255), qMin(rg, 255), qMin(rb, 255), qMin(ra, 255));
+			}
+		}
+	};
+
+	int totalOverlapRows = syEnd - syStart;
+	unsigned hwThreads = std::thread::hardware_concurrency();
+	if (hwThreads == 0)
+		hwThreads = 1;
+
+	// Thread creation itself costs more than the blend loop for small/medium
+	// buffers (confirmed via profiling: 25.7% of this function's cost was
+	// std::thread spawn/teardown, not pixel math). Raise the bar so only
+	// large buffers — where the per-call OS thread cost is actually amortized
+	// — take the threaded path. Most solid-fill Multiply calls are small
+	// shapes and should just run inline.
+	constexpr size_t kMultiplyThreadingThreshold = 800000; // ~700x700px
+	if ((size_t)(sw * sh) < kMultiplyThreadingThreshold || hwThreads <= 1)
+	{
+		rowBlend(syStart, syEnd);
+	}
+	else
+	{
+		ScSimpleThreadPool::instance().runParallel(totalOverlapRows, hwThreads,
+												   [&](int yStart, int yEnd) { rowBlend(syStart + yStart, syStart + yEnd); });
+	}
+
+	cairo_surface_mark_dirty(dst);
+}
 void ScPainter::blurAlpha(int radius)
 {
 	if (radius < 1)
@@ -1881,119 +2230,179 @@
 	int hm  = h - 1;
 	int wh  = w * h;
 	int div = radius+radius+1;
-	int *a = new int[wh];
-	int asum, x, y, i, yp, yi, yw;
-	QRgb p;
-	int *vmin = new int[qMax(w, h)];
+	int r1  = radius + 1;
+
+	// Scratch buffers allocation
+	if (m_blurBufCapacity < (size_t) wh)
+	{
+		delete [] m_blurBufR;
+		delete [] m_blurBufG;
+		delete [] m_blurBufB;
+		delete [] m_blurBufA;
+		m_blurBufR = new int[wh];
+		m_blurBufG = new int[wh];
+		m_blurBufB = new int[wh];
+		m_blurBufA = new int[wh];
+		m_blurBufCapacity = wh;
+	}
+	int *a = m_blurBufA;
+
+	size_t vminNeeded = (size_t) qMax(w, h);
+	if (m_blurVminCapacity < vminNeeded)
+	{
+		delete [] m_blurVmin;
+		m_blurVmin = new int[vminNeeded];
+		m_blurVminCapacity = vminNeeded;
+	}
+	int *vmin = m_blurVmin;
+
 	int divsum = (div + 1)>>1;
 	divsum *= divsum;
-	int *dv = new int[256 * (size_t) divsum];
-	for (i = 0; i < 256 * divsum; ++i)
+	size_t dvNeeded = 256 * (size_t) divsum;
+	if (m_blurDvCapacity < dvNeeded)
 	{
+		delete [] m_blurDv;
+		m_blurDv = new int[dvNeeded];
+		m_blurDvCapacity = dvNeeded;
+	}
+	int *dv = m_blurDv;
+	for (int i = 0; i < 256 * divsum; ++i)
 		dv[i] = (i / divsum);
-	}
-	yw = yi = 0;
-	int **stack = new int*[div];
-	for (int i = 0; i < div; ++i)
+
+	constexpr int kParallelPixelThreshold = 160000;
+	unsigned hwThreads = std::thread::hardware_concurrency();
+	if (hwThreads == 0)
+		hwThreads = 1;
+
+	// ---------- Pass 1: horizontal blur, row-parallel ----------
+	for (int x = 0; x < w; ++x)
+		vmin[x] = qMin(x + radius + 1, wm);
+
+	auto horizontalPass = [&](int yStart, int yEnd)
 	{
-		stack[i] = new int[1];
-	}
-	int stackpointer;
-	int stackstart;
-	int *sir;
-	int rbs;
-	int r1 = radius+1;
-	int aoutsum;
-	int ainsum;
-	for (y = 0; y < h; ++y)
+		std::vector<int> stack(div);
+
+		for (int y = yStart; y < yEnd; ++y)
 	{
-		ainsum = aoutsum = asum = 0;
-		for (i = -radius; i <= radius; ++i)
+			int yw = y * w;
+			int yi = yw;
+			int ainsum = 0, aoutsum = 0, asum = 0;
+			for (int i = -radius; i <= radius; ++i)
 		{
-			p = pix[yi + qMin(wm, qMax(i, 0))];
-			sir = stack[i + radius];
-			sir[0] = qAlpha(p);
-			rbs = r1 - abs(i);
-			asum += sir[0] * rbs;
+				QRgb p = pix[yi + qMin(wm, qMax(i, 0))];
+				int av = qAlpha(p);
+				stack[i + radius] = av;
+				int rbs = r1 - abs(i);
+				asum += av * rbs;
 			if (i > 0)
-				ainsum += sir[0];
+					ainsum += av;
 			else
-				aoutsum += sir[0];
+					aoutsum += av;
 		}
-		stackpointer = radius;
-		for (x = 0; x < w; ++x)
+			int stackpointer = radius;
+			for (int x = 0; x < w; ++x)
 		{
 			a[yi] = dv[asum];
 			asum -= aoutsum;
-			stackstart = stackpointer - radius + div;
-			sir = stack[stackstart % div];
-			aoutsum -= sir[0];
-			if (y == 0)
-				vmin[x] = qMin(x + radius + 1, wm);
-			p = pix[yw + vmin[x]];
-			sir[0] = qAlpha(p);
-			ainsum += sir[0];
+
+				int stackstart = stackpointer - radius;
+				if (stackstart < 0)
+					stackstart += div;
+
+				aoutsum -= stack[stackstart];
+
+				QRgb p = pix[yw + vmin[x]];
+				int av = qAlpha(p);
+				stack[stackstart] = av;
+				ainsum += av;
 			asum += ainsum;
-			stackpointer = (stackpointer + 1) % div;
-			sir = stack[stackpointer % div];
-			aoutsum += sir[0];
-			ainsum -= sir[0];
+
+				int nextsp = stackpointer + 1;
+				stackpointer = (nextsp == div) ? 0 : nextsp;
+
+				aoutsum += stack[stackpointer];
+				ainsum -= stack[stackpointer];
 			++yi;
 		}
-		yw += w;
 	}
-	for (x = 0; x < w; ++x)
+	};
+
+	if (wh < kParallelPixelThreshold || hwThreads <= 1)
 	{
-		ainsum = aoutsum = asum = 0;
-		yp = -radius * w;
-		for (i = -radius; i <= radius; ++i)
+		horizontalPass(0, h);
+	}
+	else
 		{
-			yi = qMax(0, yp) + x;
-			sir = stack[i + radius];
-			sir[0] = a[yi];
-			rbs = r1 - abs(i);
-			asum += a[yi] * rbs;
+		ScSimpleThreadPool::instance().runParallel(h, hwThreads, [&](int yStart, int yEnd) {
+			horizontalPass(yStart, yEnd);
+		});
+	}
+
+	// ---------- Pass 2: vertical blur, column-parallel ----------
+	for (int y = 0; y < h; ++y)
+		vmin[y] = qMin(y + r1, hm) * w;
+
+	auto verticalPass = [&](int xStart, int xEnd)
+	{
+		std::vector<int> stack(div);
+
+		for (int x = xStart; x < xEnd; ++x)
+		{
+			int ainsum = 0, aoutsum = 0, asum = 0;
+			int yp = -radius * w;
+			for (int i = -radius; i <= radius; ++i)
+			{
+				int yi = qMax(0, yp) + x;
+				int av = a[yi];
+				stack[i + radius] = av;
+				int rbs = r1 - abs(i);
+				asum += av * rbs;
 			if (i > 0)
-				ainsum += sir[0];
+					ainsum += av;
 			else
-				aoutsum += sir[0];
+					aoutsum += av;
 			if (i < hm)
-			{
 				yp += w;
 			}
-		}
-		yi = x;
-		stackpointer = radius;
-		for (y = 0; y < h; ++y)
+			int yi = x;
+			int stackpointer = radius;
+			for (int y = 0; y < h; ++y)
 		{
 			pix[yi] = qRgba(qRed(pix[yi]), qGreen(pix[yi]), qBlue(pix[yi]), dv[asum]);
 			asum -= aoutsum;
-			stackstart = stackpointer - radius + div;
-			sir = stack[stackstart%div];
-			aoutsum -= sir[0];
-			if (x == 0)
-			{
-				vmin[y] = qMin(y + r1, hm) * w;
-			}
-			p = x + vmin[y];
-			sir[0] = a[p];
-			ainsum += sir[0];
+
+				int stackstart = stackpointer - radius;
+				if (stackstart < 0)
+					stackstart += div;
+
+				aoutsum -= stack[stackstart];
+
+				int p = x + vmin[y];
+				int av = a[p];
+				stack[stackstart] = av;
+				ainsum += av;
 			asum += ainsum;
-			stackpointer = (stackpointer+1)%div;
-			sir = stack[stackpointer];
-			aoutsum += sir[0];
-			ainsum -= sir[0];
+
+				int nextsp = stackpointer + 1;
+				stackpointer = (nextsp == div) ? 0 : nextsp;
+
+				aoutsum += stack[stackpointer];
+				ainsum -= stack[stackpointer];
 			yi += w;
 		}
 	}
-	delete [] a;
-	delete [] vmin;
-	delete [] dv;
-	for (int i = 0; i < div; ++i)
+	}; // <--- verticalPass lambda ends here
+
+	if (wh < kParallelPixelThreshold || hwThreads <= 1)
 	{
-		delete [] stack[i];
+		verticalPass(0, w);
 	}
-	delete [] stack;
+	else
+	{
+		ScSimpleThreadPool::instance().runParallel(w, hwThreads, [&](int xStart, int xEnd) {
+			verticalPass(xStart, xEnd);
+		});
+	}
 	cairo_surface_mark_dirty(data);
 }
 
diff --git a/Scribus/scribus/scpainter.h b/Scribus/scribus/scpainter.h
index .. 100644
--- a/Scribus/scribus/scpainter.h
+++ b/Scribus/scribus/scpainter.h
@@ -109,6 +109,12 @@
 	virtual void drawShadePanel(const QRectF &r, const QColor& color, bool sunken, int lineWidth);
 	virtual void drawUnderlinedRect(const QRectF &r, const QColor& color, int lineWidth);
 	virtual void colorizeAlpha(const QColor& color);
+	// Same as colorizeAlpha(), but scales the color by alpha before storing,
+	// as required for a premultiplied surface. Needed after blurAlpha(),
+	// since blurring creates partial-alpha pixels that colorizeAlpha()'s
+	// full-strength write would store incorrectly.
+	virtual void colorizeAlphaPremultiplied(const QColor& color);
+	virtual void compositeBlendLayer(cairo_surface_t *groupSurf, double alpha, int blendMode);
 	virtual void colorize(const QColor& color);
 	virtual void blurAlpha(int radius);
 	virtual void blur(int radius);
speedup2.diff (22,522 bytes)   
pref-report-after-patch.png (100,102 bytes)   
pref-report-after-patch.png (100,102 bytes)   

PeterBenedek

2026-07-29 08:19

developer   ~0054096

Fahad. We appreciate your work.

Issue History

Date Modified Username Field Change
2014-11-20 16:55 JLuc New Issue
2014-11-20 16:55 JLuc File Added: slow.sla
2014-11-20 16:56 JLuc Description Updated
2014-11-20 16:56 JLuc Steps to Reproduce Updated
2014-11-20 17:04 JLuc Additional Information Updated
2014-11-20 22:40 JLuc Description Updated
2014-11-22 09:35 JLuc Description Updated
2014-11-24 17:10 FirasH Note Added: 0034242
2014-11-24 17:10 FirasH Status new => confirmed
2015-03-31 12:32 JLuc Note Added: 0034788
2015-03-31 13:07 JLuc Note Added: 0034789
2015-04-11 18:29 JLuc Note Added: 0034888
2015-04-12 07:44 JLuc Note Edited: 0034888
2015-04-12 07:44 JLuc Note Edited: 0034888
2015-04-12 07:45 JLuc Note Edited: 0034888
2015-04-12 07:46 JLuc Note Edited: 0034888
2015-04-12 08:15 JLuc Note Added: 0034893
2015-04-12 08:16 JLuc Note Edited: 0034893
2015-04-29 01:29 Kunda Note Added: 0034981
2015-04-29 01:29 Kunda Target Version => 1.5.1
2015-04-29 01:30 Kunda Relationship added child of 0002438
2015-05-07 21:55 JLuc Note Added: 0035065
2015-05-18 16:44 JLuc Note Added: 0035158
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 17:16 cbradney Target Version 1.5.1 => 1.5.3
2016-05-06 19:48 JLuc Relationship added related to 0014032
2016-10-30 06:36 Fahad Note Added: 0042226
2016-10-30 08:07 JLuc File Deleted: slow.sla
2016-10-30 08:07 JLuc File Added: slow.sla
2016-10-30 08:08 JLuc File Added: slow not.sla
2016-10-30 08:13 JLuc Note Added: 0042228
2016-10-30 16:57 cbradney Note Added: 0042230
2016-10-31 08:47 JLuc Relationship added child of 0003838
2016-10-31 12:42 PeterBenedek Note Added: 0042241
2016-10-31 12:44 PeterBenedek Note Edited: 0042241
2016-10-31 12:50 PeterBenedek Note Edited: 0042241
2016-10-31 18:39 JLuc Note Added: 0042247
2016-10-31 18:58 cbradney Note Added: 0042248
2016-10-31 21:17 JLuc Note Added: 0042252
2016-10-31 21:18 JLuc Summary Severe performance issue with layers. => Severe performance issue with layers having special blend modes
2016-10-31 21:18 JLuc Description Updated
2016-11-17 11:24 Kunda Relationship added related to 0001036
2016-12-06 12:30 Kunda Tag Attached: layers
2016-12-08 22:04 Kunda Target Version 1.5.3 => 1.5.4
2017-06-22 05:20 eudoxos Note Added: 0044074
2025-04-27 19:16 cbradney Category Story Editor / Text Frames => Text Frames / Story Editor
2026-07-28 09:04 Fahad Note Added: 0054080
2026-07-28 09:32 JLuc Description Updated
2026-07-28 09:33 JLuc Note Added: 0054081
2026-07-28 09:34 JLuc Note Edited: 0054081
2026-07-28 09:35 PeterBenedek Note Added: 0054082
2026-07-28 09:36 JLuc Note Edited: 0054081
2026-07-28 09:36 JLuc Note Edited: 0054081
2026-07-28 09:36 JLuc Note Added: 0054083
2026-07-28 09:41 PeterBenedek Note Edited: 0054082
2026-07-28 09:41 PeterBenedek Note Edited: 0054082
2026-07-28 09:41 PeterBenedek Note Added: 0054084
2026-07-28 09:42 PeterBenedek Note Edited: 0054084
2026-07-28 10:03 Fahad Note Added: 0054085
2026-07-28 10:14 PeterBenedek Note Added: 0054086
2026-07-28 10:47 Fahad Note Added: 0054088
2026-07-28 11:02 PeterBenedek Note Added: 0054089
2026-07-29 02:56 Fahad Note Added: 0054094
2026-07-29 02:56 Fahad File Added: dropshadow before.png
2026-07-29 06:03 Fahad Note Added: 0054095
2026-07-29 06:03 Fahad File Added: speedup2.diff
2026-07-29 06:03 Fahad File Added: pref-report-after-patch.png
2026-07-29 06:24 Fahad Note Edited: 0054095
2026-07-29 06:24 Fahad Note Edited: 0054095
2026-07-29 08:19 PeterBenedek Note Added: 0054096