File indexing completed on 2025-01-19 13:27:13

0001 /* This file is part of the KDE project
0002    Copyright (C) 2000 David Faure <faure@kde.org>
0003    Copyright (C) 2004 Nicolas GOUTTE <goutte@kde.org>
0004 
0005    This library is free software; you can redistribute it and/or
0006    modify it under the terms of the GNU Library General Public
0007    License as published by the Free Software Foundation; either
0008    version 2 of the License, or (at your option) any later version.
0009 
0010    This library is distributed in the hope that it will be useful,
0011    but WITHOUT ANY WARRANTY; without even the implied warranty of
0012    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013    Library General Public License for more details.
0014 
0015    You should have received a copy of the GNU Library General Public License
0016    along with this library; see the file COPYING.LIB.  If not, write to
0017    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018  * Boston, MA 02110-1301, USA.
0019 */
0020 
0021 #include <csvexport.h>
0022 
0023 #include <QFile>
0024 #include <QTextCodec>
0025 #include <QTextStream>
0026 #include <QByteArray>
0027 #include <QDebug>
0028 
0029 #include <kpluginfactory.h>
0030 #include <KoFilterChain.h>
0031 #include <KoFilterManager.h>
0032 #include <KoPart.h>
0033 
0034 #include <sheets/CellStorage.h>
0035 #include <sheets/Map.h>
0036 #include <sheets/Sheet.h>
0037 #include <sheets/part/Doc.h>
0038 #include <sheets/Value.h>
0039 #include <sheets/part/View.h>
0040 #include <sheets/ui/Selection.h>
0041 
0042 #include <csvexportdialog.h>
0043 
0044 using namespace Calligra::Sheets;
0045 
0046 K_PLUGIN_FACTORY_WITH_JSON(CSVExportFactory, "calligra_filter_sheets2csv.json", registerPlugin<CSVExport>();)
0047 
0048 Q_LOGGING_CATEGORY(lcCsvExport, "calligra.filter.csv.export")
0049 
0050 class Cell
0051 {
0052 public:
0053     int row, col;
0054     QString text;
0055 
0056     bool operator < (const Cell & c) const {
0057         return row < c.row || (row == c.row && col < c.col);
0058     }
0059     bool operator == (const Cell & c) const {
0060         return row == c.row && col == c.col;
0061     }
0062 };
0063 
0064 
0065 CSVExport::CSVExport(QObject* parent, const QVariantList &)
0066         : KoFilter(parent), m_eol("\n")
0067 {
0068 }
0069 
0070 QString CSVExport::exportCSVCell(const Calligra::Sheets::Doc* doc, Sheet const * const sheet,
0071                                  int col, int row, QChar const & textQuote, QChar csvDelimiter)
0072 {
0073     // This function, given a cell, returns a string corresponding to its export in CSV format
0074     // It proceeds by:
0075     //  - getting the value of the cell, if any
0076     //  - protecting quote characters within cells, if any
0077     //  - enclosing the cell in quotes if the cell is non empty
0078 
0079     Q_UNUSED(doc);
0080     const Calligra::Sheets::Cell cell(sheet, col, row);
0081     QString text;
0082 
0083     if (!cell.isDefault() && !cell.isEmpty()) {
0084         if (cell.isFormula())
0085             text = cell.displayText();
0086         else if (!cell.link().isEmpty())
0087             text = cell.userInput(); // untested
0088         else if (cell.isTime())
0089             text = cell.value().asTime().toString("hh:mm:ss");
0090         else if (cell.isDate())
0091             text = cell.value().asDate(sheet->map()->calculationSettings()).toString("yyyy-MM-dd");
0092         else
0093             text = cell.displayText();
0094     }
0095 
0096     // quote only when needed (try to mimic excel)
0097     bool quote = false;
0098     if (!text.isEmpty()) {
0099         if (text.indexOf(textQuote) != -1) {
0100             QString doubleTextQuote(textQuote);
0101             doubleTextQuote.append(textQuote);
0102             text.replace(textQuote, doubleTextQuote);
0103             quote = true;
0104 
0105         } else if (text[0].isSpace() || text[text.length()-1].isSpace())
0106             quote = true;
0107         else if (text.indexOf(csvDelimiter) != -1)
0108             quote = true;
0109     }
0110 
0111     if (quote) {
0112         text.prepend(textQuote);
0113         text.append(textQuote);
0114     }
0115 
0116     return text;
0117 }
0118 
0119 // The reason why we use the KoDocument* approach and not the QDomDocument
0120 // approach is because we don't want to export formulas but values !
0121 KoFilter::ConversionStatus CSVExport::convert(const QByteArray & from, const QByteArray & to)
0122 {
0123     qDebug(lcCsvExport) << "CSVExport::convert";
0124     KoDocument* document = m_chain->inputDocument();
0125 
0126     if (!document)
0127         return KoFilter::StupidError;
0128 
0129     if (!qobject_cast<const Calligra::Sheets::Doc *>(document)) {
0130         qWarning(lcCsvExport) << "document isn't a Calligra::Sheets::Doc but a " << document->metaObject()->className();
0131         return KoFilter::NotImplemented;
0132     }
0133     if ((to != "text/csv" && to != "text/plain") || from != "application/vnd.oasis.opendocument.spreadsheet") {
0134         qWarning(lcCsvExport) << "Invalid mimetypes " << to << " " << from;
0135         return KoFilter::NotImplemented;
0136     }
0137 
0138     Doc *ksdoc = qobject_cast<Doc *>(document);
0139 
0140     if (ksdoc->mimeType() != "application/vnd.oasis.opendocument.spreadsheet") {
0141         qWarning(lcCsvExport) << "Invalid document mimetype " << ksdoc->mimeType();
0142         return KoFilter::NotImplemented;
0143     }
0144 
0145     CSVExportDialog *expDialog = 0;
0146     if (!m_chain->manager()->getBatchMode()) {
0147         expDialog = new CSVExportDialog(0);
0148 
0149         if (!expDialog) {
0150             qCritical(lcCsvExport) << "Dialog has not been created! Aborting!" << endl;
0151             return KoFilter::StupidError;
0152         }
0153         expDialog->fillSheet(ksdoc->map());
0154 
0155         if (!expDialog->exec()) {
0156             delete expDialog;
0157             return KoFilter::UserCancelled;
0158         }
0159     }
0160 
0161     QTextCodec* codec = 0;
0162     QChar csvDelimiter;
0163     if (expDialog) {
0164         codec = expDialog->getCodec();
0165         if (!codec) {
0166             delete expDialog;
0167             return KoFilter::StupidError;
0168         }
0169         csvDelimiter = expDialog->getDelimiter();
0170         m_eol = expDialog->getEndOfLine();
0171     } else {
0172         codec = QTextCodec::codecForName("UTF-8");
0173         csvDelimiter = ',';
0174     }
0175 
0176 
0177     // Now get hold of the sheet to export
0178     // (Hey, this could be part of the dialog too, choosing which sheet to export....
0179     //  It's great to have parametrable filters... IIRC even MSOffice doesn't have that)
0180     // Ok, for now we'll use the first sheet - my document has only one sheet anyway ;-)))
0181 
0182     bool first = true;
0183     QString str;
0184     QChar textQuote;
0185     if (expDialog)
0186         textQuote = expDialog->getTextQuote();
0187     else
0188         textQuote = '"';
0189 
0190     if (expDialog && expDialog->exportSelectionOnly()) {
0191         qDebug(lcCsvExport) << "Export as selection mode";
0192         View *view = ksdoc->documentPart()->views().isEmpty() ? 0 : static_cast<View*>(ksdoc->documentPart()->views().first());
0193 
0194         if (!view) { // no view if embedded document
0195             delete expDialog;
0196             return KoFilter::StupidError;
0197         }
0198 
0199         Sheet const * const sheet = view->activeSheet();
0200 
0201         QRect selection = view->selection()->lastRange();
0202         // Compute the highest row and column indexes (within the selection)
0203         // containing non-empty cells, respectively called CSVMaxRow CSVMaxCol.
0204         // The CSV will have CSVMaxRow rows, all with CSVMaxCol columns
0205         int right       = selection.right();
0206         int bottom      = selection.bottom();
0207         int CSVMaxRow   = 0;
0208         int CSVMaxCol   = 0;
0209 
0210         for (int idxRow = 1, row = selection.top(); row <= bottom; ++row, ++idxRow) {
0211             for (int idxCol = 1, col = selection.left(); col <= right; ++col, ++idxCol) {
0212                 if (!Calligra::Sheets::Cell(sheet, col, row).isEmpty()) {
0213                     if (idxRow > CSVMaxRow)
0214                         CSVMaxRow = idxRow;
0215 
0216                     if (idxCol > CSVMaxCol)
0217                         CSVMaxCol = idxCol;
0218                 }
0219             }
0220         }
0221 
0222         for (int idxRow = 1, row = selection.top();
0223                 row <= bottom && idxRow <= CSVMaxRow; ++row, ++idxRow) {
0224             int idxCol = 1;
0225             for (int col = selection.left();
0226                     col <= right && idxCol <= CSVMaxCol; ++col, ++idxCol) {
0227                 str += exportCSVCell(ksdoc, sheet, col, row, textQuote, csvDelimiter);
0228 
0229                 if (idxCol < CSVMaxCol)
0230                     str += csvDelimiter;
0231             }
0232 
0233             // This is to deal with the case of non-rectangular selections
0234             for (; idxCol < CSVMaxCol; ++idxCol)
0235                 str += csvDelimiter;
0236 
0237             str += m_eol;
0238         }
0239     } else {
0240         qDebug(lcCsvExport) << "Export as full mode";
0241         foreach(Sheet const * const sheet, ksdoc->map()->sheetList()) {
0242             if (expDialog && !expDialog->exportSheet(sheet->sheetName())) {
0243                 continue;
0244             }
0245 
0246             // Compute the highest row and column indexes containing non-empty cells,
0247             // respectively called CSVMaxRow CSVMaxCol.
0248             // The CSV will have CSVMaxRow rows, all with CSVMaxCol columns
0249             int sheetMaxRow = sheet->cellStorage()->rows();
0250             int sheetMaxCol = sheet->cellStorage()->columns();
0251             int CSVMaxRow   = 0;
0252             int CSVMaxCol   = 0;
0253 
0254             for (int row = 1 ; row <= sheetMaxRow ; ++row) {
0255                 for (int col = 1 ; col <= sheetMaxCol ; col++) {
0256                     if (!Calligra::Sheets::Cell(sheet, col, row).isEmpty()) {
0257                         if (row > CSVMaxRow)
0258                             CSVMaxRow = row;
0259 
0260                         if (col > CSVMaxCol)
0261                             CSVMaxCol = col;
0262                     }
0263                 }
0264             }
0265 
0266             // Skip the sheet altogether if it is empty
0267             if (CSVMaxRow + CSVMaxCol == 0)
0268                 continue;
0269 
0270             qDebug(lcCsvExport) << "Max row x column:" << CSVMaxRow << " x" << CSVMaxCol;
0271 
0272             // Print sheet separators, except for the first sheet
0273             if (!first || (expDialog && expDialog->printAlwaysSheetDelimiter())) {
0274                 if (!first)
0275                     str += m_eol;
0276 
0277                 QString name;
0278                 if (expDialog)
0279                     name = expDialog->getSheetDelimiter();
0280                 else
0281                     name = "********<SHEETNAME>********";
0282                 const QString tname(i18n("<SHEETNAME>"));
0283                 int pos = name.indexOf(tname);
0284                 if (pos != -1) {
0285                     name.replace(pos, tname.length(), sheet->sheetName());
0286                 }
0287                 str += name + m_eol + m_eol;
0288             }
0289 
0290             first = false;
0291 
0292 
0293             // this is just a bad approximation which fails for documents with less than 50 rows, but
0294             // we don't need any progress stuff there anyway :) (Werner)
0295             int value = 0;
0296             int step  = CSVMaxRow > 50 ? CSVMaxRow / 50 : 1;
0297 
0298             // Print the CSV for the sheet data
0299             for (int row = 1, i = 1 ; row <= CSVMaxRow ; ++row, ++i) {
0300                 if (i > step) {
0301                     value += 2;
0302                     emit sigProgress(value);
0303                     i = 0;
0304                 }
0305 
0306                 QString collect;  // buffer delimiters while reading empty cells
0307 
0308                 for (int col = 1 ; col <= CSVMaxCol ; col++) {
0309                     const QString txt = exportCSVCell(ksdoc, sheet, col, row, textQuote, csvDelimiter);
0310 
0311                     // if we encounter a non-empty cell, commit the buffered delimiters
0312                     if (!txt.isEmpty()) {
0313                         str += collect + txt;
0314                         collect.clear();
0315                     }
0316 
0317                     collect += csvDelimiter;
0318                 }
0319                 // Here, throw away buffered delimiters. They're trailing and therefore
0320                 // superfluous.
0321 
0322                 str += m_eol;
0323             }
0324         }
0325     }
0326 
0327     emit sigProgress(100);
0328 
0329     QFile out(m_chain->outputFile());
0330     if (!out.open(QIODevice::WriteOnly)) {
0331         qCritical(lcCsvExport) << "Unable to open output file!" << endl;
0332         out.close();
0333         delete expDialog;
0334         return KoFilter::StupidError;
0335     }
0336 
0337     QTextStream outStream(&out);
0338     outStream.setCodec(codec);
0339 
0340     outStream << str;
0341 
0342     out.close();
0343     delete expDialog;
0344     return KoFilter::OK;
0345 }
0346 
0347 #include <csvexport.moc>