1 /* This file is part of the KDE project
2 Copyright (C) 2001 Eva Brucherseifer <eva@kde.org>
3 Copyright (C) 2005 Bram Schoenmakers <bramschoenmakers@kde.nl>
4 based on kspread csv export filter by David Faure
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public
8 License as published by the Free Software Foundation; either
9 version 2 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
20 */
21
22 #include <htmlexport.h>
23 #include <exportdialog.h>
24
25 #include <QFile>
26 #include <QTextCodec>
27 #include <QTextStream>
28 #include <QByteArray>
29 #include <QUrl>
30
31 #include <kdebug.h>
32 #include <kpluginfactory.h>
33 #include <KoFilterChain.h>
34 #include <KoFilterManager.h>
35 #include <KoDocumentInfo.h>
36 #include <CalligraVersionWrapper.h>
37
38 #include <sheets/CellStorage.h>
39 #include <sheets/Map.h>
40 #include <sheets/Sheet.h>
41 #include <sheets/part/Doc.h>
42 #include <sheets/Util.h>
43
44 using namespace Calligra::Sheets;
45
46 K_PLUGIN_FACTORY_WITH_JSON(HTMLExportFactory, "calligra_filter_sheets2html.json",
47 registerPlugin<HTMLExport>();)
48
49 const QString html_table_tag = "table";
50 const QString html_table_options = QString(" border=\"%1\" cellspacing=\"%2\"");
51 const QString html_row_tag = "tr";
52 const QString html_row_options = "";
53 const QString html_cell_tag = "td";
54 const QString html_cell_options = "";
55 const QString html_bold = "b";
56 const QString html_italic = "i";
57 const QString html_underline = "u";
58 const QString html_right = "right";
59 const QString html_left = "left";
60 const QString html_center = "center";
61 const QString html_top = "top";
62 const QString html_bottom = "bottom";
63 const QString html_middle = "middle";
64 const QString html_h1 = "h1";
65
HTMLExport(QObject * parent,const QVariantList &)66 HTMLExport::HTMLExport(QObject* parent, const QVariantList&) :
67 KoFilter(parent), m_dialog(new ExportDialog())
68 {
69 }
70
~HTMLExport()71 HTMLExport::~HTMLExport()
72 {
73 delete m_dialog;
74 }
75
76 // HTML enitities, AFAIK we don't need to escape " to " (dnaber):
77 const QString strAmp("&");
78 const QString nbsp(" ");
79 const QString strLt("<");
80 const QString strGt(">");
81
82 // The reason why we use the KoDocument* approach and not the QDomDocument
83 // approach is because we don't want to export formulas but values !
convert(const QByteArray & from,const QByteArray & to)84 KoFilter::ConversionStatus HTMLExport::convert(const QByteArray& from, const QByteArray& to)
85 {
86 if (to != "text/html" || from != "application/x-kspread") {
87 kWarning(30501) << "Invalid mimetypes " << to << " " << from;
88 return KoFilter::NotImplemented;
89 }
90
91 KoDocument* document = m_chain->inputDocument();
92
93 if (!document)
94 return KoFilter::StupidError;
95
96 if (!::qobject_cast<const Calligra::Sheets::Doc *>(document)) { // it's safer that way :)
97 kWarning(30501) << "document isn't a Calligra::Sheets::Doc but a " << document->metaObject()->className();
98 return KoFilter::NotImplemented;
99 }
100
101 const Doc * ksdoc = static_cast<const Doc *>(document);
102
103 if (ksdoc->mimeType() != "application/x-kspread") {
104 kWarning(30501) << "Invalid document mimetype " << ksdoc->mimeType();
105 return KoFilter::NotImplemented;
106 }
107
108 QString filenameBase = m_chain->outputFile();
109 filenameBase = filenameBase.left(filenameBase.lastIndexOf('.'));
110
111 QStringList sheets;
112 foreach(Sheet* sheet, ksdoc->map()->sheetList()) {
113 int rows = 0;
114 int columns = 0;
115 detectFilledCells(sheet, rows, columns);
116 m_rowmap[ sheet->sheetName()] = rows;
117 m_columnmap[ sheet->sheetName()] = columns;
118
119 if (rows > 0 && columns > 0) {
120 sheets.append(sheet->sheetName());
121 }
122 }
123 m_dialog->setSheets(sheets);
124 if (!m_chain->manager()->getBatchMode() ) {
125 if (m_dialog->exec() == QDialog::Rejected) {
126 return KoFilter::UserCancelled;
127 }
128 }
129
130 Sheet* sheet = 0;
131 sheets = m_dialog->sheets();
132 QString str;
133 for (int i = 0; i < sheets.count() ; ++i) {
134 sheet = ksdoc->map()->findSheet(sheets[i]);
135 if (!sheet)
136 continue;
137
138 QString file = fileName(filenameBase, sheet->sheetName(), sheets.count() > 1);
139
140 if (m_dialog->separateFiles() || sheets[i] == sheets.first()) {
141 str.clear();
142 openPage(sheet, document, str);
143 writeTOC(sheets, filenameBase, str);
144 }
145
146 convertSheet(sheet, str, m_rowmap[ sheet->sheetName()], m_columnmap[ sheet->sheetName()]);
147
148 if (m_dialog->separateFiles() || sheets[i] == sheets.last()) {
149 closePage(str);
150 QFile out(file);
151 if (!out.open(QIODevice::WriteOnly)) {
152 kError(30501) << "Unable to open output file!" << endl;
153 out.close();
154 return KoFilter::FileNotFound;
155 }
156 QTextStream streamOut(&out);
157 streamOut.setCodec(m_dialog->encoding());
158 streamOut << str << endl;
159 out.close();
160 }
161
162 if (!m_dialog->separateFiles()) {
163 createSheetSeparator(str);
164 }
165
166 }
167
168 emit sigProgress(100);
169 return KoFilter::OK;
170 }
171
openPage(Sheet * sheet,KoDocument * document,QString & str)172 void HTMLExport::openPage(Sheet *sheet, KoDocument *document, QString &str)
173 {
174 QString title;
175 KoDocumentInfo *info = document->documentInfo();
176 if (info && !info->aboutInfo("title").isEmpty())
177 title = info->aboutInfo("title") + " - ";
178 title += sheet->sheetName();
179
180 // header
181 str = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "
182 " \"http://www.w3.org/TR/html4/loose.dtd\"> \n"
183 "<html>\n"
184 "<head>\n"
185 "<meta http-equiv=\"Content-Type\" " +
186 QString("content=\"text/html; charset=%1\">\n").arg(QString(m_dialog->encoding()->name())) +
187 "<meta name=\"Generator\" "
188 "content=\"KSpread HTML Export Filter Version = " +
189 CalligraVersionWrapper::versionString() +
190 "\">\n";
191
192 // Insert stylesheet
193 if (!m_dialog->customStyleURL().isEmpty()) {
194 str += "<link ref=\"stylesheet\" type=\"text/css\" href=\"" +
195 m_dialog->customStyleURL().url() +
196 "\" title=\"Style\" >\n";
197 }
198
199 str += "<title>" + title + "</title>\n"
200 "</head>\n" +
201 QString("<body bgcolor=\"#FFFFFF\" dir=\"%1\">\n").arg(
202 (sheet->layoutDirection() == Qt::RightToLeft) ? "rtl" : "ltr") +
203
204 "<a name=\"__top\">\n";
205 }
206
closePage(QString & str)207 void HTMLExport::closePage(QString &str)
208 {
209 str += "<p align=\"" + html_center + "\"><a href=\"#__top\">" + i18n("Top") + "</a></p>\n"
210 "</body>\n"
211 "</html>\n\n";
212 }
213
convertSheet(Sheet * sheet,QString & str,int iMaxUsedRow,int iMaxUsedColumn)214 void HTMLExport::convertSheet(Sheet *sheet, QString &str, int iMaxUsedRow, int iMaxUsedColumn)
215 {
216 QString emptyLines;
217
218 // Either we get hold of KSpreadTable::m_dctCells and apply the old method below (for sorting)
219 // or, cleaner and already sorted, we use KSpreadTable's API (slower probably, though)
220 int iMaxRow = sheet->cellStorage()->rows();
221
222 if (!m_dialog->separateFiles())
223 str += "<a name=\"" + sheet->sheetName().toLower().trimmed() + "\">\n";
224
225 str += ("<h1>" + sheet->sheetName() + "</h1><br>\n");
226
227 // this is just a bad approximation which fails for documents with less than 50 rows, but
228 // we don't need any progress stuff there anyway :) (Werner)
229 int value = 0;
230 int step = iMaxRow > 50 ? iMaxRow / 50 : 1;
231 int i = 1;
232
233 str += '<' + html_table_tag + html_table_options.arg(m_dialog->useBorders() ? "1" : "0").arg(m_dialog->pixelsBetweenCells()) +
234 QString("dir=\"%1\">\n").arg((sheet->layoutDirection() == Qt::RightToLeft) ? "rtl" : "ltr");
235
236 unsigned int nonempty_cells_prev = 0;
237
238 for (int currentrow = 1 ; currentrow <= iMaxUsedRow ; ++currentrow, ++i) {
239 if (i > step) {
240 value += 2;
241 emit sigProgress(value);
242 i = 0;
243 }
244
245 QString separators;
246 QString line;
247 unsigned int nonempty_cells = 0;
248
249 for (int currentcolumn = 1 ; currentcolumn <= iMaxUsedColumn ; currentcolumn++) {
250 Cell cell(sheet, currentcolumn, currentrow);
251 const Style style = cell.effectiveStyle();
252 if (cell.needsPrinting())
253 nonempty_cells++;
254 QString text;
255 // FIXME: some formatting seems to be missing with cell.userInput(), e.g.
256 // "208.00" in KSpread will be "208" in HTML (not always?!)
257 bool link = false;
258
259 if (!cell.link().isEmpty()) {
260 if (Util::localReferenceAnchor(cell.link())) {
261 text = cell.userInput();
262 } else {
263 text = " <A href=\"" + cell.link() + "\">" + cell.userInput() + "</A>";
264 link = true;
265 }
266 } else
267 text = cell.displayText();
268 #if 0
269 switch (cell.content()) {
270 case Cell::Text:
271 text = cell.userInput();
272 break;
273 case Cell::RichText:
274 case Cell::VisualFormula:
275 text = cell.userInput(); // untested
276 break;
277 case Cell::Formula:
278 cell.calc(true); // Incredible, cells are not calculated if the document was just opened
279 text = cell.valueString();
280 break;
281 }
282 text = cell.prefix(currentrow, currentcolumn) + ' ' + text + ' '
283 + cell.postfix(currentrow, currentcolumn);
284 #endif
285 line += " <" + html_cell_tag + html_cell_options;
286 if (text.isRightToLeft() != (sheet->layoutDirection() == Qt::RightToLeft))
287 line += QString(" dir=\"%1\" ").arg(text.isRightToLeft() ? "rtl" : "ltr");
288 const QColor bgcolor = style.backgroundColor();
289 if (bgcolor.isValid() && bgcolor.name() != "#ffffff") // change color only for non-white cells
290 line += " bgcolor=\"" + bgcolor.name() + "\"";
291
292 switch ((Style::HAlign)cell.effectiveAlignX()) {
293 case Style::Left:
294 line += " align=\"" + html_left + "\"";
295 break;
296 case Style::Right:
297 line += " align=\"" + html_right + "\"";
298 break;
299 case Style::Center:
300 line += " align=\"" + html_center + "\"";
301 break;
302 case Style::HAlignUndefined:
303 case Style::Justified:
304 break;
305 }
306 switch ((Style::VAlign) style.valign()) {
307 case Style::Top:
308 line += " valign=\"" + html_top + "\"";
309 break;
310 case Style::Middle:
311 line += " valign=\"" + html_middle + "\"";
312 break;
313 case Style::Bottom:
314 line += " valign=\"" + html_bottom + "\"";
315 break;
316 case Style::VAlignUndefined:
317 case Style::VJustified:
318 case Style::VDistributed:
319 break;
320 }
321 line += " width=\"" + QString::number(cell.width()) + "\"";
322 line += " height=\"" + QString::number(cell.height()) + "\"";
323
324 if (cell.mergedXCells() > 0) {
325 QString tmp;
326 int extra_cells = cell.mergedXCells();
327 line += " colspan=\"" + tmp.setNum(extra_cells + 1) + "\"";
328 currentcolumn += extra_cells;
329 }
330 text = text.trimmed();
331 if (!text.isEmpty() && text.at(0) == '!') {
332 // this is supposed to be markup, just remove the '!':
333 text = text.right(text.length() - 1);
334 } else if (!link) {
335 // Escape HTML characters.
336 text.replace('&' , strAmp)
337 .replace('<' , strLt)
338 .replace('>' , strGt)
339 .replace(' ' , nbsp);
340 }
341 line += ">\n";
342
343 if (style.bold()) {
344 text.insert(0, '<' + html_bold + '>');
345 text.append("</" + html_bold + '>');
346 }
347 if (style.italic()) {
348 text.insert(0, '<' + html_italic + '>');
349 text.append("</" + html_italic + '>');
350 }
351 if (style.underline()) {
352 text.insert(0, '<' + html_underline + '>');
353 text.append("</" + html_underline + '>');
354 }
355 QColor textColor = style.fontColor();
356 if (textColor.isValid() && textColor.name() != "#000000") { // change color only for non-default text
357 text.insert(0, "<font color=\"" + textColor.name() + "\">");
358 text.append("</font>");
359 }
360 line += ' ' + text +
361 "\n </" + html_cell_tag + ">\n";
362 }
363
364 if (nonempty_cells == 0 && nonempty_cells_prev == 0) {
365 nonempty_cells_prev = nonempty_cells;
366 // skip line if there's more than one empty line
367 continue;
368 } else {
369 nonempty_cells_prev = nonempty_cells;
370 str += emptyLines +
371 '<' + html_row_tag + html_row_options + ">\n" +
372 line +
373 "</" + html_row_tag + '>';
374 emptyLines.clear();
375 // Append a CR, but in a temp string -> if no other real line,
376 // then those will be dropped
377 emptyLines += '\n';
378 }
379 }
380 str += "\n</" + html_table_tag + ">\n<br>\n";
381 }
382
createSheetSeparator(QString & str)383 void HTMLExport::createSheetSeparator(QString &str)
384 {
385 str += "<p align=\"" + html_center + "\"><a href=\"#__top\">" + i18n("Top") + "</a></p>\n"
386 "<hr width=\"80%\">\n";
387 }
388
writeTOC(const QStringList & sheets,const QString & base,QString & str)389 void HTMLExport::writeTOC(const QStringList &sheets, const QString &base, QString &str)
390 {
391 // don't create TOC for 1 sheet
392 if (sheets.count() == 1)
393 return;
394
395 str += "<p align=\"" + html_center + "\">\n";
396
397 for (int i = 0 ; i < sheets.count() ; ++i) {
398 str += "<a href=\"";
399
400 if (m_dialog->separateFiles()) {
401 str += fileName(base, sheets[i], sheets.count() > 1);
402 } else {
403 str += '#' + sheets[i].toLower().trimmed();
404 }
405
406 str += "\">" + sheets[i] + "</a>\n";
407 if (i != sheets.count() - 1)
408 str += " - ";
409 }
410
411 str += "</p><hr width=\"80%\">\n";
412 }
413
fileName(const QString & base,const QString & sheetName,bool multipleFiles)414 QString HTMLExport::fileName(const QString &base, const QString &sheetName, bool multipleFiles)
415 {
416 QString fileName = base;
417 if (m_dialog->separateFiles() && multipleFiles) {
418 fileName += '-' + sheetName;
419 }
420 fileName += ".html";
421
422 return fileName;
423 }
424
detectFilledCells(Sheet * sheet,int & rows,int & columns)425 void HTMLExport::detectFilledCells(Sheet *sheet, int &rows, int &columns)
426 {
427 int iMaxColumn = sheet->cellStorage()->columns();
428 int iMaxRow = sheet->cellStorage()->rows();
429
430 rows = 0;
431 columns = 0;
432
433 for (int currentrow = 1 ; currentrow <= iMaxRow ; ++currentrow) {
434 Cell cell;
435 int iUsedColumn = 0;
436 for (int currentcolumn = 1 ; currentcolumn <= iMaxColumn ; currentcolumn++) {
437 cell = Cell(sheet, currentcolumn, currentrow);
438 QString text;
439 if (!cell.isDefault() && !cell.isEmpty()) {
440 iUsedColumn = currentcolumn;
441 }
442 }
443 if (!cell.isNull())
444 iUsedColumn += cell.mergedXCells();
445 if (iUsedColumn > columns)
446 columns = iUsedColumn;
447 if (iUsedColumn > 0)
448 rows = currentrow;
449 }
450 }
451
452 #include <htmlexport.moc>
453