/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" |