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 <QPixmap>
13 #include <QImage>
14 #include <QPainter>
15 #include <QPalette>
16 #include <QTextCodec>
17 #include <QTextDocument>
18
19 #include <KSyntaxHighlighting/SyntaxHighlighter>
20 #include <KSyntaxHighlighting/Theme>
21 #include <KSyntaxHighlighting/Definition>
22 #include <KDesktopFile>
23
24 // TODO Fix or remove kencodingprober code
25 // #include <kencodingprober.h>
26
27 extern "C"
28 {
new_creator()29 Q_DECL_EXPORT KIO::ThumbDevicePixelRatioDependentCreator *new_creator()
30 {
31 return new TextCreator;
32 }
33 }
34
TextCreator()35 TextCreator::TextCreator()
36 : KIO::ThumbDevicePixelRatioDependentCreator(),
37 m_data(nullptr),
38 m_dataSize(0) {}
39
~TextCreator()40 TextCreator::~TextCreator()
41 {
42 delete [] m_data;
43 }
44
codecFromContent(const char * data,int dataSize)45 static QTextCodec *codecFromContent(const char *data, int dataSize)
46 {
47 #if 0 // ### Use this when KEncodingProber does not return junk encoding for UTF-8 data)
48 KEncodingProber prober;
49 prober.feed(data, dataSize);
50 return QTextCodec::codecForName(prober.encoding());
51 #else
52 QByteArray ba = QByteArray::fromRawData(data, dataSize);
53 // try to detect UTF text, fall back to locale default (which is usually UTF-8)
54 return QTextCodec::codecForUtfText(ba, QTextCodec::codecForLocale());
55 #endif
56 }
57
create(const QString & path,int width,int height,QImage & img)58 bool TextCreator::create(const QString &path, int width, int height, QImage &img)
59 {
60 // Desktop files, .directory files, and flatpakrefs aren't traditional
61 // text files, so their icons should be shown instead
62 if (KDesktopFile::isDesktopFile(path)
63 || path.endsWith(QStringLiteral(".directory"))
64 || path.endsWith(QStringLiteral(".flatpakref"))
65 ) {
66 return false;
67 }
68
69 bool ok = false;
70
71 // determine some sizes...
72 // example: width: 60, height: 64
73 QSize pixmapSize( width * devicePixelRatio(), height * devicePixelRatio() );
74 if (height * 3 > width * 4)
75 pixmapSize.setHeight( width * 4 / 3 * devicePixelRatio());
76 else
77 pixmapSize.setWidth( height * 3 / 4 * devicePixelRatio());
78
79 if ( pixmapSize != m_pixmap.size() ) {
80 m_pixmap = QPixmap( pixmapSize );
81 m_pixmap.setDevicePixelRatio(devicePixelRatio());
82 }
83
84 // one pixel for the rectangle, the rest. whitespace
85 int xborder = 1 + pixmapSize.width()/16 / devicePixelRatio(); // minimum x-border
86 int yborder = 1 + pixmapSize.height()/16 / devicePixelRatio(); // minimum y-border
87
88 // this font is supposed to look good at small sizes
89 QFont font = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont);
90
91 font.setPixelSize( qMax(7, qMin( 10, ( pixmapSize.height()/ devicePixelRatio() - 2 * yborder ) / 16 ) ) );
92 QFontMetrics fm( font );
93
94 // calculate a better border so that the text is centered
95 const QSizeF canvasSize(pixmapSize.width() / devicePixelRatio() - 2 * xborder, pixmapSize.height() / devicePixelRatio() - 2 * yborder);
96 const int numLines = (int) (canvasSize.height() / fm.height());
97
98 // assumes an average line length of <= 120 chars
99 const int bytesToRead = 120 * numLines;
100
101 // create text-preview
102 QFile file( path );
103 if ( file.open( QIODevice::ReadOnly ))
104 {
105 if ( !m_data || m_dataSize < bytesToRead + 1 )
106 {
107 delete [] m_data;
108 m_data = new char[bytesToRead+1];
109 m_dataSize = bytesToRead + 1;
110 }
111
112 int read = file.read( m_data, bytesToRead );
113 if ( read > 0 )
114 {
115 ok = true;
116 m_data[read] = '\0';
117 QString text = codecFromContent( m_data, read )->toUnicode( m_data, read ).trimmed();
118 // FIXME: maybe strip whitespace and read more?
119
120 // If the text contains tabs or consecutive spaces, it is probably
121 // formatted using white space. Use a fixed pitch font in this case.
122 const auto textLines = text.splitRef(QLatin1Char('\n'));
123 for (const auto& line : textLines) {
124 const auto trimmedLine = line.trimmed();
125 if ( trimmedLine.contains( '\t' ) || trimmedLine.contains( " " ) ) {
126 font.setFamily( QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
127 break;
128 }
129 }
130
131 QColor bgColor = QColor ( 245, 245, 245 ); // light-grey background
132 m_pixmap.fill( bgColor );
133
134 QPainter painter( &m_pixmap );
135
136 QTextDocument textDocument(text);
137
138 // QTextDocument only supports one margin value for all borders,
139 // so we do a page-in-page behind its back, and do our own borders
140 textDocument.setDocumentMargin(0);
141 textDocument.setPageSize(canvasSize);
142 textDocument.setDefaultFont(font);
143
144 QTextOption textOption( Qt::AlignTop | Qt::AlignLeft );
145 textOption.setTabStopDistance(8 * painter.fontMetrics().horizontalAdvance(QLatin1Char(' ')));
146 textOption.setWrapMode( QTextOption::WrapAtWordBoundaryOrAnywhere );
147 textDocument.setDefaultTextOption(textOption);
148
149 KSyntaxHighlighting::SyntaxHighlighter syntaxHighlighter;
150 syntaxHighlighter.setDefinition(m_highlightingRepository.definitionForFileName(path));
151 const auto highlightingTheme = m_highlightingRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme);
152 syntaxHighlighter.setTheme(highlightingTheme);
153 syntaxHighlighter.setDocument(&textDocument);
154 syntaxHighlighter.rehighlight();
155
156 // draw page-in-page, with clipping as needed
157 painter.translate(xborder, yborder);
158 textDocument.drawContents(&painter, QRectF(QPointF(0, 0), canvasSize));
159
160 painter.end();
161
162 img = m_pixmap.toImage();
163 }
164
165 file.close();
166 }
167 return ok;
168 }
169