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 &quot; (dnaber):
77 const QString strAmp("&amp;");
78 const QString nbsp("&nbsp;");
79 const QString strLt("&lt;");
80 const QString strGt("&gt;");
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