Speed up opening folios with photos: decode their pictures in parallel

Each picture of a folio was decoded in turn while its item was built,
a quarter of a second for a 12 megapixel photo. Diagram::fromXml() now
reads every picture of the folio first, decodes them all at once on a
pool of its own (one thread per core, at most one per picture), then
builds the items in file order in the GUI thread as before.

The decoding (base64 text, then the picture file) moves out of
DiagramImageItem::fromXml() into decodePicture(), which touches only
its argument. QImage::fromData() then QPixmap::fromImage() is what
QPixmap::loadFromData() does, so the pixels are the same.

On a 263 MB project with 22 photos on one folio: --info 21.3 s ->
12.5 s, the folio's content 10.6 s -> 3.2 s, opening its .qetz 15.6 s
-> 9.3 s. Peak memory +220 MB (1.81 -> 2.03 GB), the pictures' text
being converted by several threads at once. A resave is byte-identical
to master's, and so is a PNG export.

tst_folioimageload: six pictures come back with their own pixels and
positions, and an <image> without a picture does not stop the others.
It fails when the decoded pictures are handed to the wrong items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-14 12:09:09 +13:00
parent 8b680af938
commit 6751fc6f19
5 changed files with 240 additions and 24 deletions
+32 -9
View File
@@ -53,6 +53,8 @@
#include "foliogrid.h"
#include <QGraphicsView>
#include <QTextStream>
#include <QThreadPool>
#include <QtConcurrentMap>
#include <algorithm>
#include <climits>
#include <cassert>
@@ -2006,16 +2008,37 @@ bool Diagram::fromXml(QDomElement &document,
}
// Load image
QList<DiagramImageItem *> added_images;
for (auto image_xml : QET::findInDomElement(root,
QStringLiteral("images"),
QStringLiteral("image"))) {
DiagramImageItem *dii = new DiagramImageItem ();
// Decoding is most of the time of a folio with photos (a quarter of
// a second each), so the pictures are decoded first, in parallel,
// and the items built afterwards in this thread.
const QList<QDomElement> images_xml = QET::findInDomElement(
root, QStringLiteral("images"), QStringLiteral("image"));
QList<DiagramImageItem::Picture> pictures;
pictures.reserve(images_xml.size());
for (const QDomElement &image_xml : images_xml) {
const QString picture_file = QetContainer::pictureFile(image_xml);
if (!picture_file.isEmpty() && project())
dii -> fromXml(image_xml, project() -> containerPicture(picture_file));
else
dii -> fromXml(image_xml);
pictures << DiagramImageItem::pictureOf(
image_xml,
!picture_file.isEmpty() && project()
? project() -> containerPicture(picture_file)
: QByteArray());
}
if (pictures.size() > 1) {
QThreadPool pool;
pool.setMaxThreadCount(std::min<int>(QThread::idealThreadCount(),
pictures.size()));
QtConcurrent::blockingMap(&pool, pictures,
&DiagramImageItem::decodePicture);
} else {
for (DiagramImageItem::Picture &picture : pictures)
DiagramImageItem::decodePicture(picture);
}
QList<DiagramImageItem *> added_images;
for (qsizetype i = 0 ; i < images_xml.size() ; ++i) {
const QDomElement &image_xml = images_xml.at(i);
DiagramImageItem *dii = new DiagramImageItem ();
dii -> fromXml(image_xml, std::move(pictures[i]));
settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size());
ItemGroups::setGroup(dii, ItemGroups::read(image_xml));
addItem(dii);
+59 -15
View File
@@ -1798,6 +1798,60 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
pictures as files (QetContainer::join()).
*/
bool DiagramImageItem::fromXml(const QDomElement &e, const QByteArray &picture)
{
Picture p = pictureOf(e, picture);
decodePicture(p);
return fromXml(e, std::move(p));
}
/**
@brief DiagramImageItem::pictureOf
@return the picture of @p e, @p picture unless it is empty, else the
base64 text of @p e, still encoded : decodePicture() does the slow part.
Reads the DOM, so GUI thread only.
*/
DiagramImageItem::Picture DiagramImageItem::pictureOf(const QDomElement &e,
const QByteArray &picture)
{
Picture p;
p.bytes = picture;
if (p.bytes.isEmpty())
{
// From the first text node only, not e.text(): that also collects
// the text of every child element, so on a cropped or colour-keyed
// picture it carries <image_base>'s bytes too.
const QDomNode image_node = e.firstChild();
if (image_node.isText())
p.base64 = image_node.toText().data();
else
p.found = false;
}
return p;
}
/**
@brief DiagramImageItem::decodePicture
Decodes the base64 text and the picture file of @p picture. Touches
nothing else, so it can run in any thread : a large photo takes about
a quarter of a second, and Diagram::fromXml() decodes a folio's
pictures in parallel.
*/
void DiagramImageItem::decodePicture(Picture &picture)
{
if (picture.bytes.isEmpty() && !picture.base64.isEmpty())
{
picture.bytes = QByteArray::fromBase64(picture.base64.toLatin1());
picture.base64.clear();
}
picture.image = QImage::fromData(picture.bytes);
}
/**
@brief DiagramImageItem::fromXml
Load this image from xml element e, its picture already decoded by
decodePicture().
*/
bool DiagramImageItem::fromXml(const QDomElement &e, Picture picture)
{
if (e.tagName() != "image") {
return (false);
@@ -1807,23 +1861,13 @@ bool DiagramImageItem::fromXml(const QDomElement &e, const QByteArray &picture)
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
// From the first text node only, not e.text(): that also collects the
// text of every child element, so on a cropped or colour-keyed picture
// it carries <image_base>'s bytes too. Decoded once: on a project with
// large pictures each decoding costs seconds.
QByteArray array = picture;
if (array.isEmpty())
{
QDomNode image_node = e.firstChild();
if (!image_node.isText()) {
return (false);
}
array = QByteArray::fromBase64(image_node.toText().data().toLatin1());
if (!picture.found) {
return (false);
}
const QByteArray array = picture.bytes;
//Set QPixmap from the array
QPixmap pixmap;
pixmap.loadFromData(array);
//Set QPixmap from the decoded picture
QPixmap pixmap = QPixmap::fromImage(std::move(picture.image));
setPixmap(pixmap);
if (isPng(array) && !pixmap_.isNull())
{
@@ -24,6 +24,7 @@
#include <QColor>
#include <QFont>
#include <QImage>
#include <QList>
#include <QUuid>
#include <QStyleOptionGraphicsItem>
@@ -119,8 +120,20 @@ class DiagramImageItem : public QetGraphicsItem {
*/
int type() const override { return Type; }
/// A picture as read from its <image> element, before and after
/// decodePicture(), which may run outside the GUI thread.
struct Picture {
QString base64; ///< text of <image>, decoded when bytes is empty
QByteArray bytes; ///< the picture file (PNG, JPEG...)
QImage image; ///< bytes, decoded
bool found = true; ///< false : neither bytes nor text
};
static Picture pictureOf(const QDomElement &, const QByteArray &picture);
static void decodePicture(Picture &picture);
virtual bool fromXml(const QDomElement &);
bool fromXml(const QDomElement &, const QByteArray &picture);
bool fromXml(const QDomElement &, Picture picture);
virtual QDomElement toXml(QDomDocument &) const;
QUuid uuid() const {return m_uuid;}
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}