Coverage Report

Created: 2026-01-25 07:18

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
12.1k
    : KIO::ThumbnailCreator(parent, args)
31
12.1k
    , m_data(nullptr)
32
12.1k
    , m_dataSize(0)
33
12.1k
{
34
12.1k
}
35
36
TextCreator::~TextCreator()
37
12.1k
{
38
12.1k
    delete[] m_data;
39
12.1k
}
40
41
static QStringDecoder codecFromContent(const char *data, int dataSize)
42
12.1k
{
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
12.1k
    return QStringDecoder(QStringDecoder::encodingForData(QByteArrayView(data, dataSize)).value_or(QStringDecoder::System));
50
12.1k
#endif
51
12.1k
}
52
53
KIO::ThumbnailResult TextCreator::create(const KIO::ThumbnailRequest &request)
54
12.1k
{
55
12.1k
    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
12.1k
    if (KDesktopFile::isDesktopFile(path) || path.endsWith(QStringLiteral(".directory")) || path.endsWith(QStringLiteral(".flatpakref"))) {
59
0
        return KIO::ThumbnailResult::fail();
60
0
    }
61
62
12.1k
    bool ok = false;
63
64
    // determine some sizes...
65
    // example: width: 60, height: 64
66
67
12.1k
    const int width = request.targetSize().width();
68
12.1k
    const int height = request.targetSize().height();
69
12.1k
    const qreal dpr = request.devicePixelRatio();
70
71
12.1k
    QImage img;
72
73
12.1k
    QSize pixmapSize(width, height);
74
12.1k
    if (height * 3 > width * 4)
75
0
        pixmapSize.setHeight(width * 4 / 3);
76
12.1k
    else
77
12.1k
        pixmapSize.setWidth(height * 3 / 4);
78
79
12.1k
    if (pixmapSize != m_pixmap.size()) {
80
12.1k
        m_pixmap = QPixmap(pixmapSize);
81
12.1k
        m_pixmap.setDevicePixelRatio(dpr);
82
12.1k
    }
83
84
    // one pixel for the rectangle, the rest. whitespace
85
12.1k
    int xborder = 1 + pixmapSize.width() / 16 / dpr; // minimum x-border
86
12.1k
    int yborder = 1 + pixmapSize.height() / 16 / dpr; // minimum y-border
87
88
    // this font is supposed to look good at small sizes
89
12.1k
    QFont font = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont);
90
91
12.1k
    font.setPixelSize(qMax(7.0, qMin(10.0, (pixmapSize.height() / dpr - 2 * yborder) / 16)));
92
12.1k
    QFontMetrics fm(font);
93
94
    // calculate a better border so that the text is centered
95
12.1k
    const QSizeF canvasSize(pixmapSize.width() / dpr - 2 * xborder, pixmapSize.height() / dpr - 2 * yborder);
96
12.1k
    const int numLines = (int)(canvasSize.height() / fm.height());
97
98
    // assumes an average line length of <= 120 chars
99
12.1k
    const int bytesToRead = 120 * numLines;
100
101
    // create text-preview
102
12.1k
    QFile file(path);
103
12.1k
    if (file.open(QIODevice::ReadOnly)) {
104
12.1k
        if (!m_data || m_dataSize < bytesToRead + 1) {
105
12.1k
            delete[] m_data;
106
12.1k
            m_data = new char[bytesToRead + 1];
107
12.1k
            m_dataSize = bytesToRead + 1;
108
12.1k
        }
109
110
12.1k
        int read = file.read(m_data, bytesToRead);
111
12.1k
        if (read > 0) {
112
12.1k
            ok = true;
113
12.1k
            m_data[read] = '\0';
114
12.1k
            QString text = QString(codecFromContent(m_data, read).decode(QByteArrayView(m_data, read))).trimmed();
115
            // FIXME: maybe strip whitespace and read more?
116
117
            // If the text contains tabs or consecutive spaces, it is probably
118
            // formatted using white space. Use a fixed pitch font in this case.
119
12.1k
            const auto textLines = QStringView(text).split(QLatin1Char('\n'));
120
156k
            for (const auto &line : textLines) {
121
156k
                const auto trimmedLine = line.trimmed();
122
156k
                if (trimmedLine.contains('\t') || trimmedLine.contains(QLatin1String("  "))) {
123
2.74k
                    font.setFamily(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
124
2.74k
                    break;
125
2.74k
                }
126
156k
            }
127
128
12.1k
            QColor bgColor = QColor(245, 245, 245); // light-grey background
129
12.1k
            m_pixmap.fill(bgColor);
130
131
12.1k
            QPainter painter(&m_pixmap);
132
133
12.1k
            QTextDocument textDocument(text);
134
135
            // QTextDocument only supports one margin value for all borders,
136
            // so we do a page-in-page behind its back, and do our own borders
137
12.1k
            textDocument.setDocumentMargin(0);
138
12.1k
            textDocument.setPageSize(canvasSize);
139
12.1k
            textDocument.setDefaultFont(font);
140
141
12.1k
            QTextOption textOption(Qt::AlignTop | Qt::AlignLeft);
142
12.1k
            textOption.setTabStopDistance(8 * painter.fontMetrics().horizontalAdvance(QLatin1Char(' ')));
143
12.1k
            textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
144
12.1k
            textDocument.setDefaultTextOption(textOption);
145
146
12.1k
            KSyntaxHighlighting::SyntaxHighlighter syntaxHighlighter;
147
12.1k
            syntaxHighlighter.setDefinition(m_highlightingRepository.definitionForFileName(path));
148
12.1k
            const auto highlightingTheme = m_highlightingRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme);
149
12.1k
            syntaxHighlighter.setTheme(highlightingTheme);
150
12.1k
            syntaxHighlighter.setDocument(&textDocument);
151
12.1k
            syntaxHighlighter.rehighlight();
152
153
            // draw page-in-page, with clipping as needed
154
12.1k
            painter.translate(xborder, yborder);
155
12.1k
            textDocument.drawContents(&painter, QRectF(QPointF(0, 0), canvasSize));
156
157
12.1k
            painter.end();
158
159
12.1k
            img = m_pixmap.toImage();
160
12.1k
        }
161
162
12.1k
        file.close();
163
12.1k
    }
164
12.1k
    return ok ? KIO::ThumbnailResult::pass(img) : KIO::ThumbnailResult::fail();
165
12.1k
}
166
167
#include "moc_textcreator.cpp"
168
#include "textcreator.moc"