Coverage Report

Created: 2026-03-12 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/kio-extras/thumbnail/textcreator.cpp
Line
Count
Source
1
/*  This file is part of the KDE libraries
2
    SPDX-FileCopyrightText: 2000, 2002 Carsten Pfeiffer <pfeiffer@kde.org>
3
    SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org>
4
5
    SPDX-License-Identifier: LGPL-2.0-or-later
6
*/
7
8
#include "textcreator.h"
9
10
#include <QFile>
11
#include <QFontDatabase>
12
#include <QImage>
13
#include <QPainter>
14
#include <QPalette>
15
#include <QStringDecoder>
16
#include <QTextDocument>
17
18
#include <KDesktopFile>
19
#include <KPluginFactory>
20
#include <KSyntaxHighlighting/Definition>
21
#include <KSyntaxHighlighting/SyntaxHighlighter>
22
#include <KSyntaxHighlighting/Theme>
23
24
// TODO Fix or remove kencodingprober code
25
// #include <kencodingprober.h>
26
27
0
K_PLUGIN_CLASS_WITH_JSON(TextCreator, "textthumbnail.json")
Unexecuted instantiation: textthumbnail_factory::tr(char const*, char const*, int)
Unexecuted instantiation: textthumbnail_factory::~textthumbnail_factory()
28
0
29
0
TextCreator::TextCreator(QObject *parent, const QVariantList &args)
30
17.4k
    : KIO::ThumbnailCreator(parent, args)
31
17.4k
    , m_data(nullptr)
32
17.4k
    , m_dataSize(0)
33
17.4k
{
34
17.4k
}
35
36
TextCreator::~TextCreator()
37
17.4k
{
38
17.4k
    delete[] m_data;
39
17.4k
}
40
41
static QStringDecoder codecFromContent(const char *data, int dataSize)
42
17.4k
{
43
#if 0 // ### Use this when KEncodingProber does not return junk encoding for UTF-8 data)
44
    KEncodingProber prober;
45
    prober.feed(data, dataSize);
46
    return QStringDecoder(prober.encoding());
47
#else
48
    // try to detect UTF text, fall back to locale default (which is usually UTF-8)
49
17.4k
    return QStringDecoder(QStringDecoder::encodingForData(QByteArrayView(data, dataSize)).value_or(QStringDecoder::System));
50
17.4k
#endif
51
17.4k
}
52
53
KIO::ThumbnailResult TextCreator::create(const KIO::ThumbnailRequest &request)
54
17.4k
{
55
17.4k
    const QString path = request.url().toLocalFile();
56
    // Desktop files, .directory files, and flatpakrefs aren't traditional
57
    // text files, so their icons should be shown instead
58
17.4k
    if (KDesktopFile::isDesktopFile(path) || path.endsWith(QStringLiteral(".directory")) || path.endsWith(QStringLiteral(".flatpakref"))) {
59
0
        return KIO::ThumbnailResult::fail();
60
0
    }
61
62
17.4k
    bool ok = false;
63
64
    // determine some sizes...
65
    // example: width: 60, height: 64
66
67
17.4k
    const int width = request.targetSize().width();
68
17.4k
    const int height = request.targetSize().height();
69
17.4k
    const qreal dpr = request.devicePixelRatio();
70
71
17.4k
    QImage img;
72
73
17.4k
    QSize pixmapSize(width, height);
74
17.4k
    if (height * 3 > width * 4)
75
0
        pixmapSize.setHeight(width * 4 / 3);
76
17.4k
    else
77
17.4k
        pixmapSize.setWidth(height * 3 / 4);
78
79
17.4k
    if (pixmapSize != m_pixmap.size()) {
80
17.4k
        m_pixmap = QPixmap(pixmapSize);
81
17.4k
        m_pixmap.setDevicePixelRatio(dpr);
82
17.4k
    }
83
84
    // one pixel for the rectangle, the rest. whitespace
85
17.4k
    int xborder = 1 + pixmapSize.width() / 16 / dpr; // minimum x-border
86
17.4k
    int yborder = 1 + pixmapSize.height() / 16 / dpr; // minimum y-border
87
88
    // this font is supposed to look good at small sizes
89
17.4k
    QFont font = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont);
90
91
17.4k
    font.setPixelSize(qMax(7.0, qMin(10.0, (pixmapSize.height() / dpr - 2 * yborder) / 16)));
92
17.4k
    QFontMetrics fm(font);
93
94
    // calculate a better border so that the text is centered
95
17.4k
    const QSizeF canvasSize(pixmapSize.width() / dpr - 2 * xborder, pixmapSize.height() / dpr - 2 * yborder);
96
17.4k
    const int numLines = (int)(canvasSize.height() / fm.height());
97
98
    // assumes an average line length of <= 120 chars
99
17.4k
    const int bytesToRead = 120 * numLines;
100
101
    // create text-preview
102
17.4k
    QFile file(path);
103
17.4k
    if (file.open(QIODevice::ReadOnly)) {
104
17.4k
        if (!m_data || m_dataSize < bytesToRead + 1) {
105
17.4k
            delete[] m_data;
106
17.4k
            m_data = new char[bytesToRead + 1];
107
17.4k
            m_dataSize = bytesToRead + 1;
108
17.4k
        }
109
110
17.4k
        int read = file.read(m_data, bytesToRead);
111
17.4k
        if (read > 0) {
112
17.4k
            ok = true;
113
17.4k
            m_data[read] = '\0';
114
17.4k
            QString text = QString(codecFromContent(m_data, read).decode(QByteArrayView(m_data, read))).trimmed();
115
17.4k
            if (!text.isValidUtf16()) {
116
                // See if we are unlucky and the last byte we read was half of a utf16 character
117
83
                text.chop(1);
118
83
                if (!text.isValidUtf16()) {
119
64
                    return KIO::ThumbnailResult::fail();
120
64
                }
121
83
            }
122
            // FIXME: maybe strip whitespace and read more?
123
124
            // If the text contains tabs or consecutive spaces, it is probably
125
            // formatted using white space. Use a fixed pitch font in this case.
126
17.4k
            const auto textLines = QStringView(text).split(QLatin1Char('\n'));
127
252k
            for (const auto &line : textLines) {
128
252k
                const auto trimmedLine = line.trimmed();
129
252k
                if (trimmedLine.contains('\t') || trimmedLine.contains(QLatin1String("  "))) {
130
2.95k
                    font.setFamily(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
131
2.95k
                    break;
132
2.95k
                }
133
252k
            }
134
135
17.4k
            QColor bgColor = QColor(245, 245, 245); // light-grey background
136
17.4k
            m_pixmap.fill(bgColor);
137
138
17.4k
            QPainter painter(&m_pixmap);
139
140
17.4k
            QTextDocument textDocument(text);
141
142
            // QTextDocument only supports one margin value for all borders,
143
            // so we do a page-in-page behind its back, and do our own borders
144
17.4k
            textDocument.setDocumentMargin(0);
145
17.4k
            textDocument.setPageSize(canvasSize);
146
17.4k
            textDocument.setDefaultFont(font);
147
148
17.4k
            QTextOption textOption(Qt::AlignTop | Qt::AlignLeft);
149
17.4k
            textOption.setTabStopDistance(8 * painter.fontMetrics().horizontalAdvance(QLatin1Char(' ')));
150
17.4k
            textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
151
17.4k
            textDocument.setDefaultTextOption(textOption);
152
153
17.4k
            KSyntaxHighlighting::SyntaxHighlighter syntaxHighlighter;
154
17.4k
            syntaxHighlighter.setDefinition(m_highlightingRepository.definitionForFileName(path));
155
17.4k
            const auto highlightingTheme = m_highlightingRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme);
156
17.4k
            syntaxHighlighter.setTheme(highlightingTheme);
157
17.4k
            syntaxHighlighter.setDocument(&textDocument);
158
17.4k
            syntaxHighlighter.rehighlight();
159
160
            // draw page-in-page, with clipping as needed
161
17.4k
            painter.translate(xborder, yborder);
162
17.4k
            textDocument.drawContents(&painter, QRectF(QPointF(0, 0), canvasSize));
163
164
17.4k
            painter.end();
165
166
17.4k
            img = m_pixmap.toImage();
167
17.4k
        }
168
169
17.4k
        file.close();
170
17.4k
    }
171
17.4k
    return ok ? KIO::ThumbnailResult::pass(img) : KIO::ThumbnailResult::fail();
172
17.4k
}
173
174
#include "moc_textcreator.cpp"
175
#include "textcreator.moc"