File indexing completed on 2025-06-29 10:33:18

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