csvexport.cpp
1/* This file is part of the KDE project
2 Copyright (C) 2000 David Faure <faure@kde.org>
3 Copyright (C) 2004 Nicolas GOUTTE <goutte@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19*/
20
21#include <csvexport.h>
22
23#include <tqfile.h>
24#include <tqtextcodec.h>
25
26#include <kdebug.h>
27#include <tdemessagebox.h>
28#include <kgenericfactory.h>
29#include <KoFilterChain.h>
30#include <KoFilterManager.h>
31
32#include <kspread_map.h>
33#include <kspread_sheet.h>
34#include <kspread_doc.h>
35#include <kspread_view.h>
36#include <selection.h>
37
38#include <csvexportdialog.h>
39
40using namespace KSpread;
41
42typedef KGenericFactory<CSVExport, KoFilter> CSVExportFactory;
43K_EXPORT_COMPONENT_FACTORY( libcsvexport, CSVExportFactory( "kofficefilters" ) )
44
45class Cell
46{
47 public:
48 int row, col;
49 TQString text;
50
51 bool operator < ( const Cell & c ) const
52 {
53 return row < c.row || ( row == c.row && col < c.col );
54 }
55 bool operator == ( const Cell & c ) const
56 {
57 return row == c.row && col == c.col;
58 }
59};
60
61
62CSVExport::CSVExport( KoFilter *, const char *, const TQStringList & )
63 : KoFilter(), m_eol("\n")
64{
65}
66
67TQString CSVExport::exportCSVCell( Sheet const * const sheet, int col, int row, TQChar const & textQuote, TQChar csvDelimiter )
68{
69 // This function, given a cell, returns a string corresponding to its export in CSV format
70 // It proceeds by:
71 // - getting the value of the cell, if any
72 // - protecting quote characters within cells, if any
73 // - enclosing the cell in quotes if the cell is non empty
74
75 KSpread::Cell const * const cell = sheet->cellAt( col, row );
76 TQString text;
77
78 if ( !cell->isDefault() && !cell->isEmpty() )
79 {
80 if ( cell->isFormula() )
81 text = cell->strOutText();
82 else if ( !cell->link().isEmpty() )
83 text = cell->text(); // untested
84 else if( cell->isTime() )
85 text = cell->value().asTime().toString("hh:mm:ss");
86 else if( cell->isDate() )
87 text = cell->value().asDate().toString("yyyy-MM-dd");
88 else
89 text = cell->strOutText();
90 }
91
92 // quote only when needed (try to mimic excel)
93 bool quote = false;
94 if ( !text.isEmpty() )
95 {
96 if ( text.find( textQuote ) != -1 )
97 {
98 TQString doubleTextQuote(textQuote);
99 doubleTextQuote.append(textQuote);
100 text.replace(textQuote, doubleTextQuote);
101 quote = true;
102
103 } else if ( text[0].isSpace() || text[text.length()-1].isSpace() )
104 quote = true;
105 else if ( text.find( csvDelimiter ) != -1 )
106 quote = true;
107 else if ( text.find( "\n" ) != -1 || text.find( "\r" ) != -1 )
108 quote = true;
109 }
110
111 if ( quote ) {
112 text.prepend(textQuote);
113 text.append(textQuote);
114 }
115
116 return text;
117}
118
119// The reason why we use the KoDocument* approach and not the TQDomDocument
120// approach is because we don't want to export formulas but values !
121KoFilter::ConversionStatus CSVExport::convert( const TQCString & from, const TQCString & to )
122{
123 kdDebug(30501) << "CSVExport::convert" << endl;
124 KoDocument* document = m_chain->inputDocument();
125
126 if ( !document )
127 return KoFilter::StupidError;
128
129 if ( !::tqt_cast<const KSpread::Doc *>( document ) )
130 {
131 kdWarning(30501) << "document isn't a KSpread::Doc but a " << document->className() << endl;
132 return KoFilter::NotImplemented;
133 }
134 if ( ( to != "text/x-csv" && to != "text/plain" ) || from != "application/x-kspread" )
135 {
136 kdWarning(30501) << "Invalid mimetypes " << to << " " << from << endl;
137 return KoFilter::NotImplemented;
138 }
139
140 Doc const * const ksdoc = static_cast<const Doc *>(document);
141
142 if ( ksdoc->mimeType() != "application/x-kspread" )
143 {
144 kdWarning(30501) << "Invalid document mimetype " << ksdoc->mimeType() << endl;
145 return KoFilter::NotImplemented;
146 }
147
148 CSVExportDialog *expDialog = 0;
149 if (!m_chain->manager()->getBatchMode())
150 {
151 expDialog= new CSVExportDialog( 0 );
152
153 if (!expDialog)
154 {
155 kdError(30501) << "Dialog has not been created! Aborting!" << endl;
156 return KoFilter::StupidError;
157 }
158 expDialog->fillSheet( ksdoc->map() );
159
160 if ( !expDialog->exec() )
161 {
162 delete expDialog;
163 return KoFilter::UserCancelled;
164 }
165 }
166
167 TQTextCodec* codec = 0;
168 TQChar csvDelimiter;
169 if (expDialog)
170 {
171 codec = expDialog->getCodec();
172 if ( !codec )
173 {
174 delete expDialog;
175 return KoFilter::StupidError;
176 }
177 csvDelimiter = expDialog->getDelimiter();
178 m_eol = expDialog->getEndOfLine();
179 }
180 else
181 {
182 codec = TQTextCodec::codecForName("UTF-8");
183 csvDelimiter = ',';
184 }
185
186
187 // Now get hold of the sheet to export
188 // (Hey, this could be part of the dialog too, choosing which sheet to export....
189 // It's great to have parametrable filters... IIRC even MSOffice doesn't have that)
190 // Ok, for now we'll use the first sheet - my document has only one sheet anyway ;-)))
191
192 bool first = true;
193 TQString str;
194 TQChar textQuote;
195 if (expDialog)
196 textQuote = expDialog->getTextQuote();
197 else
198 textQuote = '"';
199
200 if ( expDialog && expDialog->exportSelectionOnly() )
201 {
202 kdDebug(30501) << "Export as selection mode" << endl;
203 View const * const view = static_cast<View*>(ksdoc->views().getFirst());
204
205 if ( !view ) // no view if embedded document
206 {
207 delete expDialog;
208 return KoFilter::StupidError;
209 }
210
211 Sheet const * const sheet = view->activeSheet();
212
213 TQRect selection = view->selectionInfo()->lastRange();
214 // Compute the highest row and column indexes (within the selection)
215 // containing non-empty cells, respectively called CSVMaxRow CSVMaxCol.
216 // The CSV will have CSVMaxRow rows, all with CSVMaxCol columns
217 int right = selection.right();
218 int bottom = selection.bottom();
219 int CSVMaxRow = 0;
220 int CSVMaxCol = 0;
221
222 for ( int idxRow = 1, row = selection.top(); row <= bottom; ++row, ++idxRow )
223 {
224 for ( int idxCol = 1, col = selection.left(); col <= right; ++col, ++idxCol )
225 {
226 if( ! sheet->cellAt( col, row )->isEmpty() )
227 {
228 if ( idxRow > CSVMaxRow )
229 CSVMaxRow = idxRow;
230
231 if ( idxCol > CSVMaxCol )
232 CSVMaxCol = idxCol;
233 }
234 }
235 }
236
237 for ( int idxRow = 1, row = selection.top();
238 row <= bottom && idxRow <= CSVMaxRow; ++row, ++idxRow )
239 {
240 int idxCol = 1;
241 for ( int col = selection.left();
242 col <= right && idxCol <= CSVMaxCol; ++col, ++idxCol )
243 {
244 str += exportCSVCell( sheet, col, row, textQuote, csvDelimiter );
245
246 if ( idxCol < CSVMaxCol )
247 str += csvDelimiter;
248 }
249
250 // This is to deal with the case of non-rectangular selections
251 for ( ; idxCol < CSVMaxCol; ++idxCol )
252 str += csvDelimiter;
253
254 str += m_eol;
255 }
256 }
257 else
258 {
259 kdDebug(30501) << "Export as full mode" << endl;
260 TQPtrListIterator<Sheet> it( ksdoc->map()->sheetList() );
261 for( ; it.current(); ++it )
262 {
263 Sheet const * const sheet = it.current();
264
265 if (expDialog && !expDialog->exportSheet( sheet->sheetName() ) )
266 {
267 continue;
268 }
269
270 // Compute the highest row and column indexes containing non-empty cells,
271 // respectively called CSVMaxRow CSVMaxCol.
272 // The CSV will have CSVMaxRow rows, all with CSVMaxCol columns
273 int sheetMaxRow = sheet->maxRow();
274 int sheetMaxCol = sheet->maxColumn();
275 int CSVMaxRow = 0;
276 int CSVMaxCol = 0;
277
278 for ( int row = 1 ; row <= sheetMaxRow ; ++row)
279 {
280 for ( int col = 1 ; col <= sheetMaxCol ; col++ )
281 {
282 if( ! sheet->cellAt( col, row )->isEmpty() )
283 {
284 if ( row > CSVMaxRow )
285 CSVMaxRow = row;
286
287 if ( col > CSVMaxCol )
288 CSVMaxCol = col;
289 }
290 }
291 }
292
293 // Skip the sheet altogether if it is empty
294 if ( CSVMaxRow + CSVMaxCol == 0)
295 continue;
296
297 kdDebug(30501) << "Max row x column: " << CSVMaxRow << " x " << CSVMaxCol << endl;
298
299 // Print sheet separators, except for the first sheet
300 if ( !first || ( expDialog && expDialog->printAlwaysSheetDelimiter() ) )
301 {
302 if ( !first)
303 str += m_eol;
304
305 TQString name;
306 if (expDialog)
307 name = expDialog->getSheetDelimiter();
308 else
309 name = "********<SHEETNAME>********";
310 const TQString tname( i18n("<SHEETNAME>") );
311 int pos = name.find( tname );
312 if ( pos != -1 )
313 {
314 name.replace( pos, tname.length(), sheet->sheetName() );
315 }
316 str += name;
317 str += m_eol;
318 str += m_eol;
319 }
320
321 first = false;
322
323
324 // this is just a bad approximation which fails for documents with less than 50 rows, but
325 // we don't need any progress stuff there anyway :) (Werner)
326 int value = 0;
327 int step = CSVMaxRow > 50 ? CSVMaxRow/50 : 1;
328
329 // Print the CSV for the sheet data
330 for ( int row = 1, i = 1 ; row <= CSVMaxRow ; ++row, ++i )
331 {
332 if ( i > step )
333 {
334 value += 2;
335 emit sigProgress(value);
336 i = 0;
337 }
338
339 TQString collect; // buffer delimiters while reading empty cells
340
341 for ( int col = 1 ; col <= CSVMaxCol ; col++ )
342 {
343 const TQString txt = exportCSVCell( sheet, col, row, textQuote, csvDelimiter );
344
345 // if we encounter a non-empty cell, commit the buffered delimiters
346 if (!txt.isEmpty()) {
347 str += collect + txt;
348 collect = TQString();
349 }
350
351 collect += csvDelimiter;
352 }
353 // Here, throw away buffered delimiters. They're trailing and therefore
354 // superfluous.
355
356 str += m_eol;
357 }
358 }
359 }
360
361 emit sigProgress(100);
362
363 TQFile out(m_chain->outputFile());
364 if ( !out.open( IO_WriteOnly ) )
365 {
366 kdError(30501) << "Unable to open output file!" << endl;
367 out.close();
368 delete expDialog;
369 return KoFilter::StupidError;
370 }
371
372 TQTextStream outStream( &out );
373 outStream.setCodec( codec );
374
375 outStream << str;
376
377 out.close();
378 delete expDialog;
379 return KoFilter::OK;
380}
381
382#include <csvexport.moc>