html/export/ExportFilter.cpp
1/*
2 This file is part of the KDE project
3 Copyright (C) 2001, 2002, 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 <tqstring.h>
22#include <tqtextcodec.h>
23#include <tqfile.h>
24#include <tqfileinfo.h>
25#include <tqdir.h>
26#include <tqpicture.h>
27
28#include <tdelocale.h>
29#include <kdebug.h>
30
31#include <KWEFUtil.h>
32#include <KWEFBaseWorker.h>
33
34#include "ExportFilter.h"
35
36TQString HtmlWorker::escapeHtmlText(const TQString& strText) const
37{
38 // Escape quotes (needed in attributes)
39 // Do not escape apostrophs (only allowed in XHTML!)
40 return KWEFUtil::EscapeSgmlText(getCodec(),strText,true,false);
41}
42
43bool HtmlWorker::makeTable(const FrameAnchor& anchor)
44{
45 *m_streamOut << "<table>\n";
46 *m_streamOut << "<tbody>\n";
47
48 TQValueList<TableCell>::ConstIterator itCell;
49
50 int rowCurrent=0;
51 *m_streamOut << "<tr>\n";
52
53
54 for (itCell=anchor.table.cellList.begin();
55 itCell!=anchor.table.cellList.end(); itCell++)
56 {
57 if (rowCurrent!=(*itCell).row)
58 {
59 rowCurrent=(*itCell).row;
60 *m_streamOut << "</tr>\n<tr>\n";
61 }
62
63 *m_streamOut << "<td";
64 if ( (*itCell).m_rows > 1 )
65 *m_streamOut << " rowspan=\"" << (*itCell).m_rows << "\"";
66 if ( (*itCell).m_cols > 1 )
67 *m_streamOut << " colspan=\"" << (*itCell).m_cols << "\"";
68 *m_streamOut << ">\n";
69
70 if (!doFullAllParagraphs(*(*itCell).paraList))
71 {
72 return false;
73 }
74
75 *m_streamOut << "</td>\n";
76 }
77
78 *m_streamOut << "</tr>\n";
79 *m_streamOut << "</tbody>\n";
80 *m_streamOut << "</table>\n";
81
82 return true;
83}
84
85TQString HtmlWorker::getAdditionalFileName(const TQString& additionalName)
86{
87 kdDebug(30503) << "HtmlWorker::getAdditionalFileName " << additionalName << endl;
88
89 TQDir dir(m_strFileDir);
90 kdDebug(30503) << "Base directory: " << m_strFileDir << endl;
91
92 if (!dir.exists(m_strSubDirectoryName))
93 {
94 // Make the directory, as it does not exist yet!
95 kdDebug(30503) << "Creating directory: " << m_strSubDirectoryName << endl;
96 dir.mkdir(m_strSubDirectoryName);
97 }
98
99 TQString strFileName(m_strSubDirectoryName);
100 strFileName+="/";
101 const int result=additionalName.findRev("/");
102 if (result>=0)
103 {
104 strFileName+=additionalName.mid(result+1);
105 }
106 else
107 {
108 strFileName+=additionalName;
109 }
110
111 // Now, we have to create a backup file.
112
113 TQString strBackupName(strFileName);
114 strBackupName+="~";
115 kdDebug(30503) << "Remove backup file: " << strBackupName << endl;
116 // We need to remove the backup file, as not all filesystems or ports can do it themselves on a rename.
117 dir.remove(strBackupName);
118 kdDebug(30503) << "Moving file: " << additionalName << " => " << strBackupName << endl;
119 dir.rename(strFileName,strBackupName);
120
121 return strFileName;
122}
123
124bool HtmlWorker::makeImage(const FrameAnchor& anchor)
125{
126 const TQString strImageName(getAdditionalFileName(anchor.picture.koStoreName));
127
128 TQString strImagePath(m_strFileDir);
129 strImagePath+='/';
130 strImagePath+=strImageName;
131
132 TQByteArray image;
133
134 kdDebug(30503) << "Image " << anchor.picture.koStoreName << " will be written in " << strImageName << endl;
135
136 if (loadSubFile(anchor.picture.koStoreName,image))
137 {
138 bool writePicture = false;
139
140 const double height = anchor.frame.bottom - anchor.frame.top;
141 const double width = anchor.frame.right - anchor.frame.left;
142
143 const int pos = anchor.picture.koStoreName.findRev( '.' );
144 TQString extension;
145 if ( pos > -1 )
146 extension = anchor.picture.koStoreName.mid( pos+1 ).lower();
147
148 if ( extension == "png" || extension == "jpeg" || extension == "jpg" || extension == "gif"
149 || extension == "bmp" ) // A few file types known by all HTML user agents
150 {
151 *m_streamOut << "<img "; // This is an empty element!
152 *m_streamOut << "src=\"" << escapeHtmlText(strImageName) << "\" ";
153 *m_streamOut << "alt=\"" << escapeHtmlText(anchor.picture.key.filename()) << "\"";
154 *m_streamOut << (isXML()?"/>":">");
155 writePicture = true;
156 }
157 else if ( extension == "svg" )
158 {
159 // Save picture as SVG
160 *m_streamOut << "<object data=\"" << escapeHtmlText(strImageName) << "\"";
161 *m_streamOut << " type=\"image/svg+xml\"";
162 *m_streamOut << " height=\"" << height << "\" width=\"" << width << "\">\n";
163 *m_streamOut << "</object>"; // <object> is *not* an empty element in HTML!
164 writePicture = true;
165 }
166 else if ( extension == "qpic" )
167 {
168
169 TQPicture picture;
170
171 TQIODevice* io=getSubFileDevice(anchor.picture.koStoreName);
172 if (!io)
173 {
174 // NO message error, as there must be already one
175 return false;
176 }
177
178 // TODO: if we have alreasy SVG, do *not* go through TQPicture!
179 if (picture.load(io))
180 {
181
182 // Save picture as SVG
183 *m_streamOut << "<object data=\"" << escapeHtmlText(strImageName) << "\"";
184 *m_streamOut << " type=\"image/svg+xml\"";
185 *m_streamOut << " height=\"" << height << "\" width=\"" << width << "\">\n";
186 *m_streamOut << "</object>"; // <object> is *not* an empty element in HTML!
187 // TODO: other props for image
188
189 kdDebug(30506) << "Trying to save clipart to " << strImageName << endl;
190 if (!picture.save(strImagePath,"svg"))
191 {
192 kdError(30506) << "Could not save clipart: " << anchor.picture.koStoreName
193 << " to " << strImageName << endl;
194 return false;
195 }
196
197 }
198 }
199 else
200 {
201 // ### TODO: avoid loading the picture 2 times
202 image.resize( 0 );
203 if ( ! loadAndConvertToImage( anchor.picture.koStoreName, extension, "PNG", image ) )
204 {
205 kdWarning(30503) << "Could not convert picture to PNG!" << endl;
206 return false;
207 }
208 *m_streamOut << "<img "; // This is an empty element!
209 *m_streamOut << "src=\"" << escapeHtmlText(strImageName) << "\" ";
210 *m_streamOut << "alt=\"" << escapeHtmlText(anchor.picture.key.filename()) << "\"";
211 *m_streamOut << (isXML()?"/>":">");
212 writePicture = true;
213 }
214
215 // Do we still need to write the original picture?
216 if ( writePicture )
217 {
218 TQFile file(strImagePath);
219
220 if ( !file.open (IO_WriteOnly) )
221 {
222 kdError(30503) << "Unable to open image output file!" << endl;
223 return false;
224 }
225
226 file.writeBlock(image);
227 file.close();
228 }
229 }
230 else
231 {
232 kdWarning(30503) << "Unable to load picture " << anchor.picture.koStoreName << endl;
233 }
234
235 return true;
236}
237
238void HtmlWorker::formatTextParagraph(const TQString& strText,
239 const FormatData& formatOrigin, const FormatData& format)
240{
241 TQString strEscaped(escapeHtmlText(strText));
242
243 // Replace line feeds by line breaks
244 int pos;
245 TQString strBr(isXML()?TQString("<br />"):TQString("<br>"));
246 while ((pos=strEscaped.find(TQChar(10)))>-1)
247 {
248 strEscaped.replace(pos,1,strBr);
249 }
250
251 if (!format.text.missing)
252 {
253 // Opening elements
254 openSpan(formatOrigin,format);
255 }
256
257 // TODO: first and last characters of partialText should not be a space (white space problems!)
258 // TODO: replace multiples spaces by non-breaking spaces!
259
260 if (strText==" ")
261 {//Just a space as text. Therefore we must use a non-breaking space.
262 *m_streamOut << "&nbsp;";
263 // TODO/FIXME: only needed for <p>&nbsp;</p>, but not for </span> <span>
264 }
265 else
266 {
267 *m_streamOut << strEscaped;
268 }
269
270 if (!format.text.missing)
271 {
272 // Closing elements
273 closeSpan(formatOrigin,format);
274 }
275}
276
277void HtmlWorker::ProcessParagraphData (const TQString& strTag, const TQString &paraText,
278 const LayoutData& layout, const ValueListFormatData &paraFormatDataList)
279{
280 if (paraText.isEmpty() && paraFormatDataList.first().id != 6)
281 {
282 openParagraph(strTag,layout);
283 *m_streamOut << "&nbsp;" ; // A paragraph can never be empty in HTML
284 closeParagraph(strTag,layout);
285 }
286 else
287 {
288 bool paragraphNotOpened=true;
289
290 ValueListFormatData::ConstIterator paraFormatDataIt;
291
292 TQString partialText;
293
294 for ( paraFormatDataIt = paraFormatDataList.begin ();
295 paraFormatDataIt != paraFormatDataList.end ();
296 paraFormatDataIt++ )
297 {
298 if (1==(*paraFormatDataIt).id)
299 {
300 //Retrieve text
301 partialText=paraText.mid ( (*paraFormatDataIt).pos, (*paraFormatDataIt).len );
302 // For normal text, we need an opened paragraph
303 if (paragraphNotOpened)
304 {
305 openParagraph(strTag,layout,partialText.ref(0).direction());
306 paragraphNotOpened=false;
307 }
308 formatTextParagraph(partialText,layout.formatData,*paraFormatDataIt);
309 }
310 else if (4==(*paraFormatDataIt).id)
311 {
312 // For variables, we need an opened paragraph
313 if (paragraphNotOpened)
314 {
315 openParagraph(strTag,layout);
316 paragraphNotOpened=false;
317 }
318 if (9==(*paraFormatDataIt).variable.m_type)
319 {
320 // A link
321 *m_streamOut << "<a href=\""
322 << escapeHtmlText((*paraFormatDataIt).variable.getHrefName())
323 << "\">"
324 << escapeHtmlText((*paraFormatDataIt).variable.getLinkName())
325 << "</a>";
326 }
327 else
328 {
329 // Generic variable
330 *m_streamOut << escapeHtmlText((*paraFormatDataIt).variable.m_text);
331 }
332 }
333 else if (6==(*paraFormatDataIt).id)
334 {
335 // We have an image, a clipart or a table
336
337 if (6==(*paraFormatDataIt).frameAnchor.type)
338 {
339 // We have a table
340 // But first, we must sure that the paragraph is not opened.
341 if (!paragraphNotOpened)
342 {
343 // The paragraph was opened, so close it.
344 closeParagraph(strTag,layout);
345 }
346 makeTable((*paraFormatDataIt).frameAnchor);
347 // The paragraph will need to be opened again
348 paragraphNotOpened=true;
349
350 }
351
352 else if ( ( 2 == (*paraFormatDataIt).frameAnchor.type )
353 || ( 5 == (*paraFormatDataIt).frameAnchor.type ) )
354 {
355 // <img> and <object> need to be in a paragraph
356 if (paragraphNotOpened)
357 {
358 openParagraph( strTag, layout,partialText.ref(0). direction() );
359 paragraphNotOpened=false;
360 }
361 makeImage((*paraFormatDataIt).frameAnchor);
362 }
363 else
364 {
365 kdWarning(30503) << "Unknown anchor type: "
366 << (*paraFormatDataIt).frameAnchor.type << endl;
367 }
368 }
369 }
370 if (!paragraphNotOpened)
371 {
372 // The paragraph was opened, so close it.
373 closeParagraph(strTag,layout);
374 }
375 }
376}
377
378bool HtmlWorker::doFullParagraph(const TQString& paraText,
379 const LayoutData& layout, const ValueListFormatData& paraFormatDataList)
380{
381 kdDebug(30503) << "Entering HtmlWorker::doFullParagraph" << endl << paraText << endl;
382 TQString strParaText=paraText;
383 TQString strTag; // Tag that will be written.
384
385 if ( layout.counter.numbering == CounterData::NUM_LIST )
386 {
387 const uint layoutDepth=layout.counter.depth+1; // Word's depth starts at 0!
388 const uint listDepth=m_listStack.size();
389 // We are in a list, but has it the right depth?
390 if (layoutDepth>listDepth)
391 {
392 ListInfo newList;
393 newList.m_typeList=layout.counter.style;
394 for (uint i=listDepth; i<layoutDepth; i++)
395 {
396 *m_streamOut << getStartOfListOpeningTag(layout.counter.style,newList.m_orderedList);
397 m_listStack.push(newList);
398 }
399 }
400 else if (layoutDepth<listDepth)
401 {
402 for (uint i=listDepth; i>layoutDepth; i--)
403 {
404 ListInfo oldList=m_listStack.pop();
405 if (oldList.m_orderedList)
406 {
407 *m_streamOut << "</ol>\n";
408 }
409 else
410 {
411 *m_streamOut << "</ul>\n";
412 }
413 }
414 }
415
416 // We have a list but does it have the right type?
417 if ( layout.counter.style!=m_listStack.top().m_typeList)
418 {
419 // No, then close the previous list
420 ListInfo oldList=m_listStack.pop();
421 if (oldList.m_orderedList)
422 {
423 *m_streamOut << "</ol>\n";
424 }
425 else
426 {
427 *m_streamOut << "</ul>\n";
428 }
429 ListInfo newList;
430 *m_streamOut << getStartOfListOpeningTag(layout.counter.style,newList.m_orderedList);
431 newList.m_typeList=layout.counter.style;
432 m_listStack.push(newList);
433 }
434
435 // TODO: with Cascaded Style Sheet, we could add the exact counter type that we want
436 strTag="li";
437 }
438 else
439 {
440 // Close all open lists first
441 if (!m_listStack.isEmpty())
442 {
443 for (uint i=m_listStack.size(); i>0; i--)
444 {
445 ListInfo oldList=m_listStack.pop();
446 if (oldList.m_orderedList)
447 {
448 *m_streamOut << "</ol>\n";
449 }
450 else
451 {
452 *m_streamOut << "</ul>\n";
453 }
454 }
455 }
456 if ( (layout.counter.numbering == CounterData::NUM_CHAPTER)
457 && (layout.counter.depth<6) )
458 {
459 strTag=TQString("h%1").arg(layout.counter.depth + 1); // H1 ... H6
460 }
461 else
462 {
463 strTag="p";
464 }
465 }
466
467 ProcessParagraphData(strTag, strParaText, layout, paraFormatDataList);
468
469 kdDebug(30503) << "Quiting HtmlWorker::doFullParagraph" << endl;
470 return true;
471}
472
473bool HtmlWorker::doOpenFile(const TQString& filenameOut, const TQString& /*to*/)
474{
475 m_ioDevice=new TQFile(filenameOut);
476
477 if (!m_ioDevice)
478 {
479 kdError(30503) << "No output file! Aborting!" << endl;
480 return false;
481 }
482
483 if ( !m_ioDevice->open (IO_WriteOnly) )
484 {
485 kdError(30503) << "Unable to open output file!" << endl;
486 return false;
487 }
488
489 m_streamOut=new TQTextStream(m_ioDevice);
490
491 if (!getCodec())
492 {
493 kdError(30503) << "Could not create TQTextCodec! Aborting" << endl;
494 return false;
495 }
496
497 kdDebug(30503) << "Charset used: " << getCodec()->name() << endl;
498
499 m_streamOut->setCodec( getCodec() );
500
501 m_fileName=filenameOut;
502 TQFileInfo base(m_fileName);
503 m_strFileDir=base.dirPath();
504 m_strTitle=base.fileName();
505 m_strSubDirectoryName=base.fileName();
506 m_strSubDirectoryName+=".dir";
507
508 return true;
509}
510
511bool HtmlWorker::doCloseFile(void)
512{
513 kdDebug(30503) << __FILE__ << ":" << __LINE__ << endl;
514 delete m_streamOut;
515 m_streamOut=NULL;
516 if (m_ioDevice)
517 m_ioDevice->close();
518 return true;
519}
520
521void HtmlWorker::writeDocType(void)
522{
523 // write <!DOCTYPE
524 *m_streamOut << "<!DOCTYPE ";
525 if (isXML())
526 {
527 *m_streamOut << "html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"DTD/xhtml1-strict.dtd\">\n";
528 }
529 else
530 {
531 *m_streamOut << "HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n";
532 }
533}
534
535bool HtmlWorker::doOpenDocument(void)
536{
537 // Make the file header
538
539 if (isXML())
540 { //Write out the XML declaration
541 *m_streamOut << "<?xml version=\"1.0\" encoding=\""
542 << getCodec()->mimeName() << "\"?>" << endl;
543 }
544
545 // write <!DOCTYPE
546 writeDocType();
547
548 // No "lang" or "xml:lang" attribute for <html>, as we do not know in which language the document is!
549 *m_streamOut << "<html";
550 if (isXML())
551 {
552 // XHTML has an extra attribute defining its namespace (in the <html> opening tag)
553 *m_streamOut << " xmlns=\"http://www.w3.org/1999/xhtml\"";
554 }
555 *m_streamOut << ">\n";
556 return true;
557}
558
559bool HtmlWorker::doCloseDocument(void)
560{
561 kdDebug(30503) << __FILE__ << ":" << __LINE__ << endl;
562 *m_streamOut << "</html>\n";
563 return true;
564}
565
566bool HtmlWorker::doFullDocumentInfo(const KWEFDocumentInfo& docInfo)
567{
568 TQString strText=docInfo.title;
569 if (!strText.isEmpty())
570 {
571 m_strTitle=strText; // Set title only if it is not empty!
572 kdDebug(30503) << "Found new title " << m_strTitle << endl;
573 }
574 return true;
575}
576
577bool HtmlWorker::doOpenHead(void)
578{
579 *m_streamOut << "<head>" << endl;
580
581 // Declare what charset we are using
582 *m_streamOut << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=";
583 *m_streamOut << getCodec()->mimeName() << '"';
584 *m_streamOut << (isXML()?" /":"") << ">\n" ;
585
586 // Say who we are (with the CVS revision number) in case we have a bug in our filter output!
587 TQString strVersion("$Revision: 466447 $");
588 // Eliminate the dollar signs
589 // (We don't want that the version number changes if the HTML file is itself put in a CVS storage.)
590 *m_streamOut << "<meta name=\"Generator\" content=\"KWord HTML Export Filter Version"
591 << strVersion.mid( 10 ).remove( '$' )
592 << "\""<< (isXML()?" /":"") // X(HT)ML closes empty elements, HTML not!
593 << ">\n";
594
595 if (m_strTitle.isEmpty())
596 {
597 // Somehow we have still an empty title (this should not happen!)
598 kdWarning(30503) << "Title still empty! (HtmlWorker::doOpenHead)" << endl;
599 m_strTitle=i18n("Untitled Document");
600 }
601 *m_streamOut << "<title>"<< escapeHtmlText(m_strTitle) <<"</title>\n"; // <TITLE> is mandatory!
602
603 if( !customCSSURL().isEmpty() )
604 {
605 *m_streamOut << "<link ref=\"stylesheet\" type=\"text/css\" href=\"" << customCSSURL() << "\" title=\"Style\" >\n" << endl;
606 }
607
608 //TODO: transform documentinfo.xml into many <META> elements (at least the author!)
609
610 return true;
611}
612
613bool HtmlWorker::doCloseHead(void)
614{
615 *m_streamOut << "</head>\n";
616 return true;
617}
618
619bool HtmlWorker::doOpenBody(void)
620{
621 *m_streamOut << "<body>\n";
622 return true;
623}
624
625bool HtmlWorker::doCloseBody(void)
626{
627 *m_streamOut << "</body>\n";
628 return true;
629}
630
631bool HtmlWorker::doOpenTextFrameSet(void)
632{
633 return true;
634}
635
636bool HtmlWorker::doCloseTextFrameSet(void)
637{
638 if (!m_listStack.isEmpty())
639 {
640 for (uint i=m_listStack.size(); i>0; i--)
641 {
642 ListInfo oldList=m_listStack.pop();
643 if (oldList.m_orderedList)
644 {
645 *m_streamOut << "</ol>\n";
646 }
647 else
648 {
649 *m_streamOut << "</ul>\n";
650 }
651 }
652 }
653 return true;
654}
@ NUM_LIST
Numbered as a list item.
@ NUM_CHAPTER
Numbered as a heading.
This is basically <FRAMESET> tag.
Paragraph layout.