1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
2 /* libwps
3 * Version: MPL 2.0 / LGPLv2.1+
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 *
9 * Major Contributor(s):
10 * Copyright (C) 2006, 2007 Andrew Ziem
11 * Copyright (C) 2004 Marc Maurer (uwog@uwog.net)
12 * Copyright (C) 2004-2006 Fridrich Strba (fridrich.strba@bluewin.ch)
13 *
14 * For minor contributions see the git repository.
15 *
16 * Alternatively, the contents of this file may be used under the terms
17 * of the GNU Lesser General Public License Version 2.1 or later
18 * (LGPLv2.1+), in which case the provisions of the LGPLv2.1+ are
19 * applicable instead of those above.
20 */
21
22 #include <stdlib.h>
23 #include <string.h>
24
25 #include <algorithm>
26 #include <cctype>
27 #include <cmath>
28 #include <sstream>
29 #include <limits>
30 #include <stack>
31
32 #include <librevenge-stream/librevenge-stream.h>
33
34 #include "libwps_internal.h"
35 #include "libwps_tools_win.h"
36
37 #include "WPSCell.h"
38 #include "WKSContentListener.h"
39 #include "WPSEntry.h"
40 #include "WPSFont.h"
41 #include "WPSStream.h"
42 #include "WPSTable.h"
43
44 #include "Quattro.h"
45 #include "QuattroFormula.h"
46
47 #include "QuattroSpreadsheet.h"
48
49 namespace QuattroSpreadsheetInternal
50 {
51 //! a class used to store a style of a cell in QuattroSpreadsheet
52 struct Style final : public WPSCellFormat
53 {
54 //! construtor
StyleQuattroSpreadsheetInternal::Style55 explicit Style(libwps_tools_win::Font::Type type)
56 : WPSCellFormat()
57 , m_fontType(type)
58 , m_fileFormat(0xFF)
59 , m_alignAcrossColumn(false)
60 , m_extra("")
61 {
62 }
63 Style(Style const &)=default;
64 //! destructor
65 ~Style() final;
66 //! operator<<
67 friend std::ostream &operator<<(std::ostream &o, Style const &style);
68 //! operator==
69 bool operator==(Style const &st) const;
70 //! operator!=
operator !=QuattroSpreadsheetInternal::Style71 bool operator!=(Style const &st) const
72 {
73 return !(*this==st);
74 }
75 //! font encoding type
76 libwps_tools_win::Font::Type m_fontType;
77 //! the file format
78 int m_fileFormat;
79 //! flag to know if we must align across column
80 bool m_alignAcrossColumn;
81 /** extra data */
82 std::string m_extra;
83 };
84
~Style()85 Style::~Style()
86 {
87 }
88
89 //! operator<<
operator <<(std::ostream & o,Style const & style)90 std::ostream &operator<<(std::ostream &o, Style const &style)
91 {
92 o << static_cast<WPSCellFormat const &>(style) << ",";
93 if (style.m_fileFormat!=0xFF)
94 o << "format=" << std::hex << style.m_fileFormat << std::dec << ",";
95 if (style.m_extra.length())
96 o << "extra=[" << style.m_extra << "],";
97
98 return o;
99 }
100
operator ==(Style const & st) const101 bool Style::operator==(Style const &st) const
102 {
103 if (m_fontType!=st.m_fontType || m_fileFormat!=st.m_fileFormat) return false;
104 int diff = WPSCellFormat::compare(st);
105 if (diff) return false;
106 return m_fileFormat==st.m_fileFormat && m_alignAcrossColumn==st.m_alignAcrossColumn && m_extra==st.m_extra;
107 }
108
109 //! a cellule of a Quattro spreadsheet
110 class Cell final : public WPSCell
111 {
112 public:
113 /// constructor
Cell(libwps_tools_win::Font::Type type)114 explicit Cell(libwps_tools_win::Font::Type type)
115 : m_fontType(type)
116 , m_fileFormat(0xFF)
117 , m_styleId(-1)
118 , m_alignAcrossColumn(false)
119 , m_content()
120 , m_hasGraphic(false)
121 , m_stream() { }
122
123 //! operator<<
124 friend std::ostream &operator<<(std::ostream &o, Cell const &cell);
125
126 //! call when a cell must be send
127 bool send(WPSListenerPtr &/*listener*/) final;
128
129 //! call when the content of a cell must be send
sendContent(WPSListenerPtr &)130 bool sendContent(WPSListenerPtr &/*listener*/) final
131 {
132 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Cell::sendContent: must not be called\n"));
133 return false;
134 }
135 //! update the cell format using file format
updateFormat()136 void updateFormat()
137 {
138 if (m_fileFormat==0xFF)
139 return;
140 switch ((m_fileFormat>>4)&7)
141 {
142 case 0:
143 case 6: // checkme: date format set by system
144 switch (m_fileFormat&0xF)
145 {
146 case 1: // +/- : kind of bool
147 setFormat(F_BOOLEAN);
148 break;
149 case 2: // default
150 break;
151 case 3:
152 setFormat(F_TEXT);
153 break;
154 case 4:
155 setFormat(F_TEXT);
156 m_font.m_attributes |= WPS_HIDDEN_BIT;
157 break;
158 case 5:
159 setDTFormat(F_DATE, "%d %b %y");
160 break;
161 case 6:
162 setDTFormat(F_DATE, "%d %b");
163 break;
164 case 7:
165 setDTFormat(F_DATE, "%b-%d");
166 break;
167 case 8:
168 setDTFormat(F_DATE, "%m/%d/%y");
169 break;
170 case 9:
171 setDTFormat(F_DATE, "%m/%d");
172 break;
173 case 0xa:
174 setDTFormat(F_TIME, "%I:%M:%S%p");
175 break;
176 case 0xb:
177 setDTFormat(F_TIME, "%I:%M%p");
178 break;
179 case 0xc:
180 setDTFormat(F_TIME, "%H:%M:%S");
181 break;
182 case 0xd:
183 setDTFormat(F_TIME, "%H:%M");
184 break;
185 case 0xe:
186 setDTFormat(F_TIME, "%y");
187 break;
188 case 0xf:
189 setDTFormat(F_TIME, "%b");
190 break;
191 default:
192 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Cell::updateFormat: unknown format %x\n", unsigned(m_fileFormat)));
193 break;
194 }
195 break;
196 case 1: // fixed
197 setFormat(F_NUMBER, 1);
198 setDigits(m_fileFormat&0xF);
199 break;
200 case 2: // scientific
201 setFormat(F_NUMBER, 2);
202 setDigits(m_fileFormat&0xF);
203 break;
204 case 3: // currency
205 setFormat(F_NUMBER, 4);
206 setDigits(m_fileFormat&0xF);
207 break;
208 case 4: // percent
209 setFormat(F_NUMBER, 3);
210 setDigits(m_fileFormat&0xF);
211 break;
212 case 5: // decimal
213 setFormat(F_NUMBER, 1);
214 setDigits(m_fileFormat&0xF);
215 break;
216 case 7: // fixme use UserFormat (m_fileFormat&0xF) in Quattro.cpp at least to decode date...
217 {
218 static bool first=true;
219 if (first)
220 {
221 first=false;
222 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Cell::updateFormat: user defined format is not supported\n"));
223 }
224 break;
225 }
226 default:
227 break;
228 }
229 }
230 //! font encoding type
231 libwps_tools_win::Font::Type m_fontType;
232 //! the file format
233 int m_fileFormat;
234 //! the style id
235 int m_styleId;
236 //! flag to know if we must align across column
237 bool m_alignAcrossColumn;
238 //! the content
239 WKSContentListener::CellContent m_content;
240 //! a flag to know a cell has some graphic
241 bool m_hasGraphic;
242 //! the text stream(used to send text's zone)
243 std::shared_ptr<WPSStream> m_stream;
244 };
send(WPSListenerPtr &)245 bool Cell::send(WPSListenerPtr &/*listener*/)
246 {
247 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Cell::send: must not be called\n"));
248 return false;
249 }
250
251 //! operator<<
operator <<(std::ostream & o,Cell const & cell)252 std::ostream &operator<<(std::ostream &o, Cell const &cell)
253 {
254 o << reinterpret_cast<WPSCell const &>(cell) << cell.m_content << ",";
255 if (cell.m_fileFormat!=0xFF)
256 o << "format=" << std::hex << cell.m_fileFormat << std::dec << ",";
257 return o;
258 }
259
260 //! the spreadsheet of a Quattro Spreadsheet
261 class Spreadsheet
262 {
263 public:
264 //! a constructor
Spreadsheet(int id,libwps_tools_win::Font::Type fontType)265 Spreadsheet(int id, libwps_tools_win::Font::Type fontType)
266 : m_id(id)
267 , m_numCols(0)
268 , m_rowHeightMap()
269 , m_heightDefault(13) // fixme: use zone d2
270 , m_widthCols()
271 , m_widthDefault(54) // fixme: use zone d4
272 , m_positionToCellMap()
273 , m_dummyCell(fontType)
274 {
275 }
276 //! return a cell corresponding to a spreadsheet, create one if needed
getCell(Vec2i const & pos,libwps_tools_win::Font::Type type)277 Cell &getCell(Vec2i const &pos, libwps_tools_win::Font::Type type)
278 {
279 if (m_positionToCellMap.find(pos)==m_positionToCellMap.end())
280 {
281 Cell cell(type);
282 cell.setPosition(pos);
283 if (pos[0]<0 || pos[0]>255)
284 {
285 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Spreadsheet::getCell: find unexpected col=%d\n", pos[0]));
286 return m_dummyCell;
287 }
288 m_positionToCellMap.insert(std::map<Vec2i, Cell>::value_type(pos,cell));
289 }
290 return m_positionToCellMap.find(pos)->second;
291 }
292 //! returns true if the spreedsheet is empty
empty() const293 bool empty() const
294 {
295 return m_positionToCellMap.empty();
296 }
297 //! set the columns size
setColumnWidth(int col,int w=-1)298 void setColumnWidth(int col, int w=-1)
299 {
300 if (col < 0) return;
301 if (col >= int(m_widthCols.size())) m_widthCols.resize(size_t(col)+1, -1);
302 m_widthCols[size_t(col)] = w;
303 if (col >= m_numCols) m_numCols=col+1;
304 }
305
306 //! return the columns format
getWidths() const307 std::vector<WPSColumnFormat> getWidths() const
308 {
309 std::vector<WPSColumnFormat> widths;
310 WPSColumnFormat defWidth(m_widthDefault), actWidth;
311 defWidth.m_useOptimalWidth=true;
312 int repeat=0;
313 for (auto const &w : m_widthCols)
314 {
315 WPSColumnFormat newWidth;
316 if (w < 0)
317 newWidth=defWidth;
318 else
319 newWidth=WPSColumnFormat(float(w)/20.f);
320 if (repeat && newWidth!=actWidth)
321 {
322 actWidth.m_numRepeat=repeat;
323 widths.push_back(actWidth);
324 repeat=0;
325 }
326 if (repeat==0)
327 actWidth=newWidth;
328 ++repeat;
329 }
330 if (repeat)
331 {
332 actWidth.m_numRepeat=repeat;
333 widths.push_back(actWidth);
334 }
335 return widths;
336 }
337 //! set the rows size in TWIP
setRowHeight(int row,int h)338 void setRowHeight(int row, int h)
339 {
340 auto rIt=m_rowHeightMap.lower_bound(Vec2i(-1,row));
341 if (rIt!=m_rowHeightMap.end() && rIt->first[0]<=row && rIt->first[1]>=row)
342 {
343 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Spreadsheet::setRowHeight: oops, row %d is already set\n", row));
344 return;
345 }
346 if (h>=0)
347 m_rowHeightMap[Vec2i(row,row)]=h;
348 }
349 //! set the rows size in TWIP
setRowHeights(int minRow,int maxRow,int h)350 void setRowHeights(int minRow, int maxRow, int h)
351 {
352 auto rIt=m_rowHeightMap.lower_bound(Vec2i(-1,minRow));
353 while (rIt!=m_rowHeightMap.end())
354 {
355 auto const &cells=rIt->first;
356 if (cells[0]>maxRow) break;
357 if (cells[1]>=minRow)
358 {
359 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::Spreadsheet::setRowHeight: oops, some rows are already set in %dx%d\n", minRow, maxRow));
360 return;
361 }
362 ++rIt;
363 }
364 if (h>=0)
365 m_rowHeightMap[Vec2i(minRow,maxRow)]=h;
366 }
367 //! returns the row size in point
getRowHeight(int row) const368 float getRowHeight(int row) const
369 {
370 auto rIt=m_rowHeightMap.lower_bound(Vec2i(-1,row));
371 if (rIt!=m_rowHeightMap.end() && rIt->first[0]<=row && rIt->first[1]>=row)
372 return float(rIt->second)/20.f;
373 return m_heightDefault;
374 }
375 //! returns the height of a row in point and updated repeated row
getRowHeight(int row,int & numRepeated) const376 float getRowHeight(int row, int &numRepeated) const
377 {
378 auto rIt=m_rowHeightMap.lower_bound(Vec2i(-1,row));
379 if (rIt!=m_rowHeightMap.end() && rIt->first[0]<=row && rIt->first[1]>=row)
380 {
381 numRepeated=rIt->first[1]-row+1;
382 return float(rIt->second)/20.f;
383 }
384 numRepeated=10000;
385 return m_heightDefault;
386 }
387 //! try to compress the list of row height
compressRowHeights()388 void compressRowHeights()
389 {
390 auto oldMap=m_rowHeightMap;
391 m_rowHeightMap.clear();
392 int actHeight=-1;
393 Vec2i actPos(0,-1);
394 for (auto rIt : oldMap)
395 {
396 // first check for not filled row
397 if (rIt.first[0]!=actPos[1]+1)
398 {
399 if (actHeight==int(m_heightDefault)*20)
400 actPos[1]=rIt.first[0]-1;
401 else
402 {
403 if (actPos[1]>=actPos[0])
404 m_rowHeightMap[actPos]=actHeight;
405 actHeight=int(m_heightDefault)*20;
406 actPos=Vec2i(actPos[1]+1, rIt.first[0]-1);
407 }
408 }
409 if (rIt.second!=actHeight)
410 {
411 if (actPos[1]>=actPos[0])
412 m_rowHeightMap[actPos]=actHeight;
413 actPos[0]=rIt.first[0];
414 actHeight=rIt.second;
415 }
416 actPos[1]=rIt.first[1];
417 }
418 if (actPos[1]>=actPos[0])
419 m_rowHeightMap[actPos]=actHeight;
420 }
421 //! returns the cell position
getPosition(Vec2i const & cell) const422 Vec2f getPosition(Vec2i const &cell) const
423 {
424 float c=0;
425 int numWidth=int(m_widthCols.size());
426 for (int i=0; i<cell[0]; ++i)
427 {
428 if (i>=numWidth)
429 {
430 c+=float(i+1-numWidth)*m_widthDefault;
431 break;
432 }
433 int w=m_widthCols[size_t(i)];
434 if (w < 0)
435 c+=m_widthDefault;
436 else
437 c+=float(w)/20.f;
438 }
439 int r=0, prevR=0;
440 for (auto it : m_rowHeightMap)
441 {
442 int maxR=std::min(it.first[1],cell[1]-1);
443 if (prevR<it.first[0])
444 {
445 r+=(maxR-prevR)*int(m_heightDefault)*20;
446 prevR=maxR;
447 }
448 if (maxR<it.first[0])
449 break;
450 r+=(maxR+1-it.first[0])*it.second;
451 prevR=maxR;
452 }
453 if (prevR<cell[1]) r+=(cell[1]-prevR)*int(m_heightDefault)*20;
454 return Vec2f(c,float(r/20));
455 }
456 //! the spreadsheet id
457 int m_id;
458 /** the number of columns */
459 int m_numCols;
460
461 /** the map Vec2i(min row, max row) to size in TWIP */
462 std::map<Vec2i,int> m_rowHeightMap;
463 /** the default row size in point */
464 float m_heightDefault;
465 /** the column size in TWIP */
466 std::vector<int> m_widthCols;
467 /** the default width size in point */
468 float m_widthDefault;
469 /** a map cell to not empty cells */
470 std::map<Vec2i, Cell> m_positionToCellMap;
471 /** a dummy cell */
472 mutable Cell m_dummyCell;
473 };
474
475 //! the state of QuattroSpreadsheet
476 struct State
477 {
478 //! constructor
StateQuattroSpreadsheetInternal::State479 explicit State(QuattroFormulaManager::CellReferenceFunction const &readCellReference)
480 : m_version(-1)
481 , m_maxDimension(0,0,0)
482 , m_actSheet(-1)
483 , m_stylesList()
484 , m_formulaManager(readCellReference, 1)
485 , m_spreadsheetMap()
486 , m_idToSheetNameMap()
487 , m_idToUserFormatMap()
488 {
489 }
490 //! returns the ith real spreadsheet
getSheetQuattroSpreadsheetInternal::State491 std::shared_ptr<Spreadsheet> getSheet(int id, libwps_tools_win::Font::Type fontType)
492 {
493 auto it=m_spreadsheetMap.find(id);
494 if (it!=m_spreadsheetMap.end())
495 return it->second;
496 std::shared_ptr<Spreadsheet> sheet(new Spreadsheet(id, fontType));
497 sheet->setColumnWidth(m_maxDimension[0]);
498 if (id<0 || id>m_maxDimension[2])
499 {
500 WPS_DEBUG_MSG(("QuattroSpreadsheetInternal::State::getSheet: find unexpected id=%d\n", id));
501 if (id<0 || id>255) // too small or too big, return dummy spreadsheet
502 return sheet;
503 }
504 m_spreadsheetMap[id]=sheet;
505 return sheet;
506 }
507 //! returns the ith spreadsheet
getSheetNameQuattroSpreadsheetInternal::State508 librevenge::RVNGString getSheetName(int id) const
509 {
510 auto it = m_idToSheetNameMap.find(id);
511 if (it!=m_idToSheetNameMap.end() && !it->second.empty())
512 return it->second;
513 librevenge::RVNGString name;
514 name.sprintf("Sheet%d", id+1);
515 return name;
516 }
517 //! the file version
518 int m_version;
519 //! the maximum col, row, sheet
520 WPSVec3i m_maxDimension;
521 //! the actual sheet
522 int m_actSheet;
523 //! the list of styles
524 std::vector<Style> m_stylesList;
525 //! the formula manager
526 QuattroFormulaManager m_formulaManager;
527
528 //! the map of spreadsheet
529 std::map<int, std::shared_ptr<Spreadsheet> > m_spreadsheetMap;
530 //! the map id to sheet's name
531 std::map<int, librevenge::RVNGString> m_idToSheetNameMap;
532 //! map id to user format string
533 std::map<int, librevenge::RVNGString> m_idToUserFormatMap;
534 };
535
536 }
537
538 // constructor, destructor
QuattroSpreadsheet(QuattroParser & parser)539 QuattroSpreadsheet::QuattroSpreadsheet(QuattroParser &parser)
540 : m_listener()
541 , m_mainParser(parser)
542 , m_state()
543 {
544 m_state.reset(new QuattroSpreadsheetInternal::State(getReadCellReferenceFunction()));
545 }
546
~QuattroSpreadsheet()547 QuattroSpreadsheet::~QuattroSpreadsheet()
548 {
549 }
550
cleanState()551 void QuattroSpreadsheet::cleanState()
552 {
553 m_state.reset(new QuattroSpreadsheetInternal::State(getReadCellReferenceFunction()));
554 }
555
updateState()556 void QuattroSpreadsheet::updateState()
557 {
558 }
559
version() const560 int QuattroSpreadsheet::version() const
561 {
562 if (m_state->m_version<0)
563 m_state->m_version=m_mainParser.version();
564 return m_state->m_version;
565 }
566
getReadCellReferenceFunction()567 QuattroFormulaManager::CellReferenceFunction QuattroSpreadsheet::getReadCellReferenceFunction()
568 {
569 return [this](std::shared_ptr<WPSStream> const &stream, long endPos,
570 QuattroFormulaInternal::CellReference &ref,
571 Vec2i const &pos, int sheetId)
572 {
573 return this->readCellReference(stream, endPos, ref, pos, sheetId);
574 };
575 }
576
getNumSpreadsheets() const577 int QuattroSpreadsheet::getNumSpreadsheets() const
578 {
579 if (m_state->m_spreadsheetMap.empty())
580 return m_state->m_maxDimension[2]+1;
581 auto it=m_state->m_spreadsheetMap.end();
582 --it;
583 return std::max(it->first,m_state->m_maxDimension[2])+1;
584 }
585
getSheetName(int id) const586 librevenge::RVNGString QuattroSpreadsheet::getSheetName(int id) const
587 {
588 return m_state->getSheetName(id);
589 }
590
getPosition(int sheetId,Vec2i const & cell) const591 Vec2f QuattroSpreadsheet::getPosition(int sheetId, Vec2i const &cell) const
592 {
593 auto it=m_state->m_spreadsheetMap.find(sheetId);
594 if (it==m_state->m_spreadsheetMap.end() || !it->second)
595 {
596 WPS_DEBUG_MSG(("QuattroSpreadsheet::getPosition: can not find the sheet %d\n", sheetId));
597 return Vec2f(float(cell[0]*50), float(cell[1]*13));
598 }
599 return it->second->getPosition(cell);
600 }
601
addDLLIdName(int id,librevenge::RVNGString const & name,bool func1)602 void QuattroSpreadsheet::addDLLIdName(int id, librevenge::RVNGString const &name, bool func1)
603 {
604 m_state->m_formulaManager.addDLLIdName(id, name, func1);
605 }
606
addUserFormat(int id,librevenge::RVNGString const & name)607 void QuattroSpreadsheet::addUserFormat(int id, librevenge::RVNGString const &name)
608 {
609 if (name.empty())
610 {
611 WPS_DEBUG_MSG(("QuattroSpreadsheet::addUserFormat: called with empty name for id=%d\n", id));
612 return;
613 }
614 if (m_state->m_idToUserFormatMap.find(id)!=m_state->m_idToUserFormatMap.end())
615 {
616 WPS_DEBUG_MSG(("QuattroSpreadsheet::addUserFormat: called with dupplicated id=%d\n", id));
617 }
618 else
619 m_state->m_idToUserFormatMap[id]=name;
620 }
621
622 ////////////////////////////////////////////////////////////
623 // low level
624
625 ////////////////////////////////////////////////////////////
626 // parse sheet data
627 ////////////////////////////////////////////////////////////
readCell(std::shared_ptr<WPSStream> const & stream)628 bool QuattroSpreadsheet::readCell(std::shared_ptr<WPSStream> const &stream)
629 {
630 RVNGInputStreamPtr input = stream->m_input;
631 libwps::DebugFile &ascFile=stream->m_ascii;
632 libwps::DebugStream f;
633
634 long pos = input->tell();
635 auto type = long(libwps::readU16(input)&0x7fff);
636 if ((type < 0xc || type > 0x10) && (type!=0x33))
637 {
638 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCell: not a cell property\n"));
639 return false;
640 }
641 long sz = libwps::readU16(input);
642 long endPos = pos+4+sz;
643
644 if (sz < 5)
645 {
646 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCell: cell def is too short\n"));
647 return false;
648 }
649 int cellPos[2];
650 cellPos[0]=int(libwps::readU8(input));
651 auto sheetId=int(libwps::readU8(input));
652 cellPos[1]=int(libwps::read16(input));
653 if (cellPos[1] < 0)
654 {
655 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCell: cell pos is bad\n"));
656 return false;
657 }
658 if (sheetId)
659 f << "sheet[id]=" << sheetId << ",";
660
661 auto defFontType=m_mainParser.getDefaultFontType();
662 auto sheet = m_state->getSheet(sheetId, defFontType);
663 auto &cell=sheet->getCell(Vec2i(cellPos[0],cellPos[1]), defFontType);
664 auto format=int(libwps::readU16(input));
665 int id=format>>3;
666 // format&7: reserved
667 if (id<0 || id>int(m_state->m_stylesList.size()))
668 {
669 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCell: can not find cell format\n"));
670 f << "###Ce" << id << ",";
671 }
672 else if (id)
673 {
674 auto const &style=m_state->m_stylesList[size_t(id-1)];
675 if (type!=0x33)
676 {
677 cell.m_styleId=id-1;
678 cell.m_fileFormat=style.m_fileFormat;
679 cell.m_fontType=style.m_fontType;
680 static_cast<WPSCellFormat &>(cell)=style;
681 cell.m_alignAcrossColumn=style.m_alignAcrossColumn;
682 }
683 f << "Ce" << id-1 << ",";
684 }
685
686 long dataPos = input->tell();
687 auto dataSz = int(endPos-dataPos);
688
689 bool ok = true;
690 switch (type)
691 {
692 case 12:
693 {
694 if (dataSz == 0)
695 {
696 cell.m_content.m_contentType=WKSContentListener::CellContent::C_NONE;
697 break;
698 }
699 ok = false;
700 break;
701 }
702 case 13:
703 {
704 if (dataSz == 2)
705 {
706 cell.m_content.m_contentType=WKSContentListener::CellContent::C_NUMBER;
707 cell.m_content.setValue(libwps::read16(input));
708 break;
709 }
710 ok = false;
711 break;
712 }
713 case 14:
714 {
715 double val;
716 bool isNaN;
717 if (dataSz == 8 && libwps::readDouble8(input, val, isNaN))
718 {
719 cell.m_content.m_contentType=WKSContentListener::CellContent::C_NUMBER;
720 cell.m_content.setValue(val);
721 break;
722 }
723 ok = false;
724 break;
725 }
726 case 15:
727 cell.m_content.m_contentType=WKSContentListener::CellContent::C_TEXT;
728 WPS_FALLTHROUGH;
729 case 0x33: // formula res
730 {
731 long begText=input->tell()+1;
732 std::string s("");
733 // align + c string
734 auto align=char(libwps::readU8(input));
735 if (align=='\'') cell.setHAlignment(WPSCellFormat::HALIGN_DEFAULT);
736 else if (align=='^') cell.setHAlignment(WPSCellFormat::HALIGN_CENTER);
737 else if (align=='\"') cell.setHAlignment(WPSCellFormat::HALIGN_RIGHT);
738 else if (align=='\\') f << "repeat,"; // USEME
739 else if (align==0x7c) f << "break,"; // FIXME remove "::" in text
740 else if (align) f << "#align=" << int(align) << ",";
741
742 librevenge::RVNGString text("");
743 if (!m_mainParser.readCString(stream,text,dataSz-1))
744 f << "##sSz,";
745 else
746 {
747 if (endPos!=input->tell() && endPos!=input->tell()+1)
748 {
749 f << "#extra,";
750 ascFile.addDelimiter(input->tell(), '|');
751 }
752 cell.m_stream=stream;
753 cell.m_content.m_textEntry.setBegin(begText);
754 cell.m_content.m_textEntry.setEnd(input->tell()-1);
755 if (!text.empty())
756 f << text.cstr() << ",";
757 }
758 break;
759 }
760 case 16:
761 {
762 double val;
763 bool isNaN;
764 if (dataSz >= 10 && libwps::readDouble8(input, val, isNaN))
765 {
766 cell.m_content.m_contentType=WKSContentListener::CellContent::C_FORMULA;
767 cell.m_content.setValue(val);
768 auto state=int(libwps::readU16(input));
769 if (state)
770 {
771 f << "state[";
772 if (state&0x8) f << "constant,";
773 if (state==0x10) f << "volatile,";
774 if (state==0x100) f << "inArray,";
775 if (state==0x200) f << "useDLL,";
776 if (state&0xfcf3) f << "#state=" << std::hex << (state&0xfcf3) << std::dec << ",";
777 f << "],";
778 }
779 std::string error;
780 if (!m_state->m_formulaManager.readFormula(stream, endPos, cell.position(), sheetId, cell.m_content.m_formula, error))
781 {
782 cell.m_content.m_contentType=WKSContentListener::CellContent::C_NUMBER;
783 ascFile.addDelimiter(input->tell()-1, '#');
784 }
785 if (error.length()) f << error;
786 break;
787 }
788 ok = false;
789 break;
790 }
791 default:
792 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCell: unknown type=%ld\n", type));
793 ok = false;
794 break;
795 }
796 if (!ok) ascFile.addDelimiter(dataPos, '#');
797
798 input->seek(pos+sz, librevenge::RVNG_SEEK_SET);
799
800 std::string extra=f.str();
801 f.str("");
802 f << cell << "," << extra;
803
804 ascFile.addPos(pos);
805 ascFile.addNote(f.str().c_str());
806
807 return true;
808 }
809
readCellStyle(std::shared_ptr<WPSStream> const & stream)810 bool QuattroSpreadsheet::readCellStyle(std::shared_ptr<WPSStream> const &stream)
811 {
812 RVNGInputStreamPtr input = stream->m_input;
813 libwps::DebugFile &ascFile=stream->m_ascii;
814 libwps::DebugStream f;
815 long pos = input->tell();
816 auto type = long(libwps::readU16(input)&0x7fff);
817 if (type != 0xce)
818 {
819 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellStyle: not a style zone\n"));
820 return false;
821 }
822 long sz = libwps::readU16(input);
823 f << "[Ce" << m_state->m_stylesList.size() << "],";
824 QuattroSpreadsheetInternal::Style style(m_mainParser.getDefaultFontType());
825 if (sz<8)
826 {
827 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellStyle: size seems bad\n"));
828 f << "###";
829 ascFile.addPos(pos);
830 ascFile.addNote(f.str().c_str());
831 m_state->m_stylesList.push_back(style);
832 return true;
833 }
834 style.m_fileFormat=int(libwps::readU8(input));
835 if (style.m_fileFormat!=0xFF)
836 f << "form=" << std::hex << style.m_fileFormat << std::dec << ",";
837 auto flag=int(libwps::readU8(input));
838 switch (flag&7)
839 {
840 case 1:
841 style.setHAlignment(WPSCellFormat::HALIGN_LEFT);
842 f << "left,";
843 break;
844 case 2:
845 style.setHAlignment(WPSCellFormat::HALIGN_CENTER);
846 f << "center,";
847 break;
848 case 3:
849 style.setHAlignment(WPSCellFormat::HALIGN_RIGHT);
850 f << "right,";
851 break;
852 case 4:
853 style.setHAlignment(WPSCellFormat::HALIGN_FULL);
854 f << "block,";
855 break;
856 case 6:
857 {
858 style.setHAlignment(WPSCellFormat::HALIGN_CENTER);
859 style.m_alignAcrossColumn=true;
860 f << "center[across],";
861 break;
862 }
863 default:
864 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellStyle: find unexpected alignment\n"));
865 f << "###align=" << (flag&7) << ",";
866 break;
867 case 0: // standart
868 break;
869 }
870 if (sz>=12)
871 {
872 switch ((flag>>3)&3)
873 {
874 case 0: // default
875 style.setVAlignment(WPSCellFormat::VALIGN_BOTTOM);
876 break;
877 case 1:
878 style.setVAlignment(WPSCellFormat::VALIGN_CENTER);
879 f << "vAlign=center,";
880 break;
881 case 2:
882 style.setVAlignment(WPSCellFormat::VALIGN_TOP);
883 f << "vAlign=top,";
884 break;
885 default:
886 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellStyle: find unexpected alignment\n"));
887 f << "###valign=3,";
888 break;
889 }
890 if (flag&0x20)
891 {
892 style.setTextRotation(270);
893 f << "top[down],";
894 }
895 if (flag&0x80)
896 {
897 style.setWrapping(WPSCellFormat::WRAP_WRAP);
898 f << "wrap,";
899 }
900 flag &= 0x40;
901 }
902 else
903 {
904 switch ((flag>>6)&3) // checkme, maybe diferent in wb2, ie. I find difference with qprostyle.cpp
905 {
906 default:
907 case 0: // standart
908 break;
909 case 1:
910 f << "label[only],";
911 break;
912 case 2:
913 f << "date[only],";
914 break;
915 case 3:
916 f << "##input=3,";
917 break;
918 }
919 flag&=0x38;
920 }
921 if (flag)
922 f << "#fl=" << std::hex << flag << std::dec << ",";
923 auto val=int(libwps::readU8(input));
924 int color[3]= {val>>4, val&0xf, 0}; // col2: shade, col1: shade, textcolor
925 val=int(libwps::readU8(input));
926 color[2]=val>>4;
927 int blend=(val&0x7);
928 WPSColor colors[]= {WPSColor::white(), WPSColor::black(), WPSColor::black()};
929 for (int i=0; i<3; ++i)
930 {
931 int const expected[]= {0,3,3};
932 if (color[i]==expected[i]) continue;
933 if (m_mainParser.getColor(color[i], colors[i]))
934 f << "color" << i << "=" << colors[i] << ",";
935 else
936 f << "##color" << i << "=" << color[i] << ",";
937 }
938 if (blend==7)
939 f << "###blend=7,";
940 else
941 {
942 int const percent[]= {0,6,3,1,2,5,4};
943 float fPercent=float(percent[blend])/6.f;
944 if (blend)
945 f << "blend=" << 100.f *fPercent << "%,";
946 style.setBackgroundColor(WPSColor::barycenter(fPercent,colors[1],1.f-fPercent,colors[0]));
947 // percent col2 + (1-percent) col1
948 }
949 if (val&8) f << "fl[8],";
950 auto fId=int(libwps::readU8(input));
951 WPSFont font;
952 if (fId)
953 {
954 if (!m_mainParser.getFont(fId-1, font, style.m_fontType))
955 f << "###";
956 f << "F" << fId-1 << ",";
957 }
958 font.m_color=colors[2];
959 style.setFont(font);
960 auto bFlags=int(libwps::readU8(input));
961 // 80:has[textColor], 40:has[protect], 20:has[borders], 10:has[fonts], 8:has[color], 4:[hasAlign], 2:[hasForm], 1:[hasProtect+0x40?]...
962 val=int(libwps::readU8(input));
963 val &= 0x41;
964 if (val==0x41)
965 f << "protect=no,";
966 else if (val) f << "fl2=" << std::hex << val << std::dec << ",";
967 val=int(libwps::readU8(input)); // USEME
968 if (val) f << "style[id]=" << val << ",";
969 WPSColor borderColors[4];
970 for (auto &c : borderColors) c=WPSColor::black();
971 if (sz>=12) // sz=12, pre-wb2, wb2?
972 {
973 f << "borders[color]=[";
974 for (int i=0; i<2; ++i)
975 {
976 val=int(libwps::readU8(input));
977 for (int j=0; j<2; ++j)
978 {
979 int c=j==1 ? (val>>4) : (val&0xf);
980 if (!m_mainParser.getColor(c, borderColors[2*i+j]))
981 f << "##color=" << c << ",";
982 else if (borderColors[2*i+j].isBlack())
983 f << "_,";
984 else
985 f << borderColors[2*i+j] << ",";
986 }
987 }
988 f << "],";
989 val=int(libwps::readU16(input));
990 switch (val&3)
991 {
992 default:
993 case 0: // standart
994 break;
995 case 1:
996 f << "label[only],";
997 break;
998 case 2:
999 f << "date[only],";
1000 break;
1001 case 3:
1002 f << "##input=3,";
1003 break;
1004 }
1005 if (val&4) f << "use[lineColor],"; // or page color
1006 val&=0xfff8;
1007 if (val) f << "fl3=" << val << ",";
1008 }
1009 if (bFlags)
1010 {
1011 f << "borders=[";
1012 for (int i=0, depl=0; i<4; ++i, depl+=2) // BRTL
1013 {
1014 int bType=(bFlags>>depl)&3;
1015 if (!bType) continue;
1016 char const *wh[]= {"L","T","R","B"};
1017 WPSBorder border;
1018 switch (bType)
1019 {
1020 case 1: // normal
1021 f << wh[i] << ",";
1022 break;
1023 case 2: // double
1024 border.m_type=WPSBorder::Double;
1025 f << wh[i] << "=double,";
1026 break;
1027 case 3: // width*2
1028 border.m_width=2;
1029 f << wh[i] << "=w2,";
1030 break;
1031 default: // impossible
1032 break;
1033 }
1034 border.m_color=borderColors[i];
1035 int const which[]= {WPSBorder::LeftBit, WPSBorder::TopBit, WPSBorder::RightBit, WPSBorder::BottomBit};
1036 style.setBorders(which[i], border);
1037 }
1038 f << "],";
1039 }
1040 m_state->m_stylesList.push_back(style);
1041 ascFile.addPos(pos);
1042 ascFile.addNote(f.str().c_str());
1043 return true;
1044 }
1045
readSheetSize(std::shared_ptr<WPSStream> const & stream)1046 bool QuattroSpreadsheet::readSheetSize(std::shared_ptr<WPSStream> const &stream)
1047 {
1048 RVNGInputStreamPtr input = stream->m_input;
1049 libwps::DebugFile &ascFile=stream->m_ascii;
1050 libwps::DebugStream f;
1051 long pos = input->tell();
1052 auto type = long(libwps::readU16(input)&0x7fff);
1053 if (type != 0x6)
1054 {
1055 WPS_DEBUG_MSG(("QuattroSpreadsheet::readSheetSize: not a sheet zone\n"));
1056 return false;
1057 }
1058 long sz = libwps::readU16(input);
1059 if (sz < 8)
1060 {
1061 WPS_DEBUG_MSG(("QuattroSpreadsheet::readSheetSize: block is too short\n"));
1062 return false;
1063 }
1064 bool ok=true;
1065 for (int i = 0; i < 2; i++) // min can be invalid if the file is an extract file
1066 {
1067 f << (i==0 ? "min": "max") << "=[";
1068 int nCol = int(libwps::readU8(input))+1;
1069 f << "col=" << nCol << ",";
1070 auto nSheet = int(libwps::readU8(input));
1071 int nRow = libwps::read16(input);
1072 f << "row=" << nRow << ",";
1073 if (nSheet)
1074 f << "sheet=" << nSheet << ",";
1075 f << "],";
1076 if (i==0)
1077 continue;
1078 m_state->m_maxDimension=WPSVec3i(nCol,nRow,nSheet);
1079 if (nRow<0)
1080 ok=(nRow==-1 && nCol==1); // empty spreadsheet
1081 }
1082 ascFile.addPos(pos);
1083 ascFile.addNote(f.str().c_str());
1084 return ok;
1085 }
1086
readColumnRowDefaultSize(std::shared_ptr<WPSStream> const & stream)1087 bool QuattroSpreadsheet::readColumnRowDefaultSize(std::shared_ptr<WPSStream> const &stream)
1088 {
1089 RVNGInputStreamPtr input = stream->m_input;
1090 libwps::DebugFile &ascFile=stream->m_ascii;
1091 libwps::DebugStream f;
1092 long pos = input->tell();
1093 auto type = long(libwps::readU16(input)&0x7fff);
1094 if (type < 0xd2 || type > 0xd5)
1095 {
1096 WPS_DEBUG_MSG(("QuattroSpreadsheet::readColumnRowDefaultSize: not a column size zone\n"));
1097 return false;
1098 }
1099 long sz = libwps::readU16(input);
1100 if (sz != 2)
1101 {
1102 WPS_DEBUG_MSG(("QuattroSpreadsheet::readColumnRowDefaultSize: block is too short\n"));
1103 return false;
1104 }
1105 int val=int(libwps::readU16(input));
1106 if (val&0x8000)
1107 {
1108 f << "user,";
1109 val &= 0x7fff;
1110 }
1111 f << float(val)/20.f << ",";
1112 if (type==0xd2 || type==0xd4)
1113 {
1114 auto defFontType=m_mainParser.getDefaultFontType();
1115 auto sheet=m_state->getSheet(m_state->m_actSheet, defFontType);
1116 if (type==0xd2)
1117 sheet->m_heightDefault=float(val)/20.f;
1118 else
1119 sheet->m_widthDefault=float(val)/20.f;
1120 }
1121
1122 ascFile.addPos(pos);
1123 ascFile.addNote(f.str().c_str());
1124 return true;
1125 }
1126
readColumnSize(std::shared_ptr<WPSStream> const & stream)1127 bool QuattroSpreadsheet::readColumnSize(std::shared_ptr<WPSStream> const &stream)
1128 {
1129 RVNGInputStreamPtr input = stream->m_input;
1130 libwps::DebugFile &ascFile=stream->m_ascii;
1131 libwps::DebugStream f;
1132 long pos = input->tell();
1133 auto type = long(libwps::readU16(input)&0x7fff);
1134 if (type != 0xd8 && type != 0xd9)
1135 {
1136 WPS_DEBUG_MSG(("QuattroSpreadsheet::readColumnSize: not a column size zone\n"));
1137 return false;
1138 }
1139 long sz = libwps::readU16(input);
1140 if (sz < 4)
1141 {
1142 WPS_DEBUG_MSG(("QuattroSpreadsheet::readColumnSize: block is too short\n"));
1143 return false;
1144 }
1145
1146 int col = libwps::read16(input);
1147 int width = libwps::readU16(input);
1148
1149 auto defFontType=m_mainParser.getDefaultFontType();
1150 auto sheet=m_state->getSheet(m_state->m_actSheet, defFontType);
1151 bool ok = col >= 0 && col < sheet->m_numCols+10;
1152 f << "Col" << col << ":";
1153 if (width&0x8000)
1154 {
1155 f << "user,";
1156 width &= 0x7fff;
1157 }
1158 f << "width=" << float(width)/72.f << ",";
1159 if (ok && type==0xd8)
1160 {
1161 if (col >= sheet->m_numCols)
1162 {
1163 static bool first = true;
1164 if (first)
1165 {
1166 first = false;
1167 WPS_DEBUG_MSG(("QuattroSpreadsheet::readColumnSize: I must increase the number of columns\n"));
1168 }
1169 f << "#col[inc],";
1170 }
1171 sheet->setColumnWidth(col, width);
1172 }
1173 else if (col>256 && type==0xd8)
1174 f << "###,";
1175 ascFile.addPos(pos);
1176 ascFile.addNote(f.str().c_str());
1177
1178 return true;
1179 }
1180
readRowSize(std::shared_ptr<WPSStream> const & stream)1181 bool QuattroSpreadsheet::readRowSize(std::shared_ptr<WPSStream> const &stream)
1182 {
1183 RVNGInputStreamPtr input = stream->m_input;
1184 libwps::DebugFile &ascFile=stream->m_ascii;
1185 libwps::DebugStream f;
1186 long pos = input->tell();
1187 auto type = long(libwps::readU16(input)&0x7fff);
1188 if (type != 0xd6 && type != 0xd7)
1189 {
1190 WPS_DEBUG_MSG(("QuattroSpreadsheet::readRowSize: not a row size zone\n"));
1191 return false;
1192 }
1193 long sz = libwps::readU16(input);
1194 if (sz != 4)
1195 {
1196 WPS_DEBUG_MSG(("QuattroSpreadsheet::readRowSize: block is too short\n"));
1197 return false;
1198 }
1199
1200 int row = libwps::read16(input);
1201 int height = libwps::readU16(input);
1202
1203 f << "Row" << row << ",";
1204 if (height&0x8000) // maybe set by hand?
1205 {
1206 f << "user,";
1207 height &= 0x7fff;
1208 }
1209 f << "h=" << float(height)/20.f << ",";
1210 if (type==0xd6)
1211 {
1212 if (row>=0 && m_state->m_actSheet>=0)
1213 {
1214 auto defFontType=m_mainParser.getDefaultFontType();
1215 m_state->getSheet(m_state->m_actSheet, defFontType)->setRowHeight(row, height);
1216 }
1217 else
1218 {
1219 WPS_DEBUG_MSG(("QuattroSpreadsheet::readRowSize: can not find the current sheet\n"));
1220 f << "###";
1221 }
1222 }
1223
1224 ascFile.addPos(pos);
1225 ascFile.addNote(f.str().c_str());
1226
1227 return true;
1228 }
1229
readRowRangeSize(std::shared_ptr<WPSStream> const & stream)1230 bool QuattroSpreadsheet::readRowRangeSize(std::shared_ptr<WPSStream> const &stream)
1231 {
1232 RVNGInputStreamPtr input = stream->m_input;
1233 libwps::DebugFile &ascFile=stream->m_ascii;
1234 libwps::DebugStream f;
1235 long pos = input->tell();
1236 auto type = long(libwps::readU16(input)&0x7fff);
1237 if (type != 0x105 && type != 0x106)
1238 {
1239 WPS_DEBUG_MSG(("QuattroSpreadsheet::readRowRangeSize: not a row size zone\n"));
1240 return false;
1241 }
1242 long sz = libwps::readU16(input);
1243 if (sz != 6)
1244 {
1245 WPS_DEBUG_MSG(("QuattroSpreadsheet::readRowRangeSize: block is too short\n"));
1246 return false;
1247 }
1248
1249 int minRow = libwps::read16(input);
1250 int maxRow = libwps::read16(input);
1251 int height = libwps::readU16(input);
1252
1253 f << "Row" << minRow << "<->R" << maxRow << ",";
1254 if (height&0x8000) // maybe set by hand?
1255 {
1256 f << "user,";
1257 height &= 0x7fff;
1258 }
1259 f << "h=" << float(height)/20.f << ",";
1260 if (type==0x105)
1261 {
1262 if (minRow>=0 && minRow<=maxRow && m_state->m_actSheet>=0)
1263 {
1264 auto defFontType=m_mainParser.getDefaultFontType();
1265 m_state->getSheet(m_state->m_actSheet, defFontType)->setRowHeights(minRow, maxRow, height);
1266 }
1267 else
1268 {
1269 WPS_DEBUG_MSG(("QuattroSpreadsheet::readRowSize: can not find the current sheet\n"));
1270 f << "###";
1271 }
1272 }
1273
1274 ascFile.addPos(pos);
1275 ascFile.addNote(f.str().c_str());
1276
1277 return true;
1278 }
1279
1280 ////////////////////////////////////////////////////////////
1281 // general
1282 ////////////////////////////////////////////////////////////
readBeginEndSheet(std::shared_ptr<WPSStream> const & stream,int & sheetId)1283 bool QuattroSpreadsheet::readBeginEndSheet(std::shared_ptr<WPSStream> const &stream, int &sheetId)
1284 {
1285 RVNGInputStreamPtr input = stream->m_input;
1286 libwps::DebugFile &ascFile=stream->m_ascii;
1287 libwps::DebugStream f;
1288
1289 long pos = input->tell();
1290 auto type = long(libwps::readU16(input)&0x7fff);
1291 if (type != 0xca && type != 0xcb)
1292 {
1293 WPS_DEBUG_MSG(("QuattroSpreadsheet::readBeginEndSheet: not a zoneB type\n"));
1294 return false;
1295 }
1296 auto sz = long(libwps::readU16(input));
1297 if (sz != 1)
1298 {
1299 WPS_DEBUG_MSG(("QuattroSpreadsheet::readBeginEndSheet: size seems bad\n"));
1300 f << "###";
1301 ascFile.addPos(pos);
1302 ascFile.addNote(f.str().c_str());
1303 return true;
1304 }
1305 auto sheet=int(libwps::readU8(input));
1306 f << "sheet=" << sheet << ",";
1307 if (type==0xca)
1308 {
1309 if (m_state->m_actSheet>=0)
1310 {
1311 WPS_DEBUG_MSG(("QuattroSpreadsheet::readBeginEndSheet: oops, does not find the previous end\n"));
1312 f << "###";
1313 }
1314 sheetId=m_state->m_actSheet=sheet;
1315 }
1316 else
1317 {
1318 if (m_state->m_actSheet!=sheet)
1319 {
1320 WPS_DEBUG_MSG(("QuattroSpreadsheet::readBeginEndSheet: oops, end sheet id does not match with begin sheet id\n"));
1321 f << "###";
1322 }
1323 sheetId=m_state->m_actSheet=-1;
1324 }
1325 ascFile.addPos(pos);
1326 ascFile.addNote(f.str().c_str());
1327 return true;
1328 }
1329
readSheetName(std::shared_ptr<WPSStream> const & stream)1330 bool QuattroSpreadsheet::readSheetName(std::shared_ptr<WPSStream> const &stream)
1331 {
1332 RVNGInputStreamPtr input = stream->m_input;
1333 libwps::DebugFile &ascFile=stream->m_ascii;
1334 libwps::DebugStream f;
1335
1336 long pos = input->tell();
1337 auto type = long(libwps::readU16(input)&0x7fff);
1338 if (type != 0xcc)
1339 {
1340 WPS_DEBUG_MSG(("QuattroSpreadsheet::readSheetName: not a zoneB type\n"));
1341 return false;
1342 }
1343 auto sz = long(libwps::readU16(input));
1344 if (sz < 1)
1345 {
1346 WPS_DEBUG_MSG(("QuattroSpreadsheet::readSheetName: size seems bad\n"));
1347 f << "###";
1348 ascFile.addPos(pos);
1349 ascFile.addNote(f.str().c_str());
1350 return true;
1351 }
1352 librevenge::RVNGString name;
1353 if (!m_mainParser.readCString(stream,name,sz) || name.empty())
1354 f << "###";
1355 else
1356 {
1357 f << name.cstr() << ",";
1358 if (m_state->m_idToSheetNameMap.find(m_state->m_actSheet)!=m_state->m_idToSheetNameMap.end())
1359 {
1360 WPS_DEBUG_MSG(("QuattroSpreadsheet::readSheetName: id dupplicated\n"));
1361 f << "###id";
1362 }
1363 else
1364 m_state->m_idToSheetNameMap[m_state->m_actSheet]=name;
1365 }
1366 ascFile.addPos(pos);
1367 ascFile.addNote(f.str().c_str());
1368 return true;
1369 }
1370
readViewInfo(std::shared_ptr<WPSStream> const & stream)1371 bool QuattroSpreadsheet::readViewInfo(std::shared_ptr<WPSStream> const &stream)
1372 {
1373 RVNGInputStreamPtr input = stream->m_input;
1374 libwps::DebugFile &ascFile=stream->m_ascii;
1375 libwps::DebugStream f;
1376 long pos = input->tell();
1377 auto type = long(libwps::readU16(input)&0x7fff);
1378 if (type != 0x197 && type !=0x198)
1379 {
1380 WPS_DEBUG_MSG(("QuattroSpreadsheet::readViewInfo: not a sheet zone\n"));
1381 return false;
1382 }
1383 long sz = libwps::readU16(input);
1384 long endPos=pos+4+sz;
1385 if (sz < 21)
1386 {
1387 WPS_DEBUG_MSG(("QuattroSpreadsheet::readViewInfo: block is too short\n"));
1388 return false;
1389 }
1390 auto id=int(libwps::read8(input));
1391 f << "id=" << id << ",";
1392
1393 int val = libwps::readU16(input); // 0|1|3|f
1394 f << "show=[";
1395 if (val&1) f << "rowHeading,";
1396 if (val&2) f << "colHeading,";
1397 if (val&4) f << "horiGrid,";
1398 if (val&8) f << "vertGrid,";
1399 // val&0x10: reserved
1400 val &=0xfff0;
1401 if (val)
1402 f << "f0=" << std::hex << val << std::dec << ",";
1403 f << "],";
1404 f << "range=";
1405 for (int i=0; i<2; ++i)
1406 {
1407 f << "C" << int(libwps::readU8(input));
1408 f << "S" << int(libwps::readU8(input));
1409 f << "R" << int(libwps::readU16(input));
1410 f << (i==0 ? "<->" : ",");
1411 }
1412 val = libwps::readU16(input);
1413 switch (val)
1414 {
1415 case 0: // default
1416 break;
1417 case 1:
1418 f << "title[hori],";
1419 break;
1420 case 2:
1421 f << "title[verti],";
1422 break;
1423 case 3:
1424 f << "title[both],";
1425 break;
1426 default:
1427 f << "##title=" << val << ",";
1428 break;
1429 }
1430 f << "cell[TL]=C" << int(libwps::readU8(input));
1431 f << "S" << int(libwps::readU8(input));
1432 f << "R" << int(libwps::readU16(input)) << ",";
1433 f << "num[row]=" << int(libwps::readU16(input)) << ",";
1434 f << "num[col]=" << int(libwps::readU16(input)) << ",";
1435 if (input->tell()!=endPos)
1436 ascFile.addDelimiter(input->tell(),'|');
1437 ascFile.addPos(pos);
1438 ascFile.addNote(f.str().c_str());
1439 return true;
1440 }
1441
1442 ////////////////////////////////////////////////////////////
1443 // formula
1444 ////////////////////////////////////////////////////////////
readCell(std::shared_ptr<WPSStream> const & stream,Vec2i actPos,WKSContentListener::FormulaInstruction & instr,int sheetId,librevenge::RVNGString const & fName)1445 bool QuattroSpreadsheet::readCell
1446 (std::shared_ptr<WPSStream> const &stream, Vec2i actPos, WKSContentListener::FormulaInstruction &instr, int sheetId, librevenge::RVNGString const &fName)
1447 {
1448 RVNGInputStreamPtr input = stream->m_input;
1449 instr=WKSContentListener::FormulaInstruction();
1450 instr.m_type=WKSContentListener::FormulaInstruction::F_Cell;
1451 instr.m_fileName=fName;
1452 bool ok = true;
1453 int pos[3]; // col, sheet, fl|row
1454 bool relative[3] = { false, false, false};
1455 for (int d=0; d<2; ++d) pos[d]=int(libwps::readU8(input));
1456 pos[2]=int(libwps::readU16(input));
1457 if (pos[2]&0x8000)
1458 {
1459 pos[1] = int8_t(pos[1])+sheetId;
1460 relative[1] = true;
1461 }
1462 if (pos[2]&0x4000)
1463 {
1464 pos[0] = int8_t(pos[0])+actPos[0];
1465 relative[0] = true;
1466 }
1467 if (pos[2]&0x2000)
1468 {
1469 pos[2] = actPos[1]+(int16_t((pos[2]&0x1fff)<<3)>>3);
1470 relative[2] = true;
1471 }
1472 else
1473 pos[2] &= 0x1fff;
1474 if (pos[0] < 0 || pos[0] > 255 || pos[2] < 0)
1475 {
1476 if (ok)
1477 {
1478 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCell: can not read cell position\n"));
1479 }
1480 return false;
1481 }
1482 instr.m_position[0]=Vec2i(pos[0],pos[2]);
1483 instr.m_positionRelative[0]=Vec2b(relative[0],relative[2]);
1484 if (!fName.empty()) // external file, assume default name
1485 {
1486 librevenge::RVNGString name;
1487 name.sprintf("Sheet%d", pos[1]+1);
1488 instr.m_sheetName[0]=name;
1489 }
1490 else
1491 instr.m_sheetId[0]=pos[1];
1492 return ok;
1493 }
1494
readCellReference(std::shared_ptr<WPSStream> const & stream,long endPos,QuattroFormulaInternal::CellReference & ref,Vec2i const & cPos,int sheetId) const1495 bool QuattroSpreadsheet::readCellReference(std::shared_ptr<WPSStream> const &stream, long endPos,
1496 QuattroFormulaInternal::CellReference &ref,
1497 Vec2i const &cPos, int sheetId) const
1498 {
1499 ref.m_cells.clear();
1500 RVNGInputStreamPtr input = stream->m_input;
1501 long pos = input->tell();
1502 if (pos+4>endPos) return false;
1503 auto type=int(libwps::readU16(input));
1504 int cellType=type>>12;
1505 if (cellType>4) return false;
1506 if (cellType==4)
1507 {
1508 // type==4: +6 unused bit then a 233 data field (checkme)
1509 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellReference: find a cell collection 4\n"));
1510 return false;
1511 }
1512
1513 WKSContentListener::FormulaInstruction instr;
1514 if (cellType==3)
1515 {
1516 int dataSize=(type&0x3ff);
1517 if (pos+2+dataSize>endPos)
1518 {
1519 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellReference: can not read the cell collection data size\n"));
1520 return false;
1521 }
1522 if (type&0xc00) // check for deletion
1523 {
1524 input->seek(dataSize, librevenge::RVNG_SEEK_CUR);
1525 return true;
1526 }
1527 endPos=pos+2+dataSize;
1528 while (input->tell()<endPos)
1529 {
1530 QuattroFormulaInternal::CellReference cells;
1531 if (!readCellReference(stream, endPos, cells, cPos, sheetId))
1532 {
1533 WPS_DEBUG_MSG(("QuattroSpreadsheet::readCellReference: can not read a cell\n"));
1534 return false;
1535 }
1536 for (auto const &c : cells.m_cells) ref.addInstruction(c);
1537 }
1538 return true;
1539 }
1540 int const expectedSize[]= {4,8,2};
1541 if (pos+2+expectedSize[cellType]>endPos) return false;
1542 if (type&0xc00)
1543 {
1544 input->seek(expectedSize[cellType], librevenge::RVNG_SEEK_CUR);
1545 return true;
1546 }
1547 librevenge::RVNGString fileName;
1548 if ((type&0x3ff))
1549 {
1550 if (!m_mainParser.getExternalFileName((type&0x3ff), fileName))
1551 return false;
1552 }
1553 if (cellType==0 && pos+6<=endPos)
1554 {
1555 if (!readCell(stream, cPos, instr, sheetId, fileName))
1556 return false;
1557 ref.addInstruction(instr);
1558 return true;
1559 }
1560 else if (cellType==2)
1561 {
1562 auto fId=int(libwps::readU16(input));
1563 librevenge::RVNGString text;
1564 return m_mainParser.getField(fId, text, ref, fileName);
1565 }
1566 else if (cellType==1 && pos+10<=endPos)
1567 {
1568 WKSContentListener::FormulaInstruction cell2;
1569 if (!readCell(stream, cPos, instr, sheetId, fileName) ||
1570 !readCell(stream, cPos, cell2, sheetId, fileName))
1571 return false;
1572 instr.m_type=WKSContentListener::FormulaInstruction::F_CellList;
1573 instr.m_position[1]=cell2.m_position[0];
1574 instr.m_positionRelative[1]=cell2.m_positionRelative[0];
1575 instr.m_sheetId[1]=cell2.m_sheetId[0];
1576 instr.m_sheetName[1]=cell2.m_sheetName[0];
1577 ref.addInstruction(instr);
1578 return true;
1579 }
1580 return false;
1581 }
1582
1583 ////////////////////////////////////////////////////////////
1584 // send data
1585 ////////////////////////////////////////////////////////////
sendSpreadsheet(int sId,std::vector<Vec2i> const & listGraphicCells)1586 void QuattroSpreadsheet::sendSpreadsheet(int sId, std::vector<Vec2i> const &listGraphicCells)
1587 {
1588 if (!m_listener)
1589 {
1590 WPS_DEBUG_MSG(("QuattroSpreadsheet::sendSpreadsheet: I can not find the listener\n"));
1591 return;
1592 }
1593 auto defFontType=m_mainParser.getDefaultFontType();
1594 auto sheet = m_state->getSheet(sId, defFontType);
1595 for (auto c: listGraphicCells)
1596 sheet->getCell(c, defFontType).m_hasGraphic=true;
1597 m_listener->openSheet(sheet->getWidths(), m_state->getSheetName(sId));
1598 m_mainParser.sendPageGraphics(sId);
1599 sheet->compressRowHeights();
1600 auto it = sheet->m_positionToCellMap.begin();
1601 int prevRow = -1;
1602 while (it != sheet->m_positionToCellMap.end())
1603 {
1604 int row=it->first[1];
1605 auto const &cell=(it++)->second;
1606 if (row>prevRow+1)
1607 {
1608 while (row > prevRow+1)
1609 {
1610 if (prevRow != -1) m_listener->closeSheetRow();
1611 int numRepeat;
1612 float h=sheet->getRowHeight(prevRow+1, numRepeat);
1613 if (row<prevRow+1+numRepeat)
1614 numRepeat=row-1-prevRow;
1615 m_listener->openSheetRow(WPSRowFormat(h), numRepeat);
1616 prevRow+=numRepeat;
1617 }
1618 }
1619 if (row!=prevRow)
1620 {
1621 if (prevRow != -1) m_listener->closeSheetRow();
1622 m_listener->openSheetRow(WPSRowFormat(sheet->getRowHeight(++prevRow)));
1623 }
1624 if (cell.m_alignAcrossColumn) // we must look for "merged" cell
1625 {
1626 auto firstCol=cell.position()[0], lastCol=firstCol+1;
1627 auto fIt=it;
1628 while (fIt!=sheet->m_positionToCellMap.end() && fIt->first==Vec2i(lastCol,row))
1629 {
1630 auto const &nextCell=fIt->second;
1631 if (nextCell.m_styleId!=cell.m_styleId)
1632 break;
1633 auto const &nextContent=nextCell.m_content;
1634 if ((nextContent.m_contentType== nextContent.C_NUMBER && !nextContent.isValueSet()) ||
1635 nextContent.empty())
1636 {
1637 ++fIt;
1638 ++lastCol;
1639 }
1640 else
1641 break;
1642 }
1643 if (lastCol!=firstCol+1)
1644 {
1645 const_cast<QuattroSpreadsheetInternal::Cell &>(cell).setNumSpannedCells(Vec2i(lastCol-firstCol,1));
1646 it=fIt;
1647 }
1648 }
1649 sendCellContent(cell, sId);
1650 }
1651 if (prevRow!=-1) m_listener->closeSheetRow();
1652 m_listener->closeSheet();
1653 }
1654
1655 namespace libwps
1656 {
1657 // basic function which probably does not exist on Windows, so rewrite it
strncasecmp(char const * s1,char const * s2,size_t n)1658 static int strncasecmp(char const *s1, char const *s2, size_t n)
1659 {
1660 if (n == 0)
1661 return 0;
1662
1663 while (n-- != 0 && std::tolower(*s1) == std::tolower(*s2))
1664 {
1665 if (n == 0 || *s1 == '\0' || *s2 == '\0')
1666 break;
1667 s1++;
1668 s2++;
1669 }
1670
1671 return std::tolower(*s1) - std::tolower(*s2);
1672 }
1673 }
1674
updateCellWithUserFormat(QuattroSpreadsheetInternal::Cell & cell,librevenge::RVNGString const & format)1675 void QuattroSpreadsheet::updateCellWithUserFormat(QuattroSpreadsheetInternal::Cell &cell, librevenge::RVNGString const &format)
1676 {
1677 if (format.empty())
1678 {
1679 WPS_DEBUG_MSG(("QuattroSpreadsheet::updateCellWithUserFormat: called with empty format\n"));
1680 return;
1681 }
1682 char const *ptr=format.cstr();
1683 auto c=char(std::toupper(*(ptr++)));
1684 // first N/n: numeric, T/t: date
1685 // all: *: fill with last data, 'string': strings, \x: x
1686 if (c=='N')
1687 {
1688 bool scientific=false;
1689 bool hasThousand=false;
1690 bool percent=false;
1691 int digits=-1;
1692 bool end=false;
1693 // numeric 0:always a digits, 9: potential digit, %: percent, ,:thousand, .:decimal, ;different format pos,equal,neg, [Ee][+-], other string
1694 while (*ptr)
1695 {
1696 c=char(std::toupper(*(ptr++)));
1697 bool ok=true;
1698 switch (c)
1699 {
1700 case '0':
1701 case '9':
1702 if (digits>=0 && !scientific) ++digits;
1703 break;
1704 case ',':
1705 if (digits<0 && !scientific)
1706 hasThousand=true;
1707 else
1708 ok=false;
1709 break;
1710 case 'E':
1711 if (digits<0)
1712 scientific=true;
1713 else
1714 ok=false;
1715 break;
1716 case '.':
1717 if (digits<0 && !scientific)
1718 digits=0;
1719 else
1720 ok=false;
1721 break;
1722 case '+':
1723 case '-':
1724 ok=scientific;
1725 break;
1726 case ';':
1727 end=true;
1728 break;
1729 case '%':
1730 percent=true;
1731 break;
1732 default:
1733 if (digits || scientific)
1734 end=true;
1735 else
1736 ok=false;
1737 break;
1738 }
1739 if (!ok)
1740 {
1741 WPS_DEBUG_MSG(("QuattroSpreadsheet::updateCellWithUserFormat: unsure how to format %s\n", format.cstr()));
1742 cell.setFormat(cell.F_NUMBER, 0);
1743 return;
1744 }
1745 if (end)
1746 break;
1747 }
1748 if (digits>0)
1749 cell.setDigits(digits);
1750 if (scientific)
1751 cell.setFormat(cell.F_NUMBER,4);
1752 else if (percent)
1753 cell.setFormat(cell.F_NUMBER,3);
1754 else
1755 cell.setFormat(cell.F_NUMBER, hasThousand ? 5 : 1);
1756 return;
1757 }
1758 if (c!='T')
1759 {
1760 WPS_DEBUG_MSG(("QuattroSpreadsheet::updateCellWithUserFormat: unsure how to format %s\n", format.cstr()));
1761 return;
1762 }
1763 // d: day(1-31), dd: day(01-31), wday(sun), weekday(sunday),
1764 // m: month(1-12) or minute if preceded by h or hh, mm: month(01-12) or..
1765 // mo: month(1-12), mmo: month(01-12), mon(jan), month(january)
1766 // yy: year(00-99), yyyy: year(0001-9999), h: hour(0-23) except if ampm, hh: hour(00-23) except if ampm
1767 // mi: minute, mmi, s: second, ss
1768 // ampm:
1769 std::string dtFormat;
1770 bool hasHour=false;
1771 bool hasDate=false;
1772 bool inString=false;
1773 while (*ptr)
1774 {
1775 c=*(ptr++);
1776 if (inString)
1777 {
1778 if (c=='\'')
1779 inString=false;
1780 else if (c=='\\')
1781 {
1782 if (*ptr)
1783 dtFormat+=*(ptr++);
1784 }
1785 else
1786 dtFormat+=c;
1787 continue;
1788 }
1789 c=char(std::toupper(c));
1790 switch (c)
1791 {
1792 case 'A':
1793 if (libwps::strncasecmp(ptr, "mpm",3)==0)
1794 {
1795 dtFormat+="%p";
1796 ptr+=3;
1797 hasHour=true;
1798 }
1799 else
1800 dtFormat+=c;
1801 break;
1802 case 'D':
1803 if (*ptr=='d' || *ptr=='D')
1804 ++ptr;
1805 dtFormat+="%d";
1806 hasDate=true;
1807 break;
1808 case 'H':
1809 if (*ptr=='h' || *ptr=='H')
1810 ++ptr;
1811 dtFormat+="%H";
1812 hasHour=true;
1813 break;
1814 case 'M':
1815 if (*ptr=='m' || *ptr=='M')
1816 ++ptr;
1817 if (*ptr=='o' || *ptr=='O')
1818 {
1819 if (libwps::strncasecmp(ptr, "onth",4)==0)
1820 {
1821 dtFormat+="%B";
1822 ptr+=4;
1823 }
1824 else if (libwps::strncasecmp(ptr, "on",2)==0)
1825 {
1826 dtFormat+="%b";
1827 ptr+=2;
1828 }
1829 else
1830 {
1831 dtFormat+="%m";
1832 ++ptr;
1833 }
1834 hasDate=true;
1835 }
1836 else if (*ptr=='i' || *ptr=='I')
1837 {
1838 hasHour=true;
1839 dtFormat+="%M";
1840 ++ptr;
1841 }
1842 else if (hasHour)
1843 dtFormat+="%M";
1844 else
1845 dtFormat+="%m";
1846 break;
1847 case 'S':
1848 if (*ptr=='s' || *ptr=='S')
1849 ++ptr;
1850 dtFormat+="%S";
1851 hasHour=true;
1852 break;
1853 case 'W':
1854 if (libwps::strncasecmp(ptr, "day",3)==0)
1855 {
1856 dtFormat+="%a";
1857 ptr+=3;
1858 hasDate=true;
1859 }
1860 else if (libwps::strncasecmp(ptr, "eekday",6)==0)
1861 {
1862 dtFormat+="%A";
1863 ptr+=6;
1864 hasDate=true;
1865 }
1866 else
1867 dtFormat+=c;
1868 break;
1869 case 'Y':
1870 if (libwps::strncasecmp(ptr, "yyy",3)==0)
1871 {
1872 dtFormat+="%Y";
1873 ptr+=3;
1874 hasDate=true;
1875 }
1876 else if (libwps::strncasecmp(ptr, "y",1)==0)
1877 {
1878 dtFormat+="%y";
1879 ptr+=1;
1880 hasDate=true;
1881 }
1882 else
1883 dtFormat+=c;
1884 break;
1885 case '\'':
1886 inString=true;
1887 break;
1888 case '\\':
1889 if (*ptr)
1890 dtFormat+=*(ptr++);
1891 break;
1892 default:
1893 dtFormat+=c;
1894 break;
1895 }
1896 }
1897 cell.setDTFormat((hasDate||!hasHour) ? cell.F_DATE : cell.F_TIME, dtFormat);
1898 }
1899
sendCellContent(QuattroSpreadsheetInternal::Cell const & cell,int sheetId)1900 void QuattroSpreadsheet::sendCellContent(QuattroSpreadsheetInternal::Cell const &cell, int sheetId)
1901 {
1902 if (m_listener.get() == nullptr)
1903 {
1904 WPS_DEBUG_MSG(("QuattroSpreadsheet::sendCellContent: I can not find the listener\n"));
1905 return;
1906 }
1907
1908 libwps_tools_win::Font::Type fontType = cell.m_fontType;
1909 m_listener->setFont(cell.getFont());
1910
1911 QuattroSpreadsheetInternal::Cell finalCell(cell);
1912 auto &content=finalCell.m_content;
1913 for (auto &f : content.m_formula)
1914 {
1915 if (f.m_type==WKSContentListener::FormulaInstruction::F_Cell ||
1916 f.m_type==WKSContentListener::FormulaInstruction::F_CellList)
1917 {
1918 int dim=f.m_type==WKSContentListener::FormulaInstruction::F_Cell ? 1 : 2;
1919 for (int i=0; i<dim; ++i)
1920 {
1921 if (f.m_sheetId[i]>=0 && f.m_sheetName[i].empty() && (f.m_sheetId[i]!=sheetId || !f.m_fileName.empty()))
1922 f.m_sheetName[i]=getSheetName(f.m_sheetId[i]);
1923 }
1924 continue;
1925 }
1926 if (f.m_type!=WKSContentListener::FormulaInstruction::F_Text)
1927 continue;
1928 std::string &text=f.m_content;
1929 librevenge::RVNGString finalString=libwps_tools_win::Font::unicodeString(text, fontType);
1930 if (finalString.empty())
1931 text.clear();
1932 else
1933 text=finalString.cstr();
1934 }
1935 if ((finalCell.m_fileFormat>>4)==7)
1936 {
1937 auto it=m_state->m_idToUserFormatMap.find(finalCell.m_fileFormat&0xf);
1938 if (it==m_state->m_idToUserFormatMap.end() || it->second.empty())
1939 {
1940 WPS_DEBUG_MSG(("QuattroSpreadsheet::sendCellContent: can not find an user format\n"));
1941 }
1942 else
1943 updateCellWithUserFormat(finalCell, it->second);
1944 }
1945 else
1946 finalCell.updateFormat();
1947 m_listener->openSheetCell(finalCell, content);
1948 if (cell.m_hasGraphic)
1949 m_mainParser.sendGraphics(sheetId, cell.position());
1950 if (cell.m_content.m_textEntry.valid())
1951 {
1952 if (!cell.m_stream || !cell.m_stream->m_input)
1953 {
1954 WPS_DEBUG_MSG(("QuattroSpreadsheet::sendCellContent: oops can not find the text's stream\n"));
1955 }
1956 else
1957 {
1958 auto input=cell.m_stream->m_input;
1959 input->seek(cell.m_content.m_textEntry.begin(), librevenge::RVNG_SEEK_SET);
1960 bool prevEOL=false;
1961 std::string text;
1962 while (input->tell()<=cell.m_content.m_textEntry.end())
1963 {
1964 bool last=input->isEnd() || input->tell()>=cell.m_content.m_textEntry.end();
1965 auto c=last ? '\0' : char(libwps::readU8(input));
1966 if ((c==0 || c==0xa || c==0xd) && !text.empty())
1967 {
1968 m_listener->insertUnicodeString(libwps_tools_win::Font::unicodeString(text, fontType));
1969 text.clear();
1970 }
1971 if (last) break;
1972 if (c==0xd)
1973 {
1974 m_listener->insertEOL();
1975 prevEOL=true;
1976 }
1977 else if (c==0xa)
1978 {
1979 if (!prevEOL)
1980 {
1981 WPS_DEBUG_MSG(("QuattroSpreadsheet::sendCellContent: find 0xa without 0xd\n"));
1982 }
1983 prevEOL=false;
1984 }
1985 else
1986 {
1987 if (c)
1988 text.push_back(c);
1989 prevEOL=false;
1990 }
1991 }
1992 }
1993 }
1994 m_listener->closeSheetCell();
1995 }
1996
1997 /* vim:set shiftwidth=4 softtabstop=4 noexpandtab: */
1998