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>