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>