Warning, file /office/calligra/filters/sheets/html/htmlexport.cc was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /* This file is part of the KDE project
0002    SPDX-FileCopyrightText: 2001 Eva Brucherseifer <eva@kde.org>
0003    SPDX-FileCopyrightText: 2005 Bram Schoenmakers <bramschoenmakers@kde.nl>
0004    based on kspread csv export filter by David Faure
0005 
0006    SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "htmlexport.h"
0010 #include "exportdialog.h"
0011 
0012 // #include <QFile>
0013 #include <QTextCodec>
0014 // #include <QTextStream>
0015 // #include <QByteArray>
0016 // #include <QUrl>
0017 
0018 #include <kpluginfactory.h>
0019 #include <KoFilterChain.h>
0020 #include <KoFilterManager.h>
0021 #include <KoDocumentInfo.h>
0022 #include <CalligraVersionWrapper.h>
0023 
0024 #include <sheets/engine/CellBaseStorage.h>
0025 #include <sheets/engine/Util.h>
0026 #include <sheets/core/Cell.h>
0027 #include <sheets/core/DocBase.h>
0028 #include <sheets/core/Map.h>
0029 #include <sheets/core/Sheet.h>
0030 #include <sheets/core/Style.h>
0031 
0032 using namespace Calligra::Sheets;
0033 
0034 Q_LOGGING_CATEGORY(lcHtml, "calligra.filter.html")
0035 
0036 K_PLUGIN_FACTORY_WITH_JSON(HTMLExportFactory, "calligra_filter_sheets2html.json",
0037                            registerPlugin<HTMLExport>();)
0038 
0039 const QString html_table_tag = "table";
0040 const QString html_table_options = QString(" border=\"%1\" cellspacing=\"%2\"");
0041 const QString html_row_tag = "tr";
0042 const QString html_row_options = "";
0043 const QString html_cell_tag = "td";
0044 const QString html_cell_options = "";
0045 const QString html_bold = "b";
0046 const QString html_italic = "i";
0047 const QString html_underline = "u";
0048 const QString html_right = "right";
0049 const QString html_left = "left";
0050 const QString html_center = "center";
0051 const QString html_top = "top";
0052 const QString html_bottom = "bottom";
0053 const QString html_middle = "middle";
0054 const QString html_h1 = "h1";
0055 
0056 HTMLExport::HTMLExport(QObject* parent, const QVariantList&) :
0057         KoFilter(parent), m_dialog(new ExportDialog())
0058 {
0059 }
0060 
0061 HTMLExport::~HTMLExport()
0062 {
0063     delete m_dialog;
0064 }
0065 
0066 // HTML entities, AFAIK we don't need to escape " to &quot; (dnaber):
0067 const QString strAmp("&amp;");
0068 const QString nbsp("&nbsp;");
0069 const QString strLt("&lt;");
0070 const QString strGt("&gt;");
0071 
0072 // The reason why we use the KoDocument* approach and not the QDomDocument
0073 // approach is because we don't want to export formulas but values !
0074 KoFilter::ConversionStatus HTMLExport::convert(const QByteArray& from, const QByteArray& to)
0075 {
0076     if (to != "text/html" || from != "application/x-kspread") {
0077         qWarning(lcHtml) << "Invalid mimetypes " << to << " " << from;
0078         return KoFilter::NotImplemented;
0079     }
0080 
0081     KoDocument* document = m_chain->inputDocument();
0082 
0083     if (!document)
0084         return KoFilter::StupidError;
0085 
0086     if (!::qobject_cast<const Calligra::Sheets::DocBase *>(document)) {   // it's safer that way :)
0087         qWarning(lcHtml) << "document isn't a Calligra::Sheets::DocBase but a " << document->metaObject()->className();
0088         return KoFilter::NotImplemented;
0089     }
0090 
0091     const DocBase * ksdoc = dynamic_cast<const DocBase *>(document);
0092 
0093     if (ksdoc->mimeType() != "application/x-kspread") {
0094         qWarning(lcHtml) << "Invalid document mimetype " << ksdoc->mimeType();
0095         return KoFilter::NotImplemented;
0096     }
0097 
0098     QString filenameBase = m_chain->outputFile();
0099     filenameBase = filenameBase.left(filenameBase.lastIndexOf('.'));
0100 
0101     QStringList sheets;
0102     for(SheetBase* bsheet : ksdoc->map()->sheetList()) {
0103         Sheet *sheet = dynamic_cast<Sheet *>(bsheet);
0104         int rows = 0;
0105         int columns = 0;
0106         detectFilledCells(sheet, rows, columns);
0107         m_rowmap[ sheet->sheetName()] = rows;
0108         m_columnmap[ sheet->sheetName()] = columns;
0109 
0110         if (rows > 0 && columns > 0) {
0111             sheets.append(sheet->sheetName());
0112         }
0113     }
0114     m_dialog->setSheets(sheets);
0115     if (!m_chain->manager()->getBatchMode() ) {
0116         if (m_dialog->exec() == QDialog::Rejected) {
0117             return KoFilter::UserCancelled;
0118         }
0119     }
0120 
0121     sheets = m_dialog->sheets();
0122     QString str;
0123     for (int i = 0; i < sheets.count() ; ++i) {
0124         SheetBase *bsheet = ksdoc->map()->findSheet(sheets[i]);
0125         Sheet *sheet = dynamic_cast<Sheet *>(bsheet);
0126         if (!sheet)
0127             continue;
0128 
0129         QString file = fileName(filenameBase, sheet->sheetName(), sheets.count() > 1);
0130 
0131         if (m_dialog->separateFiles() || sheets[i] == sheets.first()) {
0132             str.clear();
0133             openPage(sheet, document, str);
0134             writeTOC(sheets, filenameBase, str);
0135         }
0136 
0137         convertSheet(sheet, str, m_rowmap[ sheet->sheetName()], m_columnmap[ sheet->sheetName()]);
0138 
0139         if (m_dialog->separateFiles() || sheets[i] == sheets.last()) {
0140             closePage(str);
0141             QFile out(file);
0142             if (!out.open(QIODevice::WriteOnly)) {
0143                 qWarning(lcHtml) << "Unable to open output file!" << endl;
0144                 out.close();
0145                 return KoFilter::FileNotFound;
0146             }
0147             QTextStream streamOut(&out);
0148             streamOut.setCodec(m_dialog->encoding());
0149             streamOut << str << endl;
0150             out.close();
0151         }
0152 
0153         if (!m_dialog->separateFiles()) {
0154             createSheetSeparator(str);
0155         }
0156 
0157     }
0158 
0159     emit sigProgress(100);
0160     return KoFilter::OK;
0161 }
0162 
0163 void HTMLExport::openPage(Sheet *sheet, KoDocument *document, QString &str)
0164 {
0165     QString title;
0166     KoDocumentInfo *info = document->documentInfo();
0167     if (info && !info->aboutInfo("title").isEmpty())
0168         title = info->aboutInfo("title") + " - ";
0169     title += sheet->sheetName();
0170 
0171     // header
0172     str = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "
0173           " \"http://www.w3.org/TR/html4/loose.dtd\"> \n"
0174           "<html>\n"
0175           "<head>\n"
0176           "<meta http-equiv=\"Content-Type\" " +
0177           QString("content=\"text/html; charset=%1\">\n").arg(QString(m_dialog->encoding()->name())) +
0178           "<meta name=\"Generator\" "
0179           "content=\"KSpread HTML Export Filter Version = " +
0180           CalligraVersionWrapper::versionString() +
0181           "\">\n";
0182 
0183     // Insert stylesheet
0184     if (!m_dialog->customStyleURL().isEmpty()) {
0185         str += "<link ref=\"stylesheet\" type=\"text/css\" href=\"" +
0186                m_dialog->customStyleURL().url() +
0187                "\" title=\"Style\" >\n";
0188     }
0189 
0190     str += "<title>" + title + "</title>\n"
0191            "</head>\n" +
0192            QString("<body bgcolor=\"#FFFFFF\" dir=\"%1\">\n").arg(
0193                (sheet->layoutDirection() == Qt::RightToLeft) ? "rtl" : "ltr") +
0194 
0195            "<a name=\"__top\">\n";
0196 }
0197 
0198 void HTMLExport::closePage(QString &str)
0199 {
0200     str += "<p align=\"" + html_center + "\"><a href=\"#__top\">" + i18n("Top") + "</a></p>\n"
0201            "</body>\n"
0202            "</html>\n\n";
0203 }
0204 
0205 void HTMLExport::convertSheet(Sheet *sheet, QString &str, int iMaxUsedRow, int iMaxUsedColumn)
0206 {
0207     QString emptyLines;
0208 
0209     // Either we get hold of KSpreadTable::m_dctCells and apply the old method below (for sorting)
0210     // or, cleaner and already sorted, we use KSpreadTable's API (slower probably, though)
0211     int iMaxRow = sheet->cellStorage()->rows();
0212 
0213     if (!m_dialog->separateFiles())
0214         str += "<a name=\"" + sheet->sheetName().toLower().trimmed() + "\">\n";
0215 
0216     str += ("<h1>" + sheet->sheetName() + "</h1><br>\n");
0217 
0218     // this is just a bad approximation which fails for documents with less than 50 rows, but
0219     // we don't need any progress stuff there anyway :) (Werner)
0220     int value = 0;
0221     int step = iMaxRow > 50 ? iMaxRow / 50 : 1;
0222     int i = 1;
0223 
0224     str += '<' + html_table_tag + html_table_options.arg(m_dialog->useBorders() ? "1" : "0").arg(m_dialog->pixelsBetweenCells()) +
0225            QString("dir=\"%1\">\n").arg((sheet->layoutDirection() == Qt::RightToLeft) ? "rtl" : "ltr");
0226 
0227     unsigned int nonempty_cells_prev = 0;
0228 
0229     for (int currentrow = 1 ; currentrow <= iMaxUsedRow ; ++currentrow, ++i) {
0230         if (i > step) {
0231             value += 2;
0232             emit sigProgress(value);
0233             i = 0;
0234         }
0235 
0236         QString line;
0237         unsigned int nonempty_cells = 0;
0238 
0239         for (int currentcolumn = 1 ; currentcolumn <= iMaxUsedColumn ; currentcolumn++) {
0240             Cell cell(sheet, currentcolumn, currentrow);
0241             const Style style = cell.effectiveStyle();
0242             if (cell.needsPrinting())
0243                 nonempty_cells++;
0244             QString text;
0245             // FIXME: some formatting seems to be missing with cell.userInput(), e.g.
0246             // "208.00" in KSpread will be "208" in HTML (not always?!)
0247             bool link = false;
0248 
0249             if (!cell.link().isEmpty()) {
0250                 if (Util::localReferenceAnchor(cell.link())) {
0251                     text = cell.userInput();
0252                 } else {
0253                     text = " <A href=\"" + cell.link() + "\">" + cell.userInput() + "</A>";
0254                     link = true;
0255                 }
0256             } else
0257                 text = cell.displayText();
0258 #if 0
0259             switch (cell.content()) {
0260             case Cell::Text:
0261                 text = cell.userInput();
0262                 break;
0263             case Cell::RichText:
0264             case Cell::VisualFormula:
0265                 text = cell.userInput(); // untested
0266                 break;
0267             case Cell::Formula:
0268                 cell.calc(true);   // Incredible, cells are not calculated if the document was just opened
0269                 text = cell.valueString();
0270                 break;
0271             }
0272             text = cell.prefix(currentrow, currentcolumn) + ' ' + text + ' '
0273                    + cell.postfix(currentrow, currentcolumn);
0274 #endif
0275             line += "  <" + html_cell_tag + html_cell_options;
0276             if (text.isRightToLeft() != (sheet->layoutDirection() == Qt::RightToLeft))
0277                 line += QString(" dir=\"%1\" ").arg(text.isRightToLeft() ? "rtl" : "ltr");
0278             const QColor bgcolor = style.backgroundColor();
0279             if (bgcolor.isValid() && bgcolor.name() != "#ffffff") // change color only for non-white cells
0280                 line += " bgcolor=\"" + bgcolor.name() + "\"";
0281 
0282             switch ((Style::HAlign)cell.effectiveAlignX()) {
0283             case Style::Left:
0284                 line += " align=\"" + html_left + "\"";
0285                 break;
0286             case Style::Right:
0287                 line += " align=\"" + html_right + "\"";
0288                 break;
0289             case Style::Center:
0290                 line += " align=\"" + html_center + "\"";
0291                 break;
0292             case Style::HAlignUndefined:
0293             case Style::Justified:
0294                 break;
0295             }
0296             switch ((Style::VAlign) style.valign()) {
0297             case Style::Top:
0298                 line += " valign=\"" + html_top + "\"";
0299                 break;
0300             case Style::Middle:
0301                 line += " valign=\"" + html_middle + "\"";
0302                 break;
0303             case Style::Bottom:
0304                 line += " valign=\"" + html_bottom + "\"";
0305                 break;
0306             case Style::VAlignUndefined:
0307             case Style::VJustified:
0308             case Style::VDistributed:
0309                 break;
0310             }
0311             line += " width=\"" + QString::number(cell.width()) + "\"";
0312             line += " height=\"" + QString::number(cell.height()) + "\"";
0313 
0314             if (cell.mergedXCells() > 0) {
0315                 QString tmp;
0316                 int extra_cells = cell.mergedXCells();
0317                 line += " colspan=\"" + tmp.setNum(extra_cells + 1) + "\"";
0318                 currentcolumn += extra_cells;
0319             }
0320             text = text.trimmed();
0321             if (!text.isEmpty() && text.at(0) == '!') {
0322                 // this is supposed to be markup, just remove the '!':
0323                 text = text.right(text.length() - 1);
0324             } else if (!link) {
0325                 // Escape HTML characters.
0326                 text.replace('&' , strAmp)
0327                 .replace('<' , strLt)
0328                 .replace('>' , strGt)
0329                 .replace(' ' , nbsp);
0330             }
0331             line += ">\n";
0332 
0333             if (style.bold()) {
0334                 text.insert(0, '<' + html_bold + '>');
0335                 text.append("</" + html_bold + '>');
0336             }
0337             if (style.italic()) {
0338                 text.insert(0, '<' + html_italic + '>');
0339                 text.append("</" + html_italic + '>');
0340             }
0341             if (style.underline()) {
0342                 text.insert(0, '<' + html_underline + '>');
0343                 text.append("</" + html_underline + '>');
0344             }
0345             QColor textColor = style.fontColor();
0346             if (textColor.isValid() && textColor.name() != "#000000") { // change color only for non-default text
0347                 text.insert(0, "<font color=\"" + textColor.name() + "\">");
0348                 text.append("</font>");
0349             }
0350             line += ' ' + text +
0351                     "\n  </" + html_cell_tag + ">\n";
0352         }
0353 
0354         if (nonempty_cells == 0 && nonempty_cells_prev == 0) {
0355             nonempty_cells_prev = nonempty_cells;
0356             // skip line if there's more than one empty line
0357             continue;
0358         } else {
0359             nonempty_cells_prev = nonempty_cells;
0360             str += emptyLines +
0361                    '<' + html_row_tag + html_row_options + ">\n" +
0362                    line +
0363                    "</" + html_row_tag + '>';
0364             emptyLines.clear();
0365             // Append a CR, but in a temp string -> if no other real line,
0366             // then those will be dropped
0367             emptyLines += '\n';
0368         }
0369     }
0370     str += "\n</" + html_table_tag + ">\n<br>\n";
0371 }
0372 
0373 void HTMLExport::createSheetSeparator(QString &str)
0374 {
0375     str += "<p align=\"" + html_center + "\"><a href=\"#__top\">" + i18n("Top") + "</a></p>\n"
0376            "<hr width=\"80%\">\n";
0377 }
0378 
0379 void HTMLExport::writeTOC(const QStringList &sheets, const QString &base, QString &str)
0380 {
0381     // don't create TOC for 1 sheet
0382     if (sheets.count() == 1)
0383         return;
0384 
0385     str += "<p align=\"" + html_center + "\">\n";
0386 
0387     for (int i = 0 ; i < sheets.count() ; ++i) {
0388         str += "<a href=\"";
0389 
0390         if (m_dialog->separateFiles()) {
0391             str += fileName(base, sheets[i], sheets.count() > 1);
0392         } else {
0393             str += '#' + sheets[i].toLower().trimmed();
0394         }
0395 
0396         str += "\">" + sheets[i] + "</a>\n";
0397         if (i != sheets.count() - 1)
0398             str += " - ";
0399     }
0400 
0401     str += "</p><hr width=\"80%\">\n";
0402 }
0403 
0404 QString HTMLExport::fileName(const QString &base, const QString &sheetName, bool multipleFiles)
0405 {
0406     QString fileName = base;
0407     if (m_dialog->separateFiles() && multipleFiles) {
0408         fileName += '-' + sheetName;
0409     }
0410     fileName += ".html";
0411 
0412     return fileName;
0413 }
0414 
0415 void HTMLExport::detectFilledCells(Sheet *sheet, int &rows, int &columns)
0416 {
0417     int iMaxColumn = sheet->cellStorage()->columns();
0418     int iMaxRow = sheet->cellStorage()->rows();
0419 
0420     rows = 0;
0421     columns = 0;
0422 
0423     for (int currentrow = 1 ; currentrow <= iMaxRow ; ++currentrow) {
0424         Cell cell;
0425         int iUsedColumn = 0;
0426         for (int currentcolumn = 1 ; currentcolumn <= iMaxColumn ; currentcolumn++) {
0427             cell = Cell(sheet, currentcolumn, currentrow);
0428             if (!cell.isDefault() && !cell.isEmpty()) {
0429                 iUsedColumn = currentcolumn;
0430             }
0431         }
0432         if (!cell.isNull())
0433             iUsedColumn += cell.mergedXCells();
0434         if (iUsedColumn > columns)
0435             columns = iUsedColumn;
0436         if (iUsedColumn > 0)
0437             rows = currentrow;
0438     }
0439 }
0440 
0441 #include <htmlexport.moc>