You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koffice/filters/kword/html/export/ExportFilter.cpp

655 lines
20 KiB

/*
This file is part of the KDE project
Copyright (C) 2001, 2002, 2004 Nicolas GOUTTE <goutte@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include <tqstring.h>
#include <tqtextcodec.h>
#include <tqfile.h>
#include <tqfileinfo.h>
#include <tqdir.h>
#include <tqpicture.h>
#include <tdelocale.h>
#include <kdebug.h>
#include <KWEFUtil.h>
#include <KWEFBaseWorker.h>
#include "ExportFilter.h"
TQString HtmlWorker::escapeHtmlText(const TQString& strText) const
{
// Escape quotes (needed in attributes)
// Do not escape apostrophs (only allowed in XHTML!)
return KWEFUtil::EscapeSgmlText(getCodec(),strText,true,false);
}
bool HtmlWorker::makeTable(const FrameAnchor& anchor)
{
*m_streamOut << "<table>\n";
*m_streamOut << "<tbody>\n";
TQValueList<TableCell>::ConstIterator itCell;
int rowCurrent=0;
*m_streamOut << "<tr>\n";
for (itCell=anchor.table.cellList.begin();
itCell!=anchor.table.cellList.end(); itCell++)
{
if (rowCurrent!=(*itCell).row)
{
rowCurrent=(*itCell).row;
*m_streamOut << "</tr>\n<tr>\n";
}
*m_streamOut << "<td";
if ( (*itCell).m_rows > 1 )
*m_streamOut << " rowspan=\"" << (*itCell).m_rows << "\"";
if ( (*itCell).m_cols > 1 )
*m_streamOut << " colspan=\"" << (*itCell).m_cols << "\"";
*m_streamOut << ">\n";
if (!doFullAllParagraphs(*(*itCell).paraList))
{
return false;
}
*m_streamOut << "</td>\n";
}
*m_streamOut << "</tr>\n";
*m_streamOut << "</tbody>\n";
*m_streamOut << "</table>\n";
return true;
}
TQString HtmlWorker::getAdditionalFileName(const TQString& additionalName)
{
kdDebug(30503) << "HtmlWorker::getAdditionalFileName " << additionalName << endl;
TQDir dir(m_strFileDir);
kdDebug(30503) << "Base directory: " << m_strFileDir << endl;
if (!dir.exists(m_strSubDirectoryName))
{
// Make the directory, as it does not exist yet!
kdDebug(30503) << "Creating directory: " << m_strSubDirectoryName << endl;
dir.mkdir(m_strSubDirectoryName);
}
TQString strFileName(m_strSubDirectoryName);
strFileName+="/";
const int result=additionalName.findRev("/");
if (result>=0)
{
strFileName+=additionalName.mid(result+1);
}
else
{
strFileName+=additionalName;
}
// Now, we have to create a backup file.
TQString strBackupName(strFileName);
strBackupName+="~";
kdDebug(30503) << "Remove backup file: " << strBackupName << endl;
// We need to remove the backup file, as not all filesystems or ports can do it themselves on a rename.
dir.remove(strBackupName);
kdDebug(30503) << "Moving file: " << additionalName << " => " << strBackupName << endl;
dir.rename(strFileName,strBackupName);
return strFileName;
}
bool HtmlWorker::makeImage(const FrameAnchor& anchor)
{
const TQString strImageName(getAdditionalFileName(anchor.picture.koStoreName));
TQString strImagePath(m_strFileDir);
strImagePath+='/';
strImagePath+=strImageName;
TQByteArray image;
kdDebug(30503) << "Image " << anchor.picture.koStoreName << " will be written in " << strImageName << endl;
if (loadSubFile(anchor.picture.koStoreName,image))
{
bool writePicture = false;
const double height = anchor.frame.bottom - anchor.frame.top;
const double width = anchor.frame.right - anchor.frame.left;
const int pos = anchor.picture.koStoreName.findRev( '.' );
TQString extension;
if ( pos > -1 )
extension = anchor.picture.koStoreName.mid( pos+1 ).lower();
if ( extension == "png" || extension == "jpeg" || extension == "jpg" || extension == "gif"
|| extension == "bmp" ) // A few file types known by all HTML user agents
{
*m_streamOut << "<img "; // This is an empty element!
*m_streamOut << "src=\"" << escapeHtmlText(strImageName) << "\" ";
*m_streamOut << "alt=\"" << escapeHtmlText(anchor.picture.key.filename()) << "\"";
*m_streamOut << (isXML()?"/>":">");
writePicture = true;
}
else if ( extension == "svg" )
{
// Save picture as SVG
*m_streamOut << "<object data=\"" << escapeHtmlText(strImageName) << "\"";
*m_streamOut << " type=\"image/svg+xml\"";
*m_streamOut << " height=\"" << height << "\" width=\"" << width << "\">\n";
*m_streamOut << "</object>"; // <object> is *not* an empty element in HTML!
writePicture = true;
}
else if ( extension == "qpic" )
{
TQPicture picture;
TQIODevice* io=getSubFileDevice(anchor.picture.koStoreName);
if (!io)
{
// NO message error, as there must be already one
return false;
}
// TODO: if we have alreasy SVG, do *not* go through TQPicture!
if (picture.load(io))
{
// Save picture as SVG
*m_streamOut << "<object data=\"" << escapeHtmlText(strImageName) << "\"";
*m_streamOut << " type=\"image/svg+xml\"";
*m_streamOut << " height=\"" << height << "\" width=\"" << width << "\">\n";
*m_streamOut << "</object>"; // <object> is *not* an empty element in HTML!
// TODO: other props for image
kdDebug(30506) << "Trying to save clipart to " << strImageName << endl;
if (!picture.save(strImagePath,"svg"))
{
kdError(30506) << "Could not save clipart: " << anchor.picture.koStoreName
<< " to " << strImageName << endl;
return false;
}
}
}
else
{
// ### TODO: avoid loading the picture 2 times
image.resize( 0 );
if ( ! loadAndConvertToImage( anchor.picture.koStoreName, extension, "PNG", image ) )
{
kdWarning(30503) << "Could not convert picture to PNG!" << endl;
return false;
}
*m_streamOut << "<img "; // This is an empty element!
*m_streamOut << "src=\"" << escapeHtmlText(strImageName) << "\" ";
*m_streamOut << "alt=\"" << escapeHtmlText(anchor.picture.key.filename()) << "\"";
*m_streamOut << (isXML()?"/>":">");
writePicture = true;
}
// Do we still need to write the original picture?
if ( writePicture )
{
TQFile file(strImagePath);
if ( !file.open (IO_WriteOnly) )
{
kdError(30503) << "Unable to open image output file!" << endl;
return false;
}
file.writeBlock(image);
file.close();
}
}
else
{
kdWarning(30503) << "Unable to load picture " << anchor.picture.koStoreName << endl;
}
return true;
}
void HtmlWorker::formatTextParagraph(const TQString& strText,
const FormatData& formatOrigin, const FormatData& format)
{
TQString strEscaped(escapeHtmlText(strText));
// Replace line feeds by line breaks
int pos;
TQString strBr(isXML()?TQString("<br />"):TQString("<br>"));
while ((pos=strEscaped.find(TQChar(10)))>-1)
{
strEscaped.replace(pos,1,strBr);
}
if (!format.text.missing)
{
// Opening elements
openSpan(formatOrigin,format);
}
// TODO: first and last characters of partialText should not be a space (white space problems!)
// TODO: replace multiples spaces by non-breaking spaces!
if (strText==" ")
{//Just a space as text. Therefore we must use a non-breaking space.
*m_streamOut << "&nbsp;";
// TODO/FIXME: only needed for <p>&nbsp;</p>, but not for </span> <span>
}
else
{
*m_streamOut << strEscaped;
}
if (!format.text.missing)
{
// Closing elements
closeSpan(formatOrigin,format);
}
}
void HtmlWorker::ProcessParagraphData (const TQString& strTag, const TQString &paraText,
const LayoutData& layout, const ValueListFormatData &paraFormatDataList)
{
if (paraText.isEmpty() && paraFormatDataList.first().id != 6)
{
openParagraph(strTag,layout);
*m_streamOut << "&nbsp;" ; // A paragraph can never be empty in HTML
closeParagraph(strTag,layout);
}
else
{
bool paragraphNotOpened=true;
ValueListFormatData::ConstIterator paraFormatDataIt;
TQString partialText;
for ( paraFormatDataIt = paraFormatDataList.begin ();
paraFormatDataIt != paraFormatDataList.end ();
paraFormatDataIt++ )
{
if (1==(*paraFormatDataIt).id)
{
//Retrieve text
partialText=paraText.mid ( (*paraFormatDataIt).pos, (*paraFormatDataIt).len );
// For normal text, we need an opened paragraph
if (paragraphNotOpened)
{
openParagraph(strTag,layout,partialText.ref(0).direction());
paragraphNotOpened=false;
}
formatTextParagraph(partialText,layout.formatData,*paraFormatDataIt);
}
else if (4==(*paraFormatDataIt).id)
{
// For variables, we need an opened paragraph
if (paragraphNotOpened)
{
openParagraph(strTag,layout);
paragraphNotOpened=false;
}
if (9==(*paraFormatDataIt).variable.m_type)
{
// A link
*m_streamOut << "<a href=\""
<< escapeHtmlText((*paraFormatDataIt).variable.getHrefName())
<< "\">"
<< escapeHtmlText((*paraFormatDataIt).variable.getLinkName())
<< "</a>";
}
else
{
// Generic variable
*m_streamOut << escapeHtmlText((*paraFormatDataIt).variable.m_text);
}
}
else if (6==(*paraFormatDataIt).id)
{
// We have an image, a clipart or a table
if (6==(*paraFormatDataIt).frameAnchor.type)
{
// We have a table
// But first, we must sure that the paragraph is not opened.
if (!paragraphNotOpened)
{
// The paragraph was opened, so close it.
closeParagraph(strTag,layout);
}
makeTable((*paraFormatDataIt).frameAnchor);
// The paragraph will need to be opened again
paragraphNotOpened=true;
}
else if ( ( 2 == (*paraFormatDataIt).frameAnchor.type )
|| ( 5 == (*paraFormatDataIt).frameAnchor.type ) )
{
// <img> and <object> need to be in a paragraph
if (paragraphNotOpened)
{
openParagraph( strTag, layout,partialText.ref(0). direction() );
paragraphNotOpened=false;
}
makeImage((*paraFormatDataIt).frameAnchor);
}
else
{
kdWarning(30503) << "Unknown anchor type: "
<< (*paraFormatDataIt).frameAnchor.type << endl;
}
}
}
if (!paragraphNotOpened)
{
// The paragraph was opened, so close it.
closeParagraph(strTag,layout);
}
}
}
bool HtmlWorker::doFullParagraph(const TQString& paraText,
const LayoutData& layout, const ValueListFormatData& paraFormatDataList)
{
kdDebug(30503) << "Entering HtmlWorker::doFullParagraph" << endl << paraText << endl;
TQString strParaText=paraText;
TQString strTag; // Tag that will be written.
if ( layout.counter.numbering == CounterData::NUM_LIST )
{
const uint layoutDepth=layout.counter.depth+1; // Word's depth starts at 0!
const uint listDepth=m_listStack.size();
// We are in a list, but has it the right depth?
if (layoutDepth>listDepth)
{
ListInfo newList;
newList.m_typeList=layout.counter.style;
for (uint i=listDepth; i<layoutDepth; i++)
{
*m_streamOut << getStartOfListOpeningTag(layout.counter.style,newList.m_orderedList);
m_listStack.push(newList);
}
}
else if (layoutDepth<listDepth)
{
for (uint i=listDepth; i>layoutDepth; i--)
{
ListInfo oldList=m_listStack.pop();
if (oldList.m_orderedList)
{
*m_streamOut << "</ol>\n";
}
else
{
*m_streamOut << "</ul>\n";
}
}
}
// We have a list but does it have the right type?
if ( layout.counter.style!=m_listStack.top().m_typeList)
{
// No, then close the previous list
ListInfo oldList=m_listStack.pop();
if (oldList.m_orderedList)
{
*m_streamOut << "</ol>\n";
}
else
{
*m_streamOut << "</ul>\n";
}
ListInfo newList;
*m_streamOut << getStartOfListOpeningTag(layout.counter.style,newList.m_orderedList);
newList.m_typeList=layout.counter.style;
m_listStack.push(newList);
}
// TODO: with Cascaded Style Sheet, we could add the exact counter type that we want
strTag="li";
}
else
{
// Close all open lists first
if (!m_listStack.isEmpty())
{
for (uint i=m_listStack.size(); i>0; i--)
{
ListInfo oldList=m_listStack.pop();
if (oldList.m_orderedList)
{
*m_streamOut << "</ol>\n";
}
else
{
*m_streamOut << "</ul>\n";
}
}
}
if ( (layout.counter.numbering == CounterData::NUM_CHAPTER)
&& (layout.counter.depth<6) )
{
strTag=TQString("h%1").arg(layout.counter.depth + 1); // H1 ... H6
}
else
{
strTag="p";
}
}
ProcessParagraphData(strTag, strParaText, layout, paraFormatDataList);
kdDebug(30503) << "Quiting HtmlWorker::doFullParagraph" << endl;
return true;
}
bool HtmlWorker::doOpenFile(const TQString& filenameOut, const TQString& /*to*/)
{
m_ioDevice=new TQFile(filenameOut);
if (!m_ioDevice)
{
kdError(30503) << "No output file! Aborting!" << endl;
return false;
}
if ( !m_ioDevice->open (IO_WriteOnly) )
{
kdError(30503) << "Unable to open output file!" << endl;
return false;
}
m_streamOut=new TQTextStream(m_ioDevice);
if (!getCodec())
{
kdError(30503) << "Could not create TQTextCodec! Aborting" << endl;
return false;
}
kdDebug(30503) << "Charset used: " << getCodec()->name() << endl;
m_streamOut->setCodec( getCodec() );
m_fileName=filenameOut;
TQFileInfo base(m_fileName);
m_strFileDir=base.dirPath();
m_strTitle=base.fileName();
m_strSubDirectoryName=base.fileName();
m_strSubDirectoryName+=".dir";
return true;
}
bool HtmlWorker::doCloseFile(void)
{
kdDebug(30503) << __FILE__ << ":" << __LINE__ << endl;
delete m_streamOut;
m_streamOut=NULL;
if (m_ioDevice)
m_ioDevice->close();
return true;
}
void HtmlWorker::writeDocType(void)
{
// write <!DOCTYPE
*m_streamOut << "<!DOCTYPE ";
if (isXML())
{
*m_streamOut << "html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"DTD/xhtml1-strict.dtd\">\n";
}
else
{
*m_streamOut << "HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n";
}
}
bool HtmlWorker::doOpenDocument(void)
{
// Make the file header
if (isXML())
{ //Write out the XML declaration
*m_streamOut << "<?xml version=\"1.0\" encoding=\""
<< getCodec()->mimeName() << "\"?>" << endl;
}
// write <!DOCTYPE
writeDocType();
// No "lang" or "xml:lang" attribute for <html>, as we do not know in which language the document is!
*m_streamOut << "<html";
if (isXML())
{
// XHTML has an extra attribute defining its namespace (in the <html> opening tag)
*m_streamOut << " xmlns=\"http://www.w3.org/1999/xhtml\"";
}
*m_streamOut << ">\n";
return true;
}
bool HtmlWorker::doCloseDocument(void)
{
kdDebug(30503) << __FILE__ << ":" << __LINE__ << endl;
*m_streamOut << "</html>\n";
return true;
}
bool HtmlWorker::doFullDocumentInfo(const KWEFDocumentInfo& docInfo)
{
TQString strText=docInfo.title;
if (!strText.isEmpty())
{
m_strTitle=strText; // Set title only if it is not empty!
kdDebug(30503) << "Found new title " << m_strTitle << endl;
}
return true;
}
bool HtmlWorker::doOpenHead(void)
{
*m_streamOut << "<head>" << endl;
// Declare what charset we are using
*m_streamOut << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=";
*m_streamOut << getCodec()->mimeName() << '"';
*m_streamOut << (isXML()?" /":"") << ">\n" ;
// Say who we are (with the CVS revision number) in case we have a bug in our filter output!
TQString strVersion("$Revision: 466447 $");
// Eliminate the dollar signs
// (We don't want that the version number changes if the HTML file is itself put in a CVS storage.)
*m_streamOut << "<meta name=\"Generator\" content=\"KWord HTML Export Filter Version"
<< strVersion.mid( 10 ).remove( '$' )
<< "\""<< (isXML()?" /":"") // X(HT)ML closes empty elements, HTML not!
<< ">\n";
if (m_strTitle.isEmpty())
{
// Somehow we have still an empty title (this should not happen!)
kdWarning(30503) << "Title still empty! (HtmlWorker::doOpenHead)" << endl;
m_strTitle=i18n("Untitled Document");
}
*m_streamOut << "<title>"<< escapeHtmlText(m_strTitle) <<"</title>\n"; // <TITLE> is mandatory!
if( !customCSSURL().isEmpty() )
{
*m_streamOut << "<link ref=\"stylesheet\" type=\"text/css\" href=\"" << customCSSURL() << "\" title=\"Style\" >\n" << endl;
}
//TODO: transform documentinfo.xml into many <META> elements (at least the author!)
return true;
}
bool HtmlWorker::doCloseHead(void)
{
*m_streamOut << "</head>\n";
return true;
}
bool HtmlWorker::doOpenBody(void)
{
*m_streamOut << "<body>\n";
return true;
}
bool HtmlWorker::doCloseBody(void)
{
*m_streamOut << "</body>\n";
return true;
}
bool HtmlWorker::doOpenTextFrameSet(void)
{
return true;
}
bool HtmlWorker::doCloseTextFrameSet(void)
{
if (!m_listStack.isEmpty())
{
for (uint i=m_listStack.size(); i>0; i--)
{
ListInfo oldList=m_listStack.pop();
if (oldList.m_orderedList)
{
*m_streamOut << "</ol>\n";
}
else
{
*m_streamOut << "</ul>\n";
}
}
}
return true;
}