diff options
Diffstat (limited to 'sw/qa/extras/htmlexport/htmlexport.cxx')
-rw-r--r-- | sw/qa/extras/htmlexport/htmlexport.cxx | 3037 |
1 files changed, 3037 insertions, 0 deletions
diff --git a/sw/qa/extras/htmlexport/htmlexport.cxx b/sw/qa/extras/htmlexport/htmlexport.cxx new file mode 100644 index 0000000000..1bd883be11 --- /dev/null +++ b/sw/qa/extras/htmlexport/htmlexport.cxx @@ -0,0 +1,3037 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <swmodeltestbase.hxx> + +#include <memory> +#include <string_view> + +#include <com/sun/star/document/XEmbeddedObjectSupplier2.hpp> +#include <com/sun/star/document/XTypeDetection.hpp> +#include <com/sun/star/embed/ElementModes.hpp> +#include <com/sun/star/io/XActiveDataStreamer.hpp> +#include <com/sun/star/io/XSeekable.hpp> +#include <com/sun/star/style/ParagraphAdjust.hpp> +#include <com/sun/star/awt/FontUnderline.hpp> +#include <com/sun/star/table/TableBorder2.hpp> +#include <com/sun/star/text/XTextTablesSupplier.hpp> +#include <com/sun/star/text/XTextTable.hpp> +#include <com/sun/star/text/XTextEmbeddedObjectsSupplier.hpp> +#include <com/sun/star/document/XStorageBasedDocument.hpp> +#include <com/sun/star/frame/XStorable.hpp> +#include <com/sun/star/packages/zip/ZipFileAccess.hpp> +#include <com/sun/star/view/XSelectionSupplier.hpp> + +#include <test/htmltesttools.hxx> +#include <tools/urlobj.hxx> +#include <svtools/rtfkeywd.hxx> +#include <comphelper/propertyvalue.hxx> +#include <comphelper/propertysequence.hxx> +#include <svtools/parrtf.hxx> +#include <rtl/strbuf.hxx> +#include <svtools/rtftoken.h> +#include <filter/msfilter/rtfutil.hxx> +#include <sot/storage.hxx> +#include <vcl/svapp.hxx> +#include <unotools/mediadescriptor.hxx> +#include <svtools/htmlcfg.hxx> +#include <unotools/ucbstreamhelper.hxx> +#include <comphelper/processfactory.hxx> +#include <vcl/graphicfilter.hxx> +#include <vcl/dibtools.hxx> +#include <o3tl/string_view.hxx> +#include <editeng/brushitem.hxx> +#include <comphelper/scopeguard.hxx> + +#include <swmodule.hxx> +#include <swdll.hxx> +#include <usrpref.hxx> +#include <wrtsh.hxx> +#include <ndtxt.hxx> +#include <paratr.hxx> +#include <docsh.hxx> +#include <unotxdoc.hxx> +#include <formatlinebreak.hxx> +#include <itabenum.hxx> + +namespace +{ +/// Test RTF parser that just extracts a single OLE2 object from a file. +class TestReqIfRtfReader : public SvRTFParser +{ +public: + TestReqIfRtfReader(SvStream& rStream); + void NextToken(int nToken) override; + bool WriteObjectData(SvStream& rOLE); + tools::Long GetObjw() const { return m_nObjw; } + tools::Long GetObjh() const { return m_nObjh; } + int getWmetafile() const { return m_nWmetafile; } + +private: + bool m_bInObjData = false; + OStringBuffer m_aHex; + tools::Long m_nObjw = 0; + tools::Long m_nObjh = 0; + int m_nWmetafile = 0; +}; + +TestReqIfRtfReader::TestReqIfRtfReader(SvStream& rStream) + : SvRTFParser(rStream) +{ +} + +void TestReqIfRtfReader::NextToken(int nToken) +{ + switch (nToken) + { + case '}': + m_bInObjData = false; + break; + case RTF_TEXTTOKEN: + if (m_bInObjData) + m_aHex.append(OUStringToOString(aToken, RTL_TEXTENCODING_ASCII_US)); + break; + case RTF_OBJDATA: + m_bInObjData = true; + break; + case RTF_OBJW: + m_nObjw = nTokenValue; + break; + case RTF_OBJH: + m_nObjh = nTokenValue; + break; + case RTF_WMETAFILE: + m_nWmetafile = nTokenValue; + break; + } +} + +bool TestReqIfRtfReader::WriteObjectData(SvStream& rOLE) +{ + OString aObjdata = m_aHex.makeStringAndClear(); + + SvMemoryStream aStream; + int b = 0; + int count = 2; + + // Feed the destination text to a stream. + for (int i = 0; i < aObjdata.getLength(); ++i) + { + char ch = aObjdata[i]; + if (ch != 0x0d && ch != 0x0a) + { + b = b << 4; + sal_Int8 parsed = msfilter::rtfutil::AsHex(ch); + if (parsed == -1) + return false; + b += parsed; + count--; + if (!count) + { + aStream.WriteChar(b); + count = 2; + b = 0; + } + } + } + + aStream.Seek(0); + rOLE.WriteStream(aStream); + return true; +} + +/// Parser for [MS-OLEDS] 2.2.5 EmbeddedObject, aka OLE1. +struct OLE1Reader +{ + sal_uInt32 m_nNativeDataSize; + std::vector<char> m_aNativeData; + sal_uInt32 m_nPresentationDataSize; + + OLE1Reader(SvStream& rStream); +}; + +OLE1Reader::OLE1Reader(SvStream& rStream) +{ + // Skip ObjectHeader, see [MS-OLEDS] 2.2.4. + rStream.Seek(0); + CPPUNIT_ASSERT(rStream.remainingSize()); + sal_uInt32 nData; + rStream.ReadUInt32(nData); // OLEVersion + rStream.ReadUInt32(nData); // FormatID + rStream.ReadUInt32(nData); // ClassName + rStream.SeekRel(nData); + rStream.ReadUInt32(nData); // TopicName + rStream.SeekRel(nData); + rStream.ReadUInt32(nData); // ItemName + rStream.SeekRel(nData); + + rStream.ReadUInt32(m_nNativeDataSize); + m_aNativeData.resize(m_nNativeDataSize); + rStream.ReadBytes(m_aNativeData.data(), m_aNativeData.size()); + + rStream.ReadUInt32(nData); // OLEVersion for presentation data + CPPUNIT_ASSERT(rStream.good()); + rStream.ReadUInt32(nData); // FormatID + rStream.ReadUInt32(nData); // ClassName + rStream.SeekRel(nData); + rStream.ReadUInt32(nData); // Width + rStream.ReadUInt32(nData); // Height + rStream.ReadUInt32(nData); // PresentationDataSize + m_nPresentationDataSize = nData; +} +} + +/// Covers sw/source/filter/html/wrthtml.cxx and related fixes. +class HtmlExportTest : public SwModelTestBase, public HtmlTestTools +{ +public: + HtmlExportTest() + : SwModelTestBase("/sw/qa/extras/htmlexport/data/", "HTML (StarWriter)") + { + } + + /// Wraps an RTF fragment into a complete RTF file, so an RTF parser can handle it. + static void wrapRtfFragment(const OUString& rURL, SvMemoryStream& rStream) + { + SvFileStream aRtfStream(rURL, StreamMode::READ); + rStream.WriteOString("{\\rtf1"); + rStream.WriteStream(aRtfStream); + rStream.WriteOString("}"); + rStream.Seek(0); + } +}; + +/// HTML export of the sw doc model tests. +class SwHtmlDomExportTest : public SwModelTestBase, public HtmlTestTools +{ +public: + SwHtmlDomExportTest() + : SwModelTestBase("/sw/qa/extras/htmlexport/data/") + { + } + + OUString GetObjectPath(const OUString& ext); + /// Get the .ole path, assuming maTempFile is an XHTML export result. + OUString GetOlePath() { return GetObjectPath(u".ole"_ustr); } + OUString GetPngPath() { return GetObjectPath(u".png"_ustr); } + /// Parse the ole1 data out of an RTF fragment URL. + void ParseOle1FromRtfUrl(const OUString& rRtfUrl, SvMemoryStream& rOle1); + /// Export using the C++ HTML export filter, with xhtmlns=reqif-xhtml. + void ExportToReqif(); + /// Import using the C++ HTML import filter, with xhtmlns=reqif-xhtml. + void ImportFromReqif(const OUString& rUrl); + /// Export using the C++ HTML export filter + void ExportToHTML(); +}; + +OUString SwHtmlDomExportTest::GetObjectPath(const OUString& ext) +{ + assert(ext.startsWith(".")); + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + OUString aOlePath + = getXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object"_ostr, + "data"_ostr); + CPPUNIT_ASSERT(aOlePath.endsWith(ext)); + INetURLObject aUrl(maTempFile.GetURL()); + aUrl.setBase(aOlePath.subView(0, aOlePath.getLength() - ext.getLength())); + aUrl.setExtension(ext.subView(1)); + return aUrl.GetMainURL(INetURLObject::DecodeMechanism::NONE); +} + +void SwHtmlDomExportTest::ParseOle1FromRtfUrl(const OUString& rRtfUrl, SvMemoryStream& rOle1) +{ + SvMemoryStream aRtf; + HtmlExportTest::wrapRtfFragment(rRtfUrl, aRtf); + tools::SvRef<TestReqIfRtfReader> xReader(new TestReqIfRtfReader(aRtf)); + CPPUNIT_ASSERT(xReader->CallParser() != SvParserState::Error); + CPPUNIT_ASSERT(xReader->WriteObjectData(rOle1)); + CPPUNIT_ASSERT(rOle1.Tell()); +} + +void SwHtmlDomExportTest::ExportToReqif() +{ + setFilterOptions("xhtmlns=reqif-xhtml"); + save("HTML (StarWriter)"); +} + +void SwHtmlDomExportTest::ExportToHTML() +{ + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); +} + +void SwHtmlDomExportTest::ImportFromReqif(const OUString& rUrl) +{ + uno::Sequence<beans::PropertyValue> aLoadProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + }; + mxComponent = loadFromDesktop(rUrl, "com.sun.star.text.TextDocument", aLoadProperties); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testFdo81276) +{ + auto verify = [this]() { + uno::Reference<container::XNameAccess> xPageStyles(getStyles("PageStyles")); + uno::Reference<beans::XPropertySet> xStyle(xPageStyles->getByName("HTML"), uno::UNO_QUERY); + // some rounding going on here? + CPPUNIT_ASSERT(abs(sal_Int32(29700) - getProperty<sal_Int32>(xStyle, "Width")) < 10); + CPPUNIT_ASSERT(abs(sal_Int32(21006) - getProperty<sal_Int32>(xStyle, "Height")) < 10); + CPPUNIT_ASSERT(abs(sal_Int32(500) - getProperty<sal_Int32>(xStyle, "LeftMargin")) < 10); + CPPUNIT_ASSERT(abs(sal_Int32(500) - getProperty<sal_Int32>(xStyle, "RightMargin")) < 10); + CPPUNIT_ASSERT(abs(sal_Int32(2000) - getProperty<sal_Int32>(xStyle, "TopMargin")) < 10); + CPPUNIT_ASSERT(abs(sal_Int32(500) - getProperty<sal_Int32>(xStyle, "BottomMargin")) < 10); + }; + setImportFilterName(mpFilter); + createSwDoc("fdo81276.html"); + verify(); + saveAndReload(mpFilter); + verify(); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testFdo62336) +{ + // The problem was essentially a crash during table export as docx/rtf/html + // If calc-layout is enabled, the crash does not occur, that's why loadFromFile/save is used + loadFromFile(u"fdo62336.docx"); + save("HTML (StarWriter)"); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testFdo86857) +{ + auto verify = [this]() { + // problem was that background color on page style was not exported + uno::Reference<container::XNameAccess> xPageStyles(getStyles("PageStyles")); + uno::Reference<beans::XPropertySet> xStyle(xPageStyles->getByName("HTML"), uno::UNO_QUERY); + CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, getProperty<Color>(xStyle, "BackColor")); + // check that table background color works, which still uses RES_BACKGROUND + uno::Reference<text::XTextTablesSupplier> xTablesSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<container::XIndexAccess> xTables(xTablesSupplier->getTextTables(), + uno::UNO_QUERY); + CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xTables->getCount()); + uno::Reference<text::XTextTable> xTable(xTables->getByIndex(0), uno::UNO_QUERY); + uno::Reference<text::XTextRange> xCell(xTable->getCellByName("A1"), uno::UNO_QUERY); + CPPUNIT_ASSERT_EQUAL(Color(0x66ffff), getProperty<Color>(xCell, "BackColor")); + }; + + setImportFilterName(mpFilter); + createSwDoc("fdo86857.html"); + verify(); + saveAndReload(mpFilter); + verify(); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testCharacterBorder) +{ + // FIXME if padding-top gets exported as inches, not cms, we get rounding errors. + SwGlobals::ensure(); // make sure that SW_MOD() is not 0 + SwMasterUsrPref* pPref = const_cast<SwMasterUsrPref*>(SW_MOD()->GetUsrPref(false)); + FieldUnit eUnit = pPref->GetMetric(); + pPref->SetMetric(FieldUnit::CM); + comphelper::ScopeGuard g([pPref, eUnit]() { pPref->SetMetric(eUnit); }); + + auto verify = [this]() { + CPPUNIT_ASSERT_EQUAL(1, getPages()); + + uno::Reference<beans::XPropertySet> xRun(getRun(getParagraph(1), 1), uno::UNO_QUERY); + // Different Border + { + CPPUNIT_ASSERT_BORDER_EQUAL(table::BorderLine2(0x6666FF, 12, 12, 12, 3, 37), + getProperty<table::BorderLine2>(xRun, "CharTopBorder")); + CPPUNIT_ASSERT_BORDER_EQUAL(table::BorderLine2(0xFF9900, 0, 99, 0, 2, 99), + getProperty<table::BorderLine2>(xRun, "CharLeftBorder")); + CPPUNIT_ASSERT_BORDER_EQUAL(table::BorderLine2(0xFF0000, 0, 169, 0, 1, 169), + getProperty<table::BorderLine2>(xRun, "CharBottomBorder")); + CPPUNIT_ASSERT_BORDER_EQUAL(table::BorderLine2(0x0000FF, 0, 169, 0, 0, 169), + getProperty<table::BorderLine2>(xRun, "CharRightBorder")); + } + + // Different Padding + { + CPPUNIT_ASSERT_EQUAL(sal_Int32(450), + getProperty<sal_Int32>(xRun, "CharTopBorderDistance")); + CPPUNIT_ASSERT_EQUAL(sal_Int32(550), + getProperty<sal_Int32>(xRun, "CharLeftBorderDistance")); + CPPUNIT_ASSERT_EQUAL(sal_Int32(150), + getProperty<sal_Int32>(xRun, "CharBottomBorderDistance")); + CPPUNIT_ASSERT_EQUAL(sal_Int32(250), + getProperty<sal_Int32>(xRun, "CharRightBorderDistance")); + } + + // No shadow + }; + createSwDoc("charborder.odt"); + verify(); + saveAndReload(mpFilter); + verify(); +} + +#define DECLARE_HTMLEXPORT_TEST(TestName, filename) \ + DECLARE_SW_EXPORT_TEST(TestName, filename, nullptr, HtmlExportTest) + +DECLARE_HTMLEXPORT_TEST(testExportOfImages, "textAndImage.docx") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body"_ostr, 1); + assertXPath(pDoc, "/html/body/p/img"_ostr, 1); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testExportOfImagesWithSkipImagesEnabled) +{ + createSwDoc("textAndImage.docx"); + setFilterOptions("SkipImages"); + save(mpFilter); + + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body"_ostr, 1); + assertXPath(pDoc, "/html/body/p/img"_ostr, 0); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testSkipImagesEmbedded) +{ + createSwDoc("skipimage-embedded.doc"); + setFilterOptions("SkipImages"); + save(mpFilter); + + // Embedded spreadsheet was exported as image, so content was lost. Make + // sure it's exported as HTML instead. + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // This was 0. + assertXPath(pDoc, "//table"_ostr, 1); + // This was 2, the HTML header was in the document two times. + assertXPath(pDoc, "//meta[@name='generator']"_ostr, 1); + // This was 0, <table> was directly under <p>, which caused errors in the parser. + assertXPath(pDoc, "//span/table"_ostr, 1); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testSkipImagesEmbeddedDocument) +{ + createSwDoc("skipimage-embedded-document.docx"); + setFilterOptions("SkipImages"); + save(mpFilter); + + // Similar to testSkipImagesEmbedded, but with an embedded Writer object, + // not a Calc one, and this time OOXML, not WW8. + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // This was 2, the HTML header was in the document two times. + assertXPath(pDoc, "//meta[@name='generator']"_ostr, 1); + // Text of embedded document was missing. + assertXPathContent(pDoc, "/html/body/p/span/p/span"_ostr, "Inner."); +} + +DECLARE_HTMLEXPORT_TEST(testExportImageProperties, "HTMLImage.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body"_ostr, 1); + + assertXPath(pDoc, "/html/body/p/map/area"_ostr, "shape"_ostr, "poly"); + assertXPath(pDoc, "/html/body/p/map/area"_ostr, "href"_ostr, "http://www.microsoft.com/"); + assertXPath(pDoc, "/html/body/p/map/area"_ostr, "target"_ostr, "_self"); + assertXPath(pDoc, "/html/body/p/map/area"_ostr, "alt"_ostr, "microsoft"); + + assertXPath(pDoc, "/html/body/p/a"_ostr, 1); + assertXPath(pDoc, "/html/body/p/a"_ostr, "href"_ostr, "http://www.google.com/"); + + assertXPath(pDoc, "/html/body/p/a/font"_ostr, 1); + assertXPath(pDoc, "/html/body/p/a/font"_ostr, "color"_ostr, "#ff0000"); + + assertXPath(pDoc, "/html/body/p/a/font/img"_ostr, 1); + assertXPath(pDoc, "/html/body/p/a/font/img"_ostr, "name"_ostr, "Text"); + assertXPath(pDoc, "/html/body/p/a/font/img"_ostr, "alt"_ostr, "Four colors"); + assertXPath(pDoc, "/html/body/p/a/font/img"_ostr, "align"_ostr, "middle"); + + // Probably the DPI in OSX is different and Twip -> Pixel conversion produces + // different results - so disable OSX for now. + // + // It would make sense to switch to use CSS and use "real world" units instead + // i.e. (style="margin: 0cm 1.5cm; width: 1cm; height: 1cm") + +#if 0 // disabled as it depends that the system DPI is set to 96 + assertXPath(pDoc, "/html/body/p/a/font/img", "hspace", "38"); + assertXPath(pDoc, "/html/body/p/a/font/img", "vspace", "19"); + assertXPath(pDoc, "/html/body/p/a/font/img", "width", "222"); + assertXPath(pDoc, "/html/body/p/a/font/img", "height", "222"); + assertXPath(pDoc, "/html/body/p/a/font/img", "border", "3"); +#endif + + assertXPath(pDoc, "/html/body/p/a/font/img"_ostr, "usemap"_ostr, "#map1"); +} + +DECLARE_HTMLEXPORT_TEST(testExportCheckboxRadioButtonState, "checkbox-radiobutton.doc") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body"_ostr, 1); + assertXPath(pDoc, "/html/body/p[1]/input"_ostr, "type"_ostr, "checkbox"); + assertXPath(pDoc, "/html/body/p[1]/input"_ostr, "checked"_ostr, "checked"); + assertXPath(pDoc, "/html/body/p[2]/input"_ostr, "type"_ostr, "checkbox"); + assertXPathNoAttribute(pDoc, "/html/body/p[2]/input"_ostr, "checked"_ostr); + assertXPath(pDoc, "/html/body/form/p[1]/input"_ostr, "type"_ostr, "checkbox"); + assertXPath(pDoc, "/html/body/form/p[1]/input"_ostr, "checked"_ostr, "checked"); + assertXPath(pDoc, "/html/body/form/p[2]/input"_ostr, "type"_ostr, "checkbox"); + assertXPathNoAttribute(pDoc, "/html/body/form/p[2]/input"_ostr, "checked"_ostr); + assertXPath(pDoc, "/html/body/form/p[3]/input"_ostr, "type"_ostr, "radio"); + assertXPath(pDoc, "/html/body/form/p[3]/input"_ostr, "checked"_ostr, "checked"); + assertXPath(pDoc, "/html/body/form/p[4]/input"_ostr, "type"_ostr, "radio"); + assertXPathNoAttribute(pDoc, "/html/body/form/p[4]/input"_ostr, "checked"_ostr); +} + +DECLARE_HTMLEXPORT_TEST(testExportUrlEncoding, "tdf76291.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // Test URI encoded hyperlink with Chinese characters + assertXPath(pDoc, "/html/body/p/a"_ostr, "href"_ostr, + "http://www.youtube.com/results?search_query=%E7%B2%B5%E8%AA%9Emv&sm=12"); +} + +DECLARE_HTMLEXPORT_TEST(testExportInternalUrl, "tdf90905.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // Internal url should be valid + assertXPath(pDoc, "/html/body/p[1]/a"_ostr, "href"_ostr, "#0.0.1.Text|outline"); + assertXPath(pDoc, "/html/body/p[2]/a"_ostr, "href"_ostr, "#bookmark"); +} + +DECLARE_HTMLEXPORT_TEST(testExportImageBulletList, "tdf66822.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // Encoded base64 SVG bullet should match and render on browser + assertXPath(pDoc, "/html/body/ul"_ostr, 1); + assertXPath( + pDoc, "/html/body/ul"_ostr, "style"_ostr, + "list-style-image: url(data:image/svg+xml;base64," + "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3" + "RyYXRvciAxMi4wLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDUxNDQ4KSAg" + "LS0+DQo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm" + "9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiIFsNCgk8IUVOVElUWSBuc19zdmcgImh0dHA6Ly93d3cu" + "dzMub3JnLzIwMDAvc3ZnIj4NCgk8IUVOVElUWSBuc194bGluayAiaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluay" + "I+DQpdPg0KPHN2ZyAgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9IiZuc19zdmc7IiB4bWxuczp4bGlu" + "az0iJm5zX3hsaW5rOyIgd2lkdGg9IjE0LjAwOCIgaGVpZ2h0PSIxNC4wMSINCgkgdmlld0JveD0iMCAwIDE0LjAwOC" + "AxNC4wMSIgb3ZlcmZsb3c9InZpc2libGUiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE0LjAwOCAxNC4wMSIg" + "eG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cmFkaWFsR3JhZGllbnQgaWQ9IlhNTElEXzRfIiBjeD0iNy4wMD" + "Q0IiBjeT0iNy4wMDQ5IiByPSI3LjAwNDQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCgkJPHN0b3Ag" + "IG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IzM1REIzNSIvPg0KCQk8c3RvcCAgb2Zmc2V0PSIxIiBzdHlsZT" + "0ic3RvcC1jb2xvcjojMDBBMDAwIi8+DQoJPC9yYWRpYWxHcmFkaWVudD4NCgk8Y2lyY2xlIGZpbGw9InVybCgjWE1M" + "SURfNF8pIiBjeD0iNy4wMDQiIGN5PSI3LjAwNSIgcj0iNy4wMDQiLz4NCgk8ZGVmcz4NCgkJPGZpbHRlciBpZD0iQW" + "RvYmVfT3BhY2l0eU1hc2tGaWx0ZXIiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMy40ODEiIHk9IjAu" + "NjkzIiB3aWR0aD0iNi45ODgiIGhlaWdodD0iMy44OTMiPg0KCQkJPGZlQ29sb3JNYXRyaXggIHR5cGU9Im1hdHJpeC" + "IgdmFsdWVzPSIxIDAgMCAwIDAgIDAgMSAwIDAgMCAgMCAwIDEgMCAwICAwIDAgMCAxIDAiLz4NCgkJPC9maWx0ZXI+" + "DQoJPC9kZWZzPg0KCTxtYXNrIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjMuNDgxIiB5PSIwLjY5MyIgd2" + "lkdGg9IjYuOTg4IiBoZWlnaHQ9IjMuODkzIiBpZD0iWE1MSURfNV8iPg0KCQk8ZyBmaWx0ZXI9InVybCgjQWRvYmVf" + "T3BhY2l0eU1hc2tGaWx0ZXIpIj4NCgkJCTxsaW5lYXJHcmFkaWVudCBpZD0iWE1MSURfNl8iIGdyYWRpZW50VW5pdH" + "M9InVzZXJTcGFjZU9uVXNlIiB4MT0iNy4xMjIxIiB5MT0iMC4xMDMiIHgyPSI3LjEyMjEiIHkyPSI1LjIzNDQiPg0K" + "CQkJCTxzdG9wICBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkYiLz4NCgkJCQk8c3RvcCAgb2Zmc2" + "V0PSIxIiBzdHlsZT0ic3RvcC1jb2xvcjojMDAwMDAwIi8+DQoJCQk8L2xpbmVhckdyYWRpZW50Pg0KCQkJPHJlY3Qg" + "eD0iMy4xOTkiIHk9IjAuMzM5IiBvcGFjaXR5PSIwLjciIGZpbGw9InVybCgjWE1MSURfNl8pIiB3aWR0aD0iNy44ND" + "YiIGhlaWdodD0iNC42MDEiLz4NCgkJPC9nPg0KCTwvbWFzaz4NCgk8ZWxsaXBzZSBtYXNrPSJ1cmwoI1hNTElEXzVf" + "KSIgZmlsbD0iI0ZGRkZGRiIgY3g9IjYuOTc1IiBjeT0iMi42NCIgcng9IjMuNDk0IiByeT0iMS45NDYiLz4NCjwvZz" + "4NCjwvc3ZnPg0K);"); +} + +DECLARE_HTMLEXPORT_TEST(testTdf83890, "tdf83890.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body/ol[2]/ol"_ostr, "start"_ostr, "2"); +} + +DECLARE_HTMLEXPORT_TEST(testExtbChars, "extb.html") +{ + OUString aExpected(u"\U00024b62"_ustr); + // Assert that UTF8 encoded non-BMP Unicode character is correct + uno::Reference<text::XTextRange> xTextRange1 = getRun(getParagraph(1), 1); + CPPUNIT_ASSERT_EQUAL(aExpected, xTextRange1->getString()); + + // Assert that non-BMP Unicode in character entity format is correct + uno::Reference<text::XTextRange> xTextRange2 = getRun(getParagraph(2), 1); + CPPUNIT_ASSERT_EQUAL(aExpected, xTextRange2->getString()); +} + +DECLARE_HTMLEXPORT_TEST(testNormalImageExport, "textAndImage.docx") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body"_ostr, 1); + + // the HTML export normally does not embed the images + OUString imgSrc = getXPath(pDoc, "/html/body/p/img"_ostr, "src"_ostr); + CPPUNIT_ASSERT(imgSrc.endsWith(".png")); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testEmbedImagesEnabled) +{ + createSwDoc("textAndImage.docx"); + setFilterOptions("EmbedImages"); + save(mpFilter); + + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body"_ostr, 1); + + // the HTML export normally does not embed the images, but here the test + // name triggers setting of the "EmbedImages" filter option, meaning the + // image will not be a separate PNG, but an embedded base64 encoded + // version of that + OUString imgSrc = getXPath(pDoc, "/html/body/p/img"_ostr, "src"_ostr); + CPPUNIT_ASSERT(imgSrc.startsWith("data:image/png;base64,")); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testXHTML) +{ + createSwWebDoc("hello.html"); + setFilterOptions("XHTML"); + save(mpFilter); + + OString aExpected("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML"_ostr); + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + OString aActual(read_uInt8s_ToOString(*pStream, aExpected.getLength())); + // This was HTML, not XHTML. + CPPUNIT_ASSERT_EQUAL(aExpected, aActual); + + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + // This was lang, not xml:lang. + assertXPath(pDoc, "/html/body"_ostr, "xml:lang"_ostr, "en-US"); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfParagraph) +{ + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-p.xhtml"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + + OString aExpected = "<reqif-xhtml:div><reqif-xhtml:p>aaa<reqif-xhtml:br/>\nbbb" + "</reqif-xhtml:p>" SAL_NEWLINE_STRING + // This was '<table' instead. + "<reqif-xhtml:table"_ostr; + + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + pStream->Seek(0); + OString aActual(read_uInt8s_ToOString(*pStream, aExpected.getLength())); + // This was a HTML header, like '<!DOCTYPE html ...'. + CPPUNIT_ASSERT_EQUAL(aExpected, aActual); + + // This was "<a", was not found. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:a") != -1); + + // This was "<u>" instead of CSS. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:span style=\"text-decoration: underline\"") != -1); + + // This was <strong>, namespace prefix was missing. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:strong>") != -1); + + // This was "<strike>" instead of CSS. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:span style=\"text-decoration: line-through\"") + != -1); + + // This was "<font>" instead of CSS + namespace prefix was missing. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:span style=\"color: #ce181e\"") != -1); + + // This was '<reqif-xhtml:a id="...">': non-unique bookmark name in reqif fragment. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(-1), aStream.indexOf("<reqif-xhtml:a id=")); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfOleData) +{ + auto verify = [this]() { + uno::Reference<text::XTextEmbeddedObjectsSupplier> xSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<container::XIndexAccess> xObjects(xSupplier->getEmbeddedObjects(), + uno::UNO_QUERY); + // This was 0, <object> without URL was ignored. + // Then this was 0 on export, as data of OLE nodes was ignored. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(1), xObjects->getCount()); + }; + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-ole-data.xhtml"); + verify(); + setFilterOptions("xhtmlns=reqif-xhtml"); + saveAndReload("HTML (StarWriter)"); + verify(); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfOleImg) +{ + auto verify = [this]() { + uno::Reference<text::XTextEmbeddedObjectsSupplier> xSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<container::XIndexAccess> xObjects(xSupplier->getEmbeddedObjects(), + uno::UNO_QUERY); + uno::Reference<document::XEmbeddedObjectSupplier2> xObject(xObjects->getByIndex(0), + uno::UNO_QUERY); + // This failed, OLE object had no replacement image. + // And then it also failed when the export lost the replacement image. + uno::Reference<graphic::XGraphic> xGraphic = xObject->getReplacementGraphic(); + // This failed when query and fragment of file:// URLs were not ignored. + CPPUNIT_ASSERT(xGraphic.is()); + + uno::Reference<drawing::XShape> xShape(xObject, uno::UNO_QUERY); + OutputDevice* pDevice = Application::GetDefaultDevice(); + Size aPixel(64, 64); + // Expected to be 1693. + Size aLogic(pDevice->PixelToLogic(aPixel, MapMode(MapUnit::Map100thMM))); + awt::Size aSize = xShape->getSize(); + // This was only 1247, size was not set explicitly. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(aLogic.getWidth()), aSize.Width); + + // Check mime/media types. + CPPUNIT_ASSERT_EQUAL(OUString("image/png"), getProperty<OUString>(xGraphic, "MimeType")); + + uno::Reference<beans::XPropertySet> xObjectProps(xObject, uno::UNO_QUERY); + uno::Reference<io::XActiveDataStreamer> xStreamProvider( + xObjectProps->getPropertyValue("EmbeddedObject"), uno::UNO_QUERY); + uno::Reference<io::XSeekable> xStream(xStreamProvider->getStream(), uno::UNO_QUERY); + // This was empty when either import or export handling was missing. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int64>(37888), xStream->getLength()); + + // Check alternate text (it was empty, for export the 'alt' attribute was used). + CPPUNIT_ASSERT_EQUAL(OUString("OLE Object"), + getProperty<OUString>(xObject, "Title").trim()); + + if (!isExported()) + return; + + // "type" attribute was missing for the inner <object> element. + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + CPPUNIT_ASSERT(aStream.indexOf("type=\"image/png\"") != -1); + }; + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-ole-img.xhtml"); + verify(); + setFilterOptions("xhtmlns=reqif-xhtml"); + saveAndReload("HTML (StarWriter)"); + verify(); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIfPngImg) +{ + auto verify = [this](bool bExported) { + uno::Reference<container::XNamed> xShape(getShape(1), uno::UNO_QUERY); + CPPUNIT_ASSERT(xShape.is()); + + if (!bExported) + { + // Imported PNG image is not an object. + CPPUNIT_ASSERT_EQUAL(OUString("Image1"), xShape->getName()); + return; + } + + // All images are exported as objects in ReqIF mode. + CPPUNIT_ASSERT_EQUAL(OUString("Object1"), xShape->getName()); + + // This was <img>, not <object>, which is not valid in the reqif-xhtml + // subset. + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:object") != -1); + + // Make sure that both RTF and PNG versions are written. + CPPUNIT_ASSERT(aStream.indexOf("text/rtf") != -1); + // This failed when images with a query in their file:// URL failed to + // import. + CPPUNIT_ASSERT(aStream.indexOf("image/png") != -1); + }; + + ImportFromReqif(createFileURL(u"reqif-png-img.xhtml")); + verify(/*bExported=*/false); + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ExportImagesAsOLE", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + mxComponent->dispose(); + ImportFromReqif(maTempFile.GetURL()); + verify(/*bExported=*/true); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfJpgImg) +{ + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-jpg-img.xhtml"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + // This was image/jpeg, JPG was not converted to PNG in ReqIF mode. + CPPUNIT_ASSERT(aStream.indexOf("type=\"image/png\"") != -1); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfTable) +{ + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-table.xhtml"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // <div> was missing, so the XHTML fragment wasn't a valid + // xhtml.BlkStruct.class type anymore. + assertXPath( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr/reqif-xhtml:th"_ostr, + 1); + // Make sure that the cell background is not written using CSS. + assertXPathNoAttribute( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr/reqif-xhtml:th"_ostr, + "style"_ostr); + // The attribute was present, which is not valid in reqif-xhtml. + assertXPathNoAttribute( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr/reqif-xhtml:th"_ostr, + "bgcolor"_ostr); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfTable2) +{ + createSwDoc("reqif-table2.odt"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + // This failed, <reqif-xhtml:td width="..."> was written. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:td>") != -1); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIfTableHeight) +{ + // Given a document with a table in it, with an explicit row height: + createSwDoc(); + uno::Sequence<beans::PropertyValue> aTableProperties = { + comphelper::makePropertyValue("Rows", static_cast<sal_Int32>(1)), + comphelper::makePropertyValue("Columns", static_cast<sal_Int32>(1)), + }; + dispatchCommand(mxComponent, ".uno:InsertTable", aTableProperties); + uno::Reference<text::XTextTablesSupplier> xTablesSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<container::XIndexAccess> xTables(xTablesSupplier->getTextTables(), + uno::UNO_QUERY); + uno::Reference<text::XTextTable> xTable(xTables->getByIndex(0), uno::UNO_QUERY); + uno::Reference<beans::XPropertySet> xRow(xTable->getRows()->getByIndex(0), uno::UNO_QUERY); + xRow->setPropertyValue("Height", uno::Any(static_cast<sal_Int32>(1000))); + + // When exporting to reqif-xhtml: + ExportToReqif(); + + // Then make sure that the explicit cell height is omitted from the output: + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed, explicit height was + // written, which is not valid reqif-xhtml. + assertXPathNoAttribute(pDoc, "//reqif-xhtml:td"_ostr, "height"_ostr); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testXHTMLUseCSS) +{ + createSwDoc("xhtml-css.odt"); + setFilterOptions("XHTML"); + save(mpFilter); + + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + // This failed, <font face="..."> was written. + CPPUNIT_ASSERT(aStream.indexOf("<span style=\"font-family:") != -1); + // This failed, <font size="..."> was written. + CPPUNIT_ASSERT(aStream.indexOf("<span style=\"font-size:") != -1); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfList) +{ + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-list.xhtml"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + // This failed, <ul> was written. + CPPUNIT_ASSERT(aStream.indexOf("<reqif-xhtml:ul>") != -1); + + // This failed, the 'style' attribute was written, even if the input did + // not had one. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(-1), aStream.indexOf(" style=\"")); + + // This failed <li> was only opened, not closed. + CPPUNIT_ASSERT(aStream.indexOf("</reqif-xhtml:li>") != -1); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfOle2) +{ + auto verify = [this]() { + uno::Reference<text::XTextEmbeddedObjectsSupplier> xSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<container::XIndexAccess> xObjects(xSupplier->getEmbeddedObjects(), + uno::UNO_QUERY); + uno::Reference<document::XEmbeddedObjectSupplier2> xObject(xObjects->getByIndex(0), + uno::UNO_QUERY); + uno::Reference<io::XActiveDataStreamer> xEmbeddedObject( + xObject->getExtendedControlOverEmbeddedObject(), uno::UNO_QUERY); + // This failed, the "RTF fragment" native data was loaded as-is, we had no + // filter to handle it, so nothing happened on double-click. + CPPUNIT_ASSERT(xEmbeddedObject.is()); + uno::Reference<io::XSeekable> xStream(xEmbeddedObject->getStream(), uno::UNO_QUERY); + // This was 38375, msfilter::rtfutil::ExtractOLE2FromObjdata() wrote + // everything after the OLE1 header into the OLE2 stream, while the + // Presentation field after the OLE2 data doesn't belong there. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int64>(37888), xStream->getLength()); + // Finally the export also failed as it tried to open the stream from the + // document storage, but the embedded object already opened it, so an + // exception of type com.sun.star.io.IOException was thrown. + + if (isExported()) + { + // Check that the replacement graphic is exported at RTF level. + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + // Get the path of the RTF data. + OUString aOlePath = getXPath( + pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object"_ostr, + "data"_ostr); + OUString aOleSuffix(".ole"); + CPPUNIT_ASSERT(aOlePath.endsWith(aOleSuffix)); + INetURLObject aUrl(maTempFile.GetURL()); + aUrl.setBase(aOlePath.subView(0, aOlePath.getLength() - aOleSuffix.getLength())); + aUrl.setExtension(u"ole"); + OUString aOleUrl = aUrl.GetMainURL(INetURLObject::DecodeMechanism::NONE); + + // Search for \result in the RTF data. + SvFileStream aOleStream(aOleUrl, StreamMode::READ); + CPPUNIT_ASSERT(aOleStream.IsOpen()); + OString aOleString(read_uInt8s_ToOString(aOleStream, aOleStream.TellEnd())); + // Without the accompanying fix in place, this test would have failed, + // replacement graphic was missing at RTF level. + CPPUNIT_ASSERT(aOleString.indexOf(OOO_STRING_SVTOOLS_RTF_RESULT) != -1); + } + }; + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-ole2.xhtml"); + verify(); + setFilterOptions("xhtmlns=reqif-xhtml"); + saveAndReload(mpFilter); + verify(); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testReqIfOle2Odg) +{ + auto verify = [this]() { + uno::Reference<text::XTextEmbeddedObjectsSupplier> xSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<container::XIndexAccess> xObjects(xSupplier->getEmbeddedObjects(), + uno::UNO_QUERY); + uno::Reference<document::XEmbeddedObjectSupplier> xTextEmbeddedObject( + xObjects->getByIndex(0), uno::UNO_QUERY); + uno::Reference<lang::XServiceInfo> xObject(xTextEmbeddedObject->getEmbeddedObject(), + uno::UNO_QUERY); + // This failed, both import and export failed to handle OLE2 that contains + // just ODF. + CPPUNIT_ASSERT(xObject.is()); + CPPUNIT_ASSERT(xObject->supportsService("com.sun.star.drawing.DrawingDocument")); + }; + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + createSwDoc("reqif-ole-odg.xhtml"); + verify(); + setFilterOptions("xhtmlns=reqif-xhtml"); + saveAndReload(mpFilter); + verify(); +} + +DECLARE_HTMLEXPORT_TEST(testList, "list.html") +{ + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + sal_uInt64 nLength = pStream->TellEnd(); + OString aStream(read_uInt8s_ToOString(*pStream, nLength)); + // This failed, it was <li/>, i.e. list item was closed before content + // started. + CPPUNIT_ASSERT(aStream.indexOf("<li>") != -1); +} + +DECLARE_HTMLEXPORT_TEST(testTransparentImage, "transparent-image.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + OUString aSource = getXPath(pDoc, "/html/body/p/img"_ostr, "src"_ostr); + OUString aMessage = "src attribute is: " + aSource; + // This was a jpeg, transparency was lost. + CPPUNIT_ASSERT_MESSAGE(aMessage.toUtf8().getStr(), aSource.endsWith(".gif")); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTransparentImageReqIf) +{ + createSwDoc("transparent-image.odt"); + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ExportImagesAsOLE", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + OUString aSource = getXPath( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object/reqif-xhtml:object"_ostr, + "data"_ostr); + OUString aMessage = "src attribute is: " + aSource; + // This was GIF, when the intention was to force PNG. + CPPUNIT_ASSERT_MESSAGE(aMessage.toUtf8().getStr(), aSource.endsWith(".png")); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testOleNodataReqIf) +{ + createSwDoc("reqif-ole-nodata.odt"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + // This failed, io::IOException was thrown during the filter() call. + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Make sure the native <object> element has the required data attribute. + OUString aSource = getXPath( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object/reqif-xhtml:object"_ostr, + "data"_ostr); + CPPUNIT_ASSERT(!aSource.isEmpty()); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testNoLangReqIf) +{ + createSwDoc("reqif-no-lang.odt"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Make sure that xml:lang is not written in ReqIF mode. + assertXPathNoAttribute(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:h1"_ostr, + "lang"_ostr); +} + +DECLARE_HTMLEXPORT_TEST(testFieldShade, "field-shade.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // Without the accompanying fix in place, this test would have failed with 'Expected: 1; Actual: + // 0', i.e. shading for the field was lost. + assertXPath(pDoc, "/html/body/p[1]/span"_ostr, "style"_ostr, "background: #c0c0c0"); + + // Check that field shading is written only in case there is no user-defined span background. + assertXPath(pDoc, "/html/body/p[2]/span"_ostr, "style"_ostr, "background: #ff0000"); + // Without the accompanying fix in place, this test would have failed with 'Expected: 0; Actual: + // 1', i.e there was an inner span hiding the wanted background color. + assertXPath(pDoc, "/html/body/p[2]/span/span"_ostr, 0); +} + +DECLARE_HTMLEXPORT_TEST(testTdf132739, "tdf132739.odt") +{ + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // Without the fix in place, this test would have failed with + // - Expected: background: #5983b0; border: 1px solid #333333; padding: 0.04in + // - Actual : background: #5983b0 + assertXPath(pDoc, "/html/body/table/tr[1]/td"_ostr, "style"_ostr, + "background: #5983b0; border: 1px solid #333333; padding: 0.04in"); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testFieldShadeReqIf) +{ + createSwDoc("field-shade-reqif.odt"); + setFilterOptions("xhtmlns=reqif-xhtml"); + save(mpFilter); + + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pDoc); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 0 + // - Actual : 2 + // i.e. the ReqIF subset of xhtml had a background color and a page number field, resulting in + // an invalid ReqIF-XHTML. + assertXPath(pDoc, "/html/body/div/p[1]/span"_ostr, 0); + assertXPath(pDoc, "/html/body/div/p[1]/sdfield"_ostr, 0); +} + +DECLARE_HTMLEXPORT_TEST(testTdf126879, "tdf126879.odt") +{ + constexpr OString aExpected("<!DOCTYPE html>"_ostr); + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + CPPUNIT_ASSERT(pStream); + const OString aActual(read_uInt8s_ToOString(*pStream, aExpected.getLength())); + // Without the fix in place, this test would have failed with + // - Expected: <!DOCTYPE html> + // - Actual : <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> + CPPUNIT_ASSERT_EQUAL(aExpected, aActual); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testBlockQuoteReqIf) +{ + // Build a document model that uses the Quotations paragraph style. + createSwDoc(); + uno::Reference<beans::XPropertySet> xParagraph(getParagraph(1), uno::UNO_QUERY); + xParagraph->setPropertyValue("ParaStyleName", uno::Any(OUString("Quotations"))); + + // Export it. + setFilterOptions("xhtmlns=reqif-xhtml"); + save("HTML (StarWriter)"); + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 0 + // i.e. <blackquote> had character (direct) children, which is invalid xhtml. + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:blockquote/reqif-xhtml:p"_ostr, + 1); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testRTFOLEMimeType) +{ + // Import a document with an embedded object. + OUString aType("test/rtf"); + ImportFromReqif(createFileURL(u"reqif-ole-data.xhtml")); + + // Export it. + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("RTFOLEMimeType", aType), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: test/rtf + // - Actual : text/rtf + // i.e. the MIME type was always text/rtf, not taking the store parameter into account. + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object"_ostr, + "type"_ostr, aType); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testChinese) +{ + // Load a document with Chinese text in it. + createSwDoc("reqif-chinese.odt"); + + // Export it. + ExportToReqif(); + // Without the accompanying fix in place, this would have failed as the output was not + // well-formed. + WrapReqifFromTempFile(); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifComment) +{ + // Create a document with a comment in it. + createSwDoc(); + uno::Sequence<beans::PropertyValue> aPropertyValues = comphelper::InitPropertySequence({ + { "Text", uno::Any(OUString("some text")) }, + { "Author", uno::Any(OUString("me")) }, + }); + dispatchCommand(mxComponent, ".uno:InsertAnnotation", aPropertyValues); + + // Export it. + ExportToReqif(); + // Without the accompanying fix in place, this would have failed as the output was not + // well-formed. + WrapReqifFromTempFile(); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifFontNameSize) +{ + // Create a document with a custom font name and size in it. + createSwDoc(); + uno::Reference<beans::XPropertySet> xParagraph(getParagraph(1), uno::UNO_QUERY); + xParagraph->setPropertyValue("CharFontName", uno::Any(OUString("Liberation Serif"))); + float fCharHeight = 14.0; + xParagraph->setPropertyValue("CharHeight", uno::Any(fCharHeight)); + sal_Int32 nCharColor = 0xff0000; + xParagraph->setPropertyValue("CharColor", uno::Any(nCharColor)); + uno::Reference<text::XTextRange> xTextRange(xParagraph, uno::UNO_QUERY); + xTextRange->setString("x"); + + // Export it. + ExportToReqif(); + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 3 + // i.e. font name and size was written, even if that's not relevant for ReqIF. + assertXPath(pDoc, "//reqif-xhtml:span"_ostr, 1); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifParagraphAlignment) +{ + // Create a document with an explicitly aligned paragraph. + createSwDoc(); + uno::Reference<beans::XPropertySet> xParagraph(getParagraph(1), uno::UNO_QUERY); + xParagraph->setPropertyValue("ParaAdjust", + uno::Any(static_cast<sal_Int16>(style::ParagraphAdjust_RIGHT))); + + // Export it. + ExportToReqif(); + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: + // - Actual : right + // i.e. the <reqif-xhtml:p align="..."> markup was used, which is invalid. + assertXPathNoAttribute(pDoc, "//reqif-xhtml:p"_ostr, "align"_ostr); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifOle1PDF) +{ + // Save to reqif-xhtml. + createSwDoc("pdf-ole.odt"); + + ExportToReqif(); + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(aRtfUrl, aOle1); + + // Check the content of the ole1 data. + OLE1Reader aOle1Reader(aOle1); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 39405 + // - Actual : 43008 + // i.e. we did not work with the Ole10Native stream, rather created an OLE1 wrapper around the + // OLE1-in-OLE2 data, resulting in additional size. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(0x99ed), aOle1Reader.m_nNativeDataSize); + + // Now import this back and check the ODT result. + mxComponent->dispose(); + mxComponent.clear(); + ImportFromReqif(maTempFile.GetURL()); + save("writer8"); + uno::Reference<packages::zip::XZipFileAccess2> xNameAccess + = packages::zip::ZipFileAccess::createWithURL(comphelper::getComponentContext(m_xSFactory), + maTempFile.GetURL()); + uno::Reference<io::XInputStream> xInputStream(xNameAccess->getByName("Object 2"), + uno::UNO_QUERY); + std::unique_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true)); + tools::SvRef<SotStorage> pStorage = new SotStorage(*pStream); + tools::SvRef<SotStorageStream> pOleNative = pStorage->OpenSotStream("\1Ole10Native"); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 39409 + // - Actual : 0 + // i.e. we didn't handle the case when the ole1 payload was not an ole2 container. Note how the + // expected value is the same as nData above + 4 bytes, since this data is length-prefixed. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(39409), pOleNative->GetSize()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifOle1Paint) +{ + // Load the bug document, which has OLE1 data in it, which is not a wrapper around OLE2 data. + ImportFromReqif(createFileURL(u"paint-ole.xhtml")); + + // Save it as ODT to inspect the result of the OLE1 -> OLE2 conversion. + save("writer8"); + uno::Reference<packages::zip::XZipFileAccess2> xNameAccess + = packages::zip::ZipFileAccess::createWithURL(comphelper::getComponentContext(m_xSFactory), + maTempFile.GetURL()); + uno::Reference<io::XInputStream> xInputStream(xNameAccess->getByName("Object 2"), + uno::UNO_QUERY); + std::unique_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true)); + tools::SvRef<SotStorage> pStorage = new SotStorage(*pStream); + // Check the clsid of the root stream of the OLE2 storage. + SvGlobalName aActual = pStorage->GetClassName(); + SvGlobalName aExpected(0x0003000A, 0, 0, 0xc0, 0, 0, 0, 0, 0, 0, 0x46); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 0003000A-0000-0000-c000-000000000046 + // - Actual : 0003000C-0000-0000-c000-000000000046 + // i.e. the "Package" clsid was used on the OLE2 storage unconditionally, even for an mspaint + // case, which has its own clsid. + CPPUNIT_ASSERT_EQUAL(aExpected.GetHexName(), aActual.GetHexName()); + + ExportToReqif(); + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(aRtfUrl, aOle1); + + // Check the content of the ole1 data. + // Skip ObjectHeader, see [MS-OLEDS] 2.2.4. + aOle1.Seek(0); + sal_uInt32 nData; + aOle1.ReadUInt32(nData); // OLEVersion + aOle1.ReadUInt32(nData); // FormatID + aOle1.ReadUInt32(nData); // ClassName + CPPUNIT_ASSERT(nData); + OString aClassName = read_uInt8s_ToOString(aOle1, nData - 1); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: PBrush + // - Actual : Package + // i.e. a hardcoded class name was written. + CPPUNIT_ASSERT_EQUAL("PBrush"_ostr, aClassName); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifOle1PaintBitmapFormat) +{ + // Given a document with a 8bpp bitmap: + createSwDoc("paint-ole-bitmap-format.odt"); + + // When exporting to reqif-xhtml with ExportImagesAsOLE enabled: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ExportImagesAsOLE", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure the resulting bitmap is 24bpp: + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(aRtfUrl, aOle1); + OLE1Reader aOle1Reader(aOle1); + Bitmap aBitmap; + SvMemoryStream aMemory; + aMemory.WriteBytes(aOle1Reader.m_aNativeData.data(), aOle1Reader.m_aNativeData.size()); + aMemory.Seek(0); + CPPUNIT_ASSERT(ReadDIB(aBitmap, aMemory, true)); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 24 + // - Actual : 8 + // i.e. it was not a pixel format ms paint could handle in OLE mode. + CPPUNIT_ASSERT_EQUAL(vcl::PixelFormat::N24_BPP, aBitmap.getPixelFormat()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testMultiParaListItem) +{ + // Create a document with 3 list items: A, B&C and D. + createSwDoc(); + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("A"); + SwDoc* pDoc = pWrtShell->GetDoc(); + { + // Enable numbering. + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + } + pWrtShell->SplitNode(); + pWrtShell->Insert("B"); + pWrtShell->SplitNode(); + pWrtShell->Insert("C"); + { + // C is in the same list item as B. + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetCountedInList(false); + } + pWrtShell->SplitNode(); + pWrtShell->Insert("D"); + + ExportToReqif(); + + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPathContent(pXmlDoc, "//reqif-xhtml:ol/reqif-xhtml:li[1]/reqif-xhtml:p"_ostr, "A"); + assertXPathContent(pXmlDoc, "//reqif-xhtml:ol/reqif-xhtml:li[2]/reqif-xhtml:p[1]"_ostr, "B"); + // Without the accompanying fix in place, this test would have failed with: + // XPath '//reqif-xhtml:ol/reqif-xhtml:li[2]/reqif-xhtml:p[2]' not found + // i.e. </li> was written before "C", not after "C", so "C" was not in the 2nd list item. + assertXPathContent(pXmlDoc, "//reqif-xhtml:ol/reqif-xhtml:li[2]/reqif-xhtml:p[2]"_ostr, "C"); + assertXPathContent(pXmlDoc, "//reqif-xhtml:ol/reqif-xhtml:li[3]/reqif-xhtml:p"_ostr, "D"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testUnderlineNone) +{ + // Create a document with a single paragraph: its underlying is set to an explicit 'none' value. + createSwDoc(); + uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY); + uno::Reference<text::XText> xText = xTextDocument->getText(); + xText->insertString(xText->getEnd(), "x", /*bAbsorb=*/false); + uno::Reference<beans::XPropertySet> xParagraph(getParagraph(1), uno::UNO_QUERY); + xParagraph->setPropertyValue("CharUnderline", uno::Any(sal_Int16(awt::FontUnderline::NONE))); + + // Export to reqif-xhtml. + ExportToReqif(); + + // Make sure that the paragraph has no explicit style, because "text-decoration: none" is + // filtered out. + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPathNoAttribute(pXmlDoc, "//reqif-xhtml:div/reqif-xhtml:p"_ostr, "style"_ostr); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifOle1PresDataNoOle2) +{ + // Save to reqif-xhtml. + createSwDoc("no-ole2-pres-data.odt"); + ExportToReqif(); + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(aRtfUrl, aOle1); + + // Check the content of the ole1 data. + // Without the accompanying fix in place, this test would have failed as there was no + // presentation data after the native data in the OLE1 container. The result was not editable in + // Word. + OLE1Reader aOle1Reader(aOle1); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifOle1PresDataWmfOnly) +{ + // Save to reqif-xhtml. + createSwDoc("ole1-pres-data-wmf.odt"); + ExportToReqif(); + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(aRtfUrl, aOle1); + + OLE1Reader aOle1Reader(aOle1); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 135660 + // - Actual : 272376 + // i.e. we wrote some additional EMF data into the WMF output, which broke Word. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(135660), aOle1Reader.m_nPresentationDataSize); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifAscharObjsize) +{ + // Given a document with an as-char anchored embedded object: + createSwDoc("reqif-aschar-objsize.odt"); + + // When exporting to reqif-xhtml: + ExportToReqif(); + + // Then make sure that the RTF snippet has the correct aspect ratio: + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aRtf; + HtmlExportTest::wrapRtfFragment(aRtfUrl, aRtf); + tools::SvRef<TestReqIfRtfReader> xReader(new TestReqIfRtfReader(aRtf)); + CPPUNIT_ASSERT(xReader->CallParser() != SvParserState::Error); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 7344 + // - Actual : 2836 + // i.e. the aspect ratio was 1:1, while the PNG aspect ratio was correctly not 1:1. + CPPUNIT_ASSERT_EQUAL(static_cast<tools::Long>(7344), xReader->GetObjw()); + CPPUNIT_ASSERT_EQUAL(static_cast<tools::Long>(4116), xReader->GetObjh()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifObjdataPresentationDataSize) +{ + // Given a document with an OLE2 embedded object, containing a preview: + createSwDoc("reqif-objdata-presentationdatasize.odt"); + + // When exporting to ReqIF: + ExportToReqif(); + + // Then make sure that the PresentationDataSize in the RTF's objdata blob is correct: + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(aRtfUrl, aOle1); + OLE1Reader aOle1Reader(aOle1); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 565994 + // - Actual : 330240 (Linux) + // - Actual : 566034 (Windows, when Word is installed) + // because PresentationData was taken from the OLE2 stream but its size was taken from RTF. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(565994), aOle1Reader.m_nPresentationDataSize); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testListHeading) +{ + // Given a document with a list heading: + createSwDoc(); + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("list header"); + SwDoc* pDoc = pWrtShell->GetDoc(); + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + + // When exporting to ReqIF: + ExportToReqif(); + + // Then make sure the output is valid xhtml: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + + // Without the accompanying fix in place, this test would have failed: + // - expected: <div><ol><li style="display: block"><p>...</p></li></ol></div> + // - actual : <div><ol><p>...</p></li></ol></div> + // because a </li> but no <li> is not well-formed and <ol> with a non-li children is invalid. + OUString aContent + = getXPathContent(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol/" + "reqif-xhtml:li[@style='display: block']/reqif-xhtml:p"_ostr); + CPPUNIT_ASSERT_EQUAL(OUString("list header"), aContent.trim()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testPartiallyNumberedList) +{ + // Given a document with a list, first para is numbered, second is not: + createSwDoc(); + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("list header"); + SwDoc* pDoc = pWrtShell->GetDoc(); + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + } + pWrtShell->Insert2("numbered"); + pWrtShell->SplitNode(); + pWrtShell->Insert2("not numbered"); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + + // When exporting to ReqIF: + ExportToReqif(); + + // Then make sure the output is well-formed xhtml: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed: + // - expected: <li><p>...</p><p>...</p></li> + // - actual : <li><p>...</p><p>...</p> + // because a <li> without a matching </li> is not well-formed, and the </li> was omitted because + // the second para was not numbered. + assertXPath( + pXmlDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol/reqif-xhtml:li/reqif-xhtml:p"_ostr, 2); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testPartiallyNumberedListHTML) +{ + // Given a document with a list, first para is numbered, second is not: + createSwDoc(); + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("list header"); + SwDoc* pDoc = pWrtShell->GetDoc(); + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->nNode.GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + } + pWrtShell->Insert2("numbered"); + pWrtShell->SplitNode(); + pWrtShell->Insert2("not numbered"); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->nNode.GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + + // When exporting to HTML: + ExportToHTML(); + + SvMemoryStream aStream; + WrapFromTempFile(aStream); + xmlDocUniquePtr pXmlDoc = parseXmlStream(&aStream); + CPPUNIT_ASSERT(pXmlDoc); // if we have missing closing marks - parse error + + // Without the accompanying fix in place, this test would have failed: + // - expected: <li><p>...</p><p>...</p></li> + // - actual : <li><p>...</p><p>...</p> + // because a <li> without a matching </li> is not well-formed, and the </li> was omitted because + // the second para was not numbered. + + assertXPath(pXmlDoc, "/html/body/ol/li/p"_ostr, 2); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testListHeaderAndItem) +{ + // Given a document with a list, first para is not numbered, but the second is: + createSwDoc(); + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("not numbered"); + SwDoc* pDoc = pWrtShell->GetDoc(); + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + pWrtShell->SplitNode(); + pWrtShell->Insert2("numbered"); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + } + + // When exporting to ReqIF: + ExportToReqif(); + + // Then make sure the output is well-formed xhtml: + // Without the accompanying fix in place, this would have failed: + // Entity: line 3: parser error : Opening and ending tag mismatch: ol line 3 and li + // <reqif-xhtml:ol><reqif-xhtml:p>not numbered</reqif-xhtml:p></reqif-xhtml:li> + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Make sure that in case the list has a header and an item, then both are wrapped in an <li> + // element. + assertXPath( + pXmlDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol/reqif-xhtml:li/reqif-xhtml:p"_ostr, 2); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testBlockQuoteNoMargin) +{ + // Given a document with some text, para style set to Quotations, no bottom margin: + createSwDoc(); + uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY); + uno::Reference<text::XText> xText = xTextDocument->getText(); + xText->insertString(xText->getEnd(), "string", /*bAbsorb=*/false); + uno::Reference<beans::XPropertySet> xQuotations( + getStyles("ParagraphStyles")->getByName("Quotations"), uno::UNO_QUERY); + xQuotations->setPropertyValue("ParaBottomMargin", uno::Any(static_cast<sal_Int32>(0))); + uno::Reference<beans::XPropertySet> xParagraph(getParagraph(1), uno::UNO_QUERY); + xParagraph->setPropertyValue("ParaStyleName", uno::Any(OUString("Quotations"))); + + // When exporting to XHTML: + ExportToReqif(); + + // Then make sure the output is valid xhtml: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed: + // - expected: <blockquote><p>...</p></blockquote> + // - actual : <blockquote>...</blockquote> + // i.e. <blockquote> is can't have character children, but it had. + assertXPathContent( + pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:blockquote/reqif-xhtml:p"_ostr, + "string"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifImageToOle) +{ + // Given a document with an image: + createSwDoc(); + uno::Sequence<beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("FileName", createFileURL(u"ole2.png")), + }; + dispatchCommand(mxComponent, ".uno:InsertGraphic", aArgs); + + // When exporting to XHTML: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ExportImagesAsOLE", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure we export that PNG as WMF in ReqIF mode: + OUString aRtfUrl = GetOlePath(); + SvMemoryStream aRtf; + HtmlExportTest::wrapRtfFragment(aRtfUrl, aRtf); + tools::SvRef<TestReqIfRtfReader> xReader(new TestReqIfRtfReader(aRtf)); + CPPUNIT_ASSERT(xReader->CallParser() != SvParserState::Error); + // Without the accompanying fix in place, this test would have failed: + // - Expected: 8 + // - Actual : 0 + // i.e. the image was exported as PNG, not as WMF (with a version). + CPPUNIT_ASSERT_EQUAL(8, xReader->getWmetafile()); + + // Make sure that the native data byte array is not empty. + SvMemoryStream aOle1; + CPPUNIT_ASSERT(xReader->WriteObjectData(aOle1)); + // Without the accompanying fix in place, this test would have failed, as aOle1 was empty. + OLE1Reader aOle1Reader(aOle1); + CPPUNIT_ASSERT(aOle1Reader.m_nNativeDataSize); + + // Make sure that the presentation data byte array is not empty. + // Without the accompanying fix in place, this test would have failed, as aOle1 only contained + // the native data. + CPPUNIT_ASSERT(aOle1Reader.m_nPresentationDataSize); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedPNGDirectly) +{ + // Given a document with an image: + createSwDoc(); + uno::Sequence<beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("FileName", createFileURL(u"ole2.png")), + }; + dispatchCommand(mxComponent, ".uno:InsertGraphic", aArgs); + + // When exporting to XHTML: + ExportToReqif(); + + // Then make sure the PNG is embedded directly, without an RTF wrapper: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: image/png + // - Actual : text/rtf + // i.e. even PNG was wrapped in an RTF. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "image/png"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedJPGDirectly) +{ + // Given a document with an image: + createSwDoc(); + uno::Sequence<beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("FileName", createFileURL(u"reqif-ole-img.jpg")), + }; + dispatchCommand(mxComponent, ".uno:InsertGraphic", aArgs); + + // When exporting to XHTML: + ExportToReqif(); + + // Then make sure the JPG is embedded directly, without an RTF wrapper: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "image/jpeg"); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: image/jpeg + // - Actual : image/png + // i.e. first the original JPG data was lost, then the inner PNG fallback was missing. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object/reqif-xhtml:object"_ostr, "type"_ostr, + "image/png"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedPNGShapeDirectly) +{ + // Given a document with an image shape: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.GraphicObjectShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(10000, 10000)); + uno::Reference<beans::XPropertySet> xShapeProps(xShape, uno::UNO_QUERY); + xShapeProps->setPropertyValue("GraphicURL", uno::Any(createFileURL(u"ole2.png"))); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + + // When exporting to XHTML: + ExportToReqif(); + + // Then make sure the PNG is embedded directly, without an RTF wrapper: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - no attribute 'type' exist + // i.e. the PNG was exported as GIF, without an explicit type. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "image/png"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedJPGShapeDirectly) +{ + // Given a document with an image: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.GraphicObjectShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(10000, 10000)); + uno::Reference<beans::XPropertySet> xShapeProps(xShape, uno::UNO_QUERY); + xShapeProps->setPropertyValue("GraphicURL", uno::Any(createFileURL(u"reqif-ole-img.jpg"))); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + + // When exporting to XHTML: + ExportToReqif(); + + // Then make sure the JPG is embedded directly, without an RTF wrapper: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: image/jpeg + // - Actual : image/png + // i.e. first the original JPG data was lost, then the inner PNG fallback was missing. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "image/jpeg"); + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object/reqif-xhtml:object"_ostr, "type"_ostr, + "image/png"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedPNGShapeAsOLE) +{ + // Given a document with an image shape: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.GraphicObjectShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(10000, 10000)); + uno::Reference<beans::XPropertySet> xShapeProps(xShape, uno::UNO_QUERY); + xShapeProps->setPropertyValue("GraphicURL", uno::Any(createFileURL(u"ole2.png"))); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + + // When exporting to XHTML: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ExportImagesAsOLE", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure the PNG is embedded with an RTF wrapper: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: text/rtf + // - Actual : image/png + // i.e. the OLE wrapper around the PNG was missing. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "text/rtf"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedShapeAsPNG) +{ + // Given a document with a shape: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.RectangleShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(10000, 10000)); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + + // When exporting to XHTML: + ExportToReqif(); + + // Then make sure the shape is embedded as a PNG: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: image/png + // - Actual : image/x-vclgraphic + // i.e. the result was invalid ReqIF. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "image/png"); + + // Then check the pixel size of the shape: + Size aPixelSize(Application::GetDefaultDevice()->LogicToPixel(Size(10000, 10000), + MapMode(MapUnit::Map100thMM))); + // Without the accompanying fix in place, this test would have failed with: + // - no attribute 'width' exist + // i.e. shapes had no width. + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "width"_ostr, + OUString::number(aPixelSize.getWidth())); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testShapeAsImageHtml) +{ + // Given a document with a shape: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.RectangleShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(5080, 2540)); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + + // When exporting to plain HTML: + saveAndReload("HTML (StarWriter)"); + + // Without the accompanying fix in place, this test would have failed with: + // - Expected: + // - Actual : /> + // i.e. the output was not well-formed. + CPPUNIT_ASSERT_EQUAL(OUString(" "), getParagraph(1)->getString()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testJson) +{ + // Given a document with a shape: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.RectangleShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(2540, 2540)); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + + // When exporting to HTML, and specifying options as JSON: + setFilterOptions("{\"XhtmlNs\":{\"type\":\"string\", \"value\":\"reqif-xhtml\"}," + "\"ShapeDPI\":{\"type\":\"long\",\"value\":\"192\"}}"); + save("HTML (StarWriter)"); + + // Then make sure those options are not ignored: + // Without the accompanying fix in place, this test would have failed, as GetPngPath() expects + // XML output, but xhtmlns=reqif-xhtml was ignored. + OUString aPngUrl = GetPngPath(); + SvFileStream aFileStream(aPngUrl, StreamMode::READ); + GraphicDescriptor aDescriptor(aFileStream, nullptr); + aDescriptor.Detect(/*bExtendedInfo=*/true); + // Make sure that the increased DPI is taken into account: + tools::Long nExpected = 192; + CPPUNIT_ASSERT_EQUAL(nExpected, aDescriptor.GetSizePixel().getWidth()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifEmbedShapeAsPNGCustomDPI) +{ + // Given a document with a shape: + createSwDoc(); + uno::Reference<css::lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XShape> xShape( + xFactory->createInstance("com.sun.star.drawing.RectangleShape"), uno::UNO_QUERY); + xShape->setSize(awt::Size(5080, 2540)); + uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY); + xDrawPageSupplier->getDrawPage()->add(xShape); + sal_Int32 nDPI = 600; + + // When exporting to XHTML: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ShapeDPI", nDPI), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure the shape is embedded as a PNG: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "type"_ostr, "image/png"); + + // Then check the pixel size of the shape: + Size aPixelSize(Application::GetDefaultDevice()->LogicToPixel(Size(5080, 2540), + MapMode(MapUnit::Map100thMM))); + tools::Long nPNGWidth = 1200; + OUString aPngUrl = GetPngPath(); + SvFileStream aFileStream(aPngUrl, StreamMode::READ); + GraphicDescriptor aDescriptor(aFileStream, nullptr); + aDescriptor.Detect(/*bExtendedInfo=*/true); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1200 + // - Actual : 1000 + // i.e. first setting a double DPI didn't result in larger pixel width of the PNG, then it was + // limited to 1000 pixels (because the pixel limit was 500k). + CPPUNIT_ASSERT_EQUAL(nPNGWidth, aDescriptor.GetSizePixel().getWidth()); + + // Then make sure the shape's logic size (in CSS pixels) don't change: + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object"_ostr, "width"_ostr, + OUString::number(aPixelSize.getWidth())); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqifOleBmpTransparent) +{ + // Given a document with a transparent image: + createSwDoc(); + uno::Sequence<beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("FileName", createFileURL(u"transparent.png")), + }; + dispatchCommand(mxComponent, ".uno:InsertGraphic", aArgs); + + // When exporting to reqif with ExportImagesAsOLE=true: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("ExportImagesAsOLE", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure the transparent pixel turns into white: + SvMemoryStream aOle1; + ParseOle1FromRtfUrl(GetOlePath(), aOle1); + OLE1Reader aOle1Reader(aOle1); + SvMemoryStream aBitmapStream(aOle1Reader.m_aNativeData.data(), aOle1Reader.m_aNativeData.size(), + StreamMode::READ); + Bitmap aBitmap; + ReadDIB(aBitmap, aBitmapStream, /*bFileHeader=*/true); + Size aBitmapSize = aBitmap.GetSizePixel(); + BitmapEx aBitmapEx(aBitmap); + Color nActualColor + = aBitmapEx.GetPixelColor(aBitmapSize.getWidth() - 1, aBitmapSize.getHeight() - 1); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: Color: R:255 G:255 B:255 A:0 + // - Actual : Color: R:0 G:0 B:0 A:0 + // i.e. the bitmap without an alpha channel was black, not white. + CPPUNIT_ASSERT_EQUAL(COL_WHITE, nActualColor); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testListsHeading) +{ + // Given a document with lh, lh, li, li, lh and lh nodes: + createSwDoc(); + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("list 1, header 1"); + pWrtShell->SplitNode(); + pWrtShell->Insert("list 1, header 2"); + pWrtShell->SplitNode(); + pWrtShell->Insert("list 2, item 1"); + pWrtShell->SplitNode(); + pWrtShell->Insert("list 2, item 2"); + pWrtShell->SplitNode(); + pWrtShell->Insert("list 3, header 1"); + pWrtShell->SplitNode(); + pWrtShell->Insert("list 3, header 2"); + SwDoc* pDoc = pWrtShell->GetDoc(); + pWrtShell->Up(false, 5); + { + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + pWrtShell->Down(false, 1); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + } + pWrtShell->Down(false, 1); + { + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + } + pWrtShell->Down(false, 1); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + } + } + pWrtShell->Down(false, 1); + { + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + pWrtShell->Down(false, 1); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetCountedInList(false); + } + } + + // When exporting to ReqIF: + ExportToReqif(); + + // Then make sure the output is valid xhtml: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + + OUString aContent + = getXPathContent(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol/" + "reqif-xhtml:li[@style='display: block']/reqif-xhtml:p"_ostr); + CPPUNIT_ASSERT_EQUAL(OUString("list 1, header 1"), aContent.trim()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testOleEmfPreviewToHtml) +{ + // Given a document containing an embedded object, with EMF preview: + createSwDoc("ole2.odt"); + + // When exporting to HTML: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure the <img> tag has matching file extension and data: + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + OUString aPath = getXPath(pDoc, "/html/body/p/img"_ostr, "src"_ostr); + // Without the accompanying fix in place, this test would have failed, as aPath was + // ole_html_3978e5f373402b43.JPG, with EMF data. + CPPUNIT_ASSERT(aPath.endsWith("gif")); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testNestedBullets) +{ + // Given a documented with nested lists: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("first"); + sal_uInt16 nPos = pDoc->MakeNumRule(pDoc->GetUniqueNumRuleName()); + SwNumRule* pNumRule = pDoc->GetNumRuleTable()[nPos]; + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetAttrListLevel(0); + } + pWrtShell->SplitNode(); + pWrtShell->Insert("second"); + { + SwNode& rNode = pWrtShell->GetCursor()->GetPoint()->GetNode(); + SwTextNode& rTextNode = *rNode.GetTextNode(); + rTextNode.SetAttr(SwNumRuleItem(pNumRule->GetName())); + rTextNode.SetAttrListLevel(1); + } + + // When exporting to xhtml: + ExportToReqif(); + + // Then make sure that there is a <li> between the outer and the inner <ol>: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - XPath '//reqif-xhtml:ol/reqif-xhtml:li/reqif-xhtml:ol/reqif-xhtml:li/reqif-xhtml:p' not found + // i.e. the <li> inside the outer <ol> was missing. + assertXPathContent( + pXmlDoc, "//reqif-xhtml:ol/reqif-xhtml:li/reqif-xhtml:ol/reqif-xhtml:li/reqif-xhtml:p"_ostr, + "second"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTrailingLineBreak) +{ + // Given a document with a trailing line-break: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("test\n"); + + // When exporting to reqif-xhtml: + ExportToReqif(); + + // Then make sure that we still have a single line-break: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 2 + // - XPath '//reqif-xhtml:br' number of nodes is incorrect + assertXPath(pXmlDoc, "//reqif-xhtml:br"_ostr, 1); + + // Then test the import side: + + // Given an empty document: + mxComponent->dispose(); + + // When importing a <br> from reqif-xhtml: + ImportFromReqif(maTempFile.GetURL()); + + // Then make sure that line-break is not lost: + SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + CPPUNIT_ASSERT(pTextDoc); + pDoc = pTextDoc->GetDocShell()->GetDoc(); + pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + OUString aActual = pWrtShell->GetCursor()->GetPointNode().GetTextNode()->GetText(); + // Without the accompanying fix in place, this test would have failed, as the trailing + // line-break was lost. + CPPUNIT_ASSERT_EQUAL(OUString("test\n"), aActual); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testLeadingTab) +{ + // Given a document with leading tabs: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("\t first"); + pWrtShell->SplitNode(); + pWrtShell->Insert("\t\t second"); + pWrtShell->SplitNode(); + pWrtShell->Insert("thi \t rd"); + + // When exporting to HTML, using LeadingTabWidth=2: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("LeadingTabWidth", static_cast<sal_Int32>(2)), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure that leading tabs are replaced with 2 nbsps: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: <nbsp><nbsp><space>first + // - Actual : <tab><space>first + // i.e. the leading tab was not replaced by 2 nbsps. + assertXPathContent(pXmlDoc, "//reqif-xhtml:p[1]"_ostr, u"\xa0\xa0 first"_ustr); + // Test a leading tab that is not at the start of the paragraph: + assertXPathContent(pXmlDoc, "//reqif-xhtml:p[2]"_ostr, u"\xa0\xa0\xa0\xa0 second"_ustr); + // Test a tab which is not leading: + assertXPathContent(pXmlDoc, "//reqif-xhtml:p[3]"_ostr, u"thi \t rd"_ustr); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testLeadingTabHTML) +{ + // Given a document with leading tabs: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("\t test"); + + // When exporting to plain HTML, using LeadingTabWidth=2: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("LeadingTabWidth", static_cast<sal_Int32>(2)), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure that leading tabs are replaced with 2 nbsps: + htmlDocUniquePtr pHtmlDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pHtmlDoc); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: <newline><nbsp><nbsp><space>test + // - Actual : <newline><tab><space>test + // i.e. the leading tab was not replaced by 2 nbsps. + assertXPathContent(pHtmlDoc, "/html/body/p"_ostr, SAL_NEWLINE_STRING u"\xa0\xa0 test"_ustr); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testClearingBreak) +{ + auto verify = [this]() { + uno::Reference<container::XEnumerationAccess> xParagraph(getParagraph(1), uno::UNO_QUERY); + uno::Reference<container::XEnumeration> xPortions = xParagraph->createEnumeration(); + uno::Reference<beans::XPropertySet> xPortion; + OUString aPortionType; + while (true) + { + // Ignore leading comments. + xPortion.set(xPortions->nextElement(), uno::UNO_QUERY); + xPortion->getPropertyValue("TextPortionType") >>= aPortionType; + if (aPortionType != "Annotation") + { + break; + } + } + // Skip "foo". + // Without the accompanying fix in place, this test would have failed with: + // An uncaught exception of type com.sun.star.container.NoSuchElementException + // i.e. the first para was just comments + text portion, the clearing break was lost. + xPortion.set(xPortions->nextElement(), uno::UNO_QUERY); + xPortion->getPropertyValue("TextPortionType") >>= aPortionType; + CPPUNIT_ASSERT_EQUAL(OUString("LineBreak"), aPortionType); + uno::Reference<text::XTextContent> xLineBreak; + xPortion->getPropertyValue("LineBreak") >>= xLineBreak; + sal_Int16 eClear{}; + uno::Reference<beans::XPropertySet> xLineBreakProps(xLineBreak, uno::UNO_QUERY); + xLineBreakProps->getPropertyValue("Clear") >>= eClear; + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(SwLineBreakClear::ALL), eClear); + }; + + // Given a document with an at-para anchored image + a clearing break: + // When loading that file: + createSwWebDoc("clearing-break.html"); + // Then make sure that the clear property of the break is not ignored: + verify(); + saveAndReload("HTML (StarWriter)"); + // Make sure that the clear property of the break is not ignored during export: + verify(); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTableBackground) +{ + // Given a document with two tables: first stable has a background, second table has a + // background in its first row: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + SwInsertTableOptions aInsertTableOptions(SwInsertTableFlags::DefaultBorder, + /*nRowsToRepeat=*/0); + pWrtShell->InsertTable(aInsertTableOptions, /*nRows=*/1, /*nCols=*/1); + pWrtShell->MoveTable(GotoPrevTable, fnTableStart); + SvxBrushItem aBrush(Color(0xff0000), RES_BACKGROUND); + pWrtShell->SetTabBackground(aBrush); + pWrtShell->Down(/*bSelect=*/false); + pWrtShell->SplitNode(); + pWrtShell->InsertTable(aInsertTableOptions, /*nRows=*/2, /*nCols=*/1); + pWrtShell->MoveTable(GotoPrevTable, fnTableStart); + aBrush.SetColor(0x00ff00); + pWrtShell->SetRowBackground(aBrush); + pWrtShell->Down(/*bSelect=*/false); + // Second row has an explicit transparent background. + aBrush.SetColor(COL_TRANSPARENT); + pWrtShell->SetRowBackground(aBrush); + + // When exporting to reqif-xhtml: + ExportToReqif(); + + // Then make sure that CSS markup is used, not HTML one: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - XPath '//reqif-xhtml:table[1]' no attribute 'style' exist + // i.e. HTML markup was used for the table background color. + assertXPath(pXmlDoc, "//reqif-xhtml:table[1]"_ostr, "style"_ostr, "background: #ff0000"); + assertXPathNoAttribute(pXmlDoc, "//reqif-xhtml:table[1]"_ostr, "bgcolor"_ostr); + assertXPath(pXmlDoc, "//reqif-xhtml:table[2]/reqif-xhtml:tr[1]"_ostr, "style"_ostr, + "background: #00ff00"); + assertXPathNoAttribute(pXmlDoc, "//reqif-xhtml:table[2]/reqif-xhtml:tr[1]"_ostr, + "bgcolor"_ostr); + // Second row has no explicit style, the default is not written. + assertXPathNoAttribute(pXmlDoc, "//reqif-xhtml:table[2]/reqif-xhtml:tr[2]"_ostr, "style"_ostr); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testImageKeepRatio) +{ + // Given a document with an image: width is relative, height is "keep ratio": + createSwDoc(); + uno::Reference<lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY); + uno::Reference<beans::XPropertySet> xTextGraphic( + xFactory->createInstance("com.sun.star.text.TextGraphicObject"), uno::UNO_QUERY); + xTextGraphic->setPropertyValue("AnchorType", + uno::Any(text::TextContentAnchorType_AS_CHARACTER)); + xTextGraphic->setPropertyValue("RelativeWidth", uno::Any(static_cast<sal_Int16>(42))); + xTextGraphic->setPropertyValue("IsSyncHeightToWidth", uno::Any(true)); + uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY); + uno::Reference<text::XText> xBodyText = xTextDocument->getText(); + uno::Reference<text::XTextCursor> xCursor(xBodyText->createTextCursor()); + uno::Reference<text::XTextContent> xTextContent(xTextGraphic, uno::UNO_QUERY); + xBodyText->insertTextContent(xCursor, xTextContent, false); + + // When exporting to HTML: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Then make sure that the width is not a fixed size, that would break on resizing the browser + // window: + htmlDocUniquePtr pDoc = parseHtml(maTempFile); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: auto + // - Actual : 2 + // i.e. a static (CSS pixel) height was written. + assertXPath(pDoc, "/html/body/p/img"_ostr, "height"_ostr, "auto"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testSectionDir) +{ + // Given a document with a section: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("test"); + pWrtShell->SelAll(); + SwSectionData aSectionData(SectionType::Content, "mysect"); + pWrtShell->InsertSection(aSectionData); + + // When exporting to (reqif-)xhtml: + ExportToReqif(); + + // Then make sure CSS is used to export the text direction of the section: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + // Without the accompanying fix in place, this test would have failed with: + // - XPath '//reqif-xhtml:div[@id='mysect']' no attribute 'style' exist + // i.e. the dir="ltr" HTML attribute was used instead. + assertXPath(pXmlDoc, "//reqif-xhtml:div[@id='mysect']"_ostr, "style"_ostr, "dir: ltr"); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testTdf114769) +{ + // Create document from scratch since relative urls to filesystem can be replaced + // by absolute during save/load + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("Hyperlink1"); + pWrtShell->SplitNode(); + pWrtShell->Insert("Hyperlink2"); + pWrtShell->SplitNode(); + pWrtShell->Insert("Hyperlink3"); + pWrtShell->SplitNode(); + pWrtShell->Insert("Hyperlink4"); + pWrtShell->SplitNode(); + pWrtShell->Insert("Hyperlink5"); + pWrtShell->SplitNode(); + + // Normal external URL + uno::Reference<beans::XPropertySet> xRun(getRun(getParagraph(1), 1), uno::UNO_QUERY); + xRun->setPropertyValue("HyperLinkURL", uno::Any(OUString("http://libreoffice.org/"))); + + // Bookmark reference + xRun.set(getRun(getParagraph(2), 1), uno::UNO_QUERY); + xRun->setPropertyValue("HyperLinkURL", uno::Any(OUString("#some_bookmark"))); + + // Filesystem absolute link + xRun.set(getRun(getParagraph(3), 1), uno::UNO_QUERY); + xRun->setPropertyValue("HyperLinkURL", uno::Any(OUString("C:\\test.txt"))); + + // Filesystem relative link + xRun.set(getRun(getParagraph(4), 1), uno::UNO_QUERY); + xRun->setPropertyValue("HyperLinkURL", uno::Any(OUString("..\\..\\test.odt"))); + + // Filesystem relative link + xRun.set(getRun(getParagraph(5), 1), uno::UNO_QUERY); + xRun->setPropertyValue("HyperLinkURL", uno::Any(OUString(".\\another.odt"))); + + // Export + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY); + uno::Sequence<beans::PropertyValue> aStoreProperties + = { comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")) }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + htmlDocUniquePtr pHtmlDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pHtmlDoc); + + CPPUNIT_ASSERT_EQUAL(OUString("http://libreoffice.org/"), + getXPath(pHtmlDoc, "/html/body/p[1]/a"_ostr, "href"_ostr)); + CPPUNIT_ASSERT_EQUAL(OUString("#some_bookmark"), + getXPath(pHtmlDoc, "/html/body/p[2]/a"_ostr, "href"_ostr)); + CPPUNIT_ASSERT_EQUAL(OUString("C:\\test.txt"), + getXPath(pHtmlDoc, "/html/body/p[3]/a"_ostr, "href"_ostr)); + CPPUNIT_ASSERT_EQUAL(OUString("..\\..\\test.odt"), + getXPath(pHtmlDoc, "/html/body/p[4]/a"_ostr, "href"_ostr)); + CPPUNIT_ASSERT_EQUAL(OUString(".\\another.odt"), + getXPath(pHtmlDoc, "/html/body/p[5]/a"_ostr, "href"_ostr)); +} + +CPPUNIT_TEST_FIXTURE(HtmlExportTest, testTdf153923) +{ + createSwDoc("TableWithIndent.fodt"); + save("HTML (StarWriter)"); + + // Parse it as XML (strict!) + xmlDocUniquePtr pDoc = parseXml(maTempFile); + // Without the fix in place, this would fail + CPPUNIT_ASSERT(pDoc); + + assertXPath(pDoc, "/html/body//dl"_ostr, 3); + // The 'dd' tag was not closed + assertXPath(pDoc, "/html/body//dd"_ostr, 3); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTdf153923_ReqIF) +{ + createSwDoc("TableWithIndent.fodt"); + ExportToReqif(); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + assertXPath(pDoc, "//reqif-xhtml:table"_ostr); + // There should be no 'dd' or 'dl' tags, used as a hack for table indentation + assertXPath(pDoc, "//reqif-xhtml:dl"_ostr, 0); + assertXPath(pDoc, "//reqif-xhtml:dd"_ostr, 0); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIfTransparentTifImg) +{ + // reqIf export must keep the TIF encoding of the image + createSwDoc("reqif-transparent-tif-img.odt"); + ExportToReqif(); + + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object[1]"_ostr, "type"_ostr, "image/tiff"); + OUString imageName + = getXPath(pXmlDoc, "//reqif-xhtml:p/reqif-xhtml:object[1]"_ostr, "data"_ostr); + // Without the accompanying fix in place, this test would have failed, + // ending with .gif, because XOutFlags::UseGifIfSensible flag combined + // with the transparent image would result in GIF export + CPPUNIT_ASSERT(imageName.endsWith(".tif")); + + INetURLObject aURL(maTempFile.GetURL()); + aURL.setName(imageName); + GraphicDescriptor aDescriptor(aURL); + aDescriptor.Detect(); + CPPUNIT_ASSERT_EQUAL(GraphicFileFormat::TIF, aDescriptor.GetFileFormat()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTdf155387) +{ + createSwDoc("sub_li_and_ctd.fodt"); + ExportToReqif(); + + // Without the fix in place, this would fail + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Single top-level list + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul"_ostr); + // Single top-level item + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul/reqif-xhtml:li"_ostr); + // 4 top-level paragraphs in the item + assertXPath( + pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul/reqif-xhtml:li/reqif-xhtml:p"_ostr, + 4); + // 2 sublists in the item + assertXPath( + pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul/reqif-xhtml:li/reqif-xhtml:ul"_ostr, + 2); + // 2 items in the first sublist + assertXPath(pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul/reqif-xhtml:li/reqif-xhtml:ul[1]/" + "reqif-xhtml:li"_ostr, + 2); + // Check the last (most nested) subitem's text + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul/reqif-xhtml:li/reqif-xhtml:ul[2]/" + "reqif-xhtml:li/reqif-xhtml:ul/reqif-xhtml:li/reqif-xhtml:p"_ostr, + "l3"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTdf155496) +{ + createSwDoc("listItemSubheader.fodt"); + ExportToReqif(); + + // Without the fix in place, this would fail + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Two top-level lists + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul"_ostr, 2); + // Single top-level item + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul[1]/reqif-xhtml:li"_ostr); + // One top-level paragraph in the item + assertXPath( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul[1]/reqif-xhtml:li/reqif-xhtml:p"_ostr); + // One sublist in the item + assertXPath( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul[1]/reqif-xhtml:li/reqif-xhtml:ul"_ostr); + // One item in the sublist + assertXPath(pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul[1]/reqif-xhtml:li/reqif-xhtml:ul/" + "reqif-xhtml:li"_ostr); + // Check its text + OUString aContent = getXPathContent( + pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ul[1]/reqif-xhtml:li/reqif-xhtml:ul/" + "reqif-xhtml:li/reqif-xhtml:p"_ostr); + CPPUNIT_ASSERT_EQUAL(OUString("list 1 item 1\n\t\tsub-header"), aContent.trim()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_RightAlignedTable) +{ + createSwDoc("tableRight.fodt"); + ExportToReqif(); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // No 'align' attribute must be present in 'div' + assertXPathNoAttribute(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:div"_ostr, + "align"_ostr); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_ListsWithNumFormat) +{ + createSwDoc("listsWithNumFormat.fodt"); + ExportToReqif(); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // No 'type' attribute must be present in 'ol' + assertXPathNoAttribute(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol[1]"_ostr, + "type"_ostr); + assertXPathNoAttribute(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol[2]"_ostr, + "type"_ostr); + assertXPathNoAttribute(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol[3]"_ostr, + "type"_ostr); + assertXPathNoAttribute(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:ol[4]"_ostr, + "type"_ostr); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTdf155871) +{ + createSwDoc("tdf155871.fodt"); + ExportToReqif(); + + // Without the fix in place, this would fail + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_ListsNoStartAttribute) +{ + createSwDoc("twoListsWithSameStyle.fodt"); + ExportToReqif(); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // No 'start' attribute must be present in 'ol' + assertXPath(pDoc, "//reqif-xhtml:ol[@start]"_ostr, 0); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_FrameTextAsObjectAltText) +{ + createSwDoc("frameWithText.fodt"); + ExportToReqif(); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // Without the fix, this would fail with + // - Expected: Some text in frame & <foo> + // - Actual : Frame1 + // i.e., frame name was used as the object element content, not frame text + assertXPathContent(pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[2]/reqif-xhtml:object"_ostr, + "Some text in frame & <foo>"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testSingleOleExport) +{ + // Given a document containing an embedded OLE object: + createSwDoc("ole2.odt"); + + // Create a selection for that object: + auto xDrawPageSupplier(mxComponent.queryThrow<css::drawing::XDrawPageSupplier>()); + auto xDrawPage(xDrawPageSupplier->getDrawPage()); + auto xModel(mxComponent.queryThrow<css::frame::XModel>()); + auto xController(xModel->getCurrentController().queryThrow<css::view::XSelectionSupplier>()); + xController->select(xDrawPage->getByIndex(0)); + + // Store only the selection + auto xStorable(mxComponent.queryThrow<css::frame::XStorable>()); + css::uno::Sequence<css::beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue("FilterName", OUString("HTML (StarWriter)")), + comphelper::makePropertyValue("FilterOptions", OUString("xhtmlns=reqif-xhtml")), + comphelper::makePropertyValue("RTFOLEMimeType", OUString("text/rtf")), + comphelper::makePropertyValue("SelectionOnly", true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + + // The root element must be reqif-xhtml:object + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:object"_ostr, "type"_ostr, "text/rtf"); + // It has no children + assertXPathChildren(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:object"_ostr, 0); + // And the content is empty + assertXPathContent(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:object"_ostr, ""); + + OUString aRtfData = getXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:object"_ostr, "data"_ostr); + INetURLObject aUrl(maTempFile.GetURL()); + aUrl.setName(aRtfData); + SvMemoryStream aRtf; + HtmlExportTest::wrapRtfFragment(aUrl.GetMainURL(INetURLObject::DecodeMechanism::NONE), aRtf); + tools::SvRef<TestReqIfRtfReader> xReader(new TestReqIfRtfReader(aRtf)); + // The RTF OLE exports correctly + CPPUNIT_ASSERT(xReader->CallParser() != SvParserState::Error); + CPPUNIT_ASSERT_EQUAL(tools::Long(9358), xReader->GetObjw()); + CPPUNIT_ASSERT_EQUAL(tools::Long(450), xReader->GetObjh()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_Tdf156602) +{ + createSwDoc("NestingInA1.fodt"); + ExportToReqif(); + + xmlDocUniquePtr pDoc = WrapReqifFromTempFile(); + + // The outer table must be kept in the document where the outer table is the first element, + // and its A1 starts with a nested table + + // Only two sub-elements must be inside the div: an outer table and a trailing paragraph + assertXPathChildren(pDoc, "/reqif-xhtml:html/reqif-xhtml:div"_ostr, 2); + // The outer table must have exactly two rows + assertXPath(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr"_ostr, 2); + // First outer table cell must have two sub-elements: an inner table and a trailing paragraph + assertXPathChildren( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]"_ostr, + 2); + // The inner table must have exactly two rows + assertXPath( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/" + "reqif-xhtml:table/reqif-xhtml:tr"_ostr, + 2); + // Check all the elements' content + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/" + "reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/reqif-xhtml:p"_ostr, + "Inner.A1"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/" + "reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[2]/reqif-xhtml:p"_ostr, + "Inner.B1"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/" + "reqif-xhtml:table/reqif-xhtml:tr[2]/reqif-xhtml:td[1]/reqif-xhtml:p"_ostr, + "Inner.A2"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/" + "reqif-xhtml:table/reqif-xhtml:tr[2]/reqif-xhtml:td[2]/reqif-xhtml:p"_ostr, + "Inner.B2"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[1]/" + "reqif-xhtml:p"_ostr, + "Outer.A1"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[1]/reqif-xhtml:td[2]/" + "reqif-xhtml:p"_ostr, + "Outer.B1"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[2]/reqif-xhtml:td[1]/" + "reqif-xhtml:p"_ostr, + "Outer.A2"); + assertXPathContent( + pDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr[2]/reqif-xhtml:td[2]/" + "reqif-xhtml:p"_ostr, + "Outer.B2"); + assertXPathContent(pDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p"_ostr, + "Following text"); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTdf156647_CellPaddingRoundtrip) +{ + // Given a document with a table with cell padding: + createSwDoc("table_cell_padding.fodt"); + { + auto xTable = getParagraphOrTable(1); + auto aTableBorder = getProperty<css::table::TableBorder2>(xTable, "TableBorder2"); + CPPUNIT_ASSERT_EQUAL(sal_Int16(1270), aTableBorder.Distance); + CPPUNIT_ASSERT(aTableBorder.IsDistanceValid); + } + // When exporting to reqif-xhtml: + ExportToReqif(); + // Make sure that we export it: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "//reqif-xhtml:table"_ostr, "cellpadding"_ostr, "48"); // px + // Now import it + mxComponent->dispose(); + ImportFromReqif(maTempFile.GetURL()); + // Then make sure that padding is not lost: + { + auto xTable = getParagraphOrTable(1); + auto aTableBorder = getProperty<css::table::TableBorder2>(xTable, "TableBorder2"); + // Without the accompanying fix in place, this test would have failed: + // - Expected: 1270 + // - Actual : 97 + // as the padding was lost, and the default 55 twip padding was used. + CPPUNIT_ASSERT_EQUAL(sal_Int16(1270), aTableBorder.Distance); + CPPUNIT_ASSERT(aTableBorder.IsDistanceValid); + } +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testTdf157643_WideHBorder) +{ + // Given a document with a table with a wide border between its two rows: + createSwDoc("table_with_wide_horizontal_border.fodt"); + // When exporting to reqif-xhtml: + ExportToReqif(); + // Make sure that there's no extra tr's: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:table/reqif-xhtml:tr"_ostr, + 2); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testHTML_PreserveSpaces) +{ + // Given a document with leading, trailing, and repeating intermediate spaces: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + constexpr OUString paraText = u"\t test \t more text \t"_ustr; + pWrtShell->Insert(paraText); + + // When exporting to plain HTML, using PreserveSpaces: + saveWithParams({ + comphelper::makePropertyValue("FilterName", u"HTML (StarWriter)"_ustr), + comphelper::makePropertyValue("PreserveSpaces", true), + }); + + // Then make sure that "white-space: pre-wrap" is written into the paragraph's style: + htmlDocUniquePtr pHtmlDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pHtmlDoc); + const OUString style = getXPath(pHtmlDoc, "/html/body/p"_ostr, "style"_ostr); + CPPUNIT_ASSERT(style.indexOf("white-space: pre-wrap") >= 0); + // Also check that the paragraph text is correct, without modifications in whitespace + assertXPathContent(pHtmlDoc, "/html/body/p"_ostr, paraText); + + // Test import + + setImportFilterName("HTML (StarWriter)"); + load(maTempFile.GetURL()); + CPPUNIT_ASSERT_EQUAL(paraText, getParagraph(1)->getString()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_PreserveSpaces) +{ + // Given a document with leading, trailing, and repeating intermediate spaces: + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + constexpr OUString paraText = u"\t test \t more text \t"_ustr; + pWrtShell->Insert(paraText); + + // When exporting to ReqIF, using PreserveSpaces: + saveWithParams({ + comphelper::makePropertyValue("FilterName", u"HTML (StarWriter)"_ustr), + comphelper::makePropertyValue("FilterOptions", u"xhtmlns=reqif-xhtml"_ustr), + comphelper::makePropertyValue("PreserveSpaces", true), + }); + + // Then make sure that xml:space="preserve" attribute exists in the paragraph element: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p"_ostr, "space"_ostr, + u"preserve"_ustr); + // Also check that the paragraph text is correct, without modifications in whitespace + assertXPathContent(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p"_ostr, paraText); + + // Test import + + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + load(maTempFile.GetURL()); + CPPUNIT_ASSERT_EQUAL(paraText, getParagraph(1)->getString()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testHTML_NoPreserveSpaces) +{ + // Test cases where "PreserveSpaces" should not introduce respective markup + + const auto assertXPath_NoWhiteSpaceInStyle + = [this](const xmlDocUniquePtr& pDoc, const OString& rXPath) { + xmlXPathObjectPtr pXmlObj = getXPathNode(pDoc, rXPath); + xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval; + CPPUNIT_ASSERT_EQUAL_MESSAGE(rXPath.getStr(), 1, xmlXPathNodeSetGetLength(pXmlNodes)); + xmlNodePtr pXmlNode = pXmlNodes->nodeTab[0]; + if (xmlChar* prop = xmlGetProp(pXmlNode, BAD_CAST("style"))) + { + OUString style = OUString::fromUtf8(reinterpret_cast<const char*>(prop)); + CPPUNIT_ASSERT_MESSAGE(rXPath.getStr(), style.indexOf("white-space:") < 0); + } + xmlXPathFreeObject(pXmlObj); + }; + const auto assertXPath_HasWhiteSpaceInStyle + = [this](const xmlDocUniquePtr& pDoc, const OString& rXPath) { + const OUString style = getXPath(pDoc, rXPath, "style"_ostr); + CPPUNIT_ASSERT_MESSAGE(rXPath.getStr(), style.indexOf("white-space: pre-wrap") >= 0); + }; + + createSwDoc("test_no_space_preserve.fodt"); + + // Export to plain HTML, using PreserveSpaces: + saveWithParams({ + comphelper::makePropertyValue("FilterName", u"HTML (StarWriter)"_ustr), + comphelper::makePropertyValue("PreserveSpaces", true), + }); + + htmlDocUniquePtr pHtmlDoc = parseHtml(maTempFile); + CPPUNIT_ASSERT(pHtmlDoc); + + // No whitespace preservation, where no leading / trailing / double whitespace + assertXPath_NoWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[1]"_ostr); + // Whitespace preserved for a leading space + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[2]"_ostr); + // Whitespace preserved for a trailing space + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[3]"_ostr); + // Whitespace preserved for a double space + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[4]"_ostr); + // No whitespace preservation for leading / trailing breaks + assertXPath_NoWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[5]"_ostr); + // Whitespace preserved for a leading break + space + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[6]"_ostr); + // Whitespace preserved for a trailing space + break + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[7]"_ostr); + // No whitespace preservation for a middle break + assertXPath_NoWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[8]"_ostr); + // Whitespace preserved for a middle space + break + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[9]"_ostr); + // Whitespace preserved for a middle break + space + assertXPath_HasWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[10]"_ostr); + // No whitespace preservation for a trailing space and SVG + assertXPath_NoWhiteSpaceInStyle(pHtmlDoc, "/html/body/p[11]"_ostr); + + // Test import + + setImportFilterName("HTML (StarWriter)"); + load(maTempFile.GetURL()); + + CPPUNIT_ASSERT_EQUAL(u"No special spaces"_ustr, getParagraph(1)->getString()); + CPPUNIT_ASSERT_EQUAL(u" Leading space"_ustr, getParagraph(2)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Trailing space "_ustr, getParagraph(3)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Double space"_ustr, getParagraph(4)->getString()); + // Trailing break is removed in SwHTMLParser::AppendTextNode, and replaced with para spacing + CPPUNIT_ASSERT_EQUAL(u"\nLeading/trailing breaks"_ustr, getParagraph(5)->getString()); + CPPUNIT_ASSERT_EQUAL(u"\n Leading break + space"_ustr, getParagraph(6)->getString()); + // Trailing break is removed in SwHTMLParser::AppendTextNode, and replaced with para spacing + CPPUNIT_ASSERT_EQUAL(u"Trailing space + break "_ustr, getParagraph(7)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Middle\nbreak"_ustr, getParagraph(8)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Middle space \n+ break"_ustr, getParagraph(9)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Middle break\n + space"_ustr, getParagraph(10)->getString()); + // The SVG is replaced by a space in SwXParagraph::getString() + CPPUNIT_ASSERT_EQUAL(u"Trailing space and SVG "_ustr, getParagraph(11)->getString()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_NoPreserveSpaces) +{ + // Test cases where "PreserveSpaces" should not introduce respective markup + + createSwDoc("test_no_space_preserve.fodt"); + + // Export to ReqIF, using PreserveSpaces: + saveWithParams({ + comphelper::makePropertyValue("FilterName", u"HTML (StarWriter)"_ustr), + comphelper::makePropertyValue("FilterOptions", u"xhtmlns=reqif-xhtml"_ustr), + comphelper::makePropertyValue("PreserveSpaces", true), + }); + + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + + // No whitespace preservation, where no leading / trailing / double whitespace + assertXPathNoAttribute(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[1]"_ostr, + "space"_ostr); + // Whitespace preserved for a leading space + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[2]"_ostr, "space"_ostr, + u"preserve"_ustr); + // Whitespace preserved for a trailing space + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[3]"_ostr, "space"_ostr, + u"preserve"_ustr); + // Whitespace preserved for a double space + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[4]"_ostr, "space"_ostr, + u"preserve"_ustr); + // No whitespace preservation for leading / trailing breaks + assertXPathNoAttribute(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[5]"_ostr, + "space"_ostr); + // Whitespace preserved for a leading break + space + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[6]"_ostr, "space"_ostr, + u"preserve"_ustr); + // No whitespace preservation for a trailing space + break + assertXPathNoAttribute(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[7]"_ostr, + "space"_ostr); + // No whitespace preservation for a middle break + assertXPathNoAttribute(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[8]"_ostr, + "space"_ostr); + // No whitespace preservation for a middle space + break + assertXPathNoAttribute(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[9]"_ostr, + "space"_ostr); + // Whitespace preserved for a middle break + space + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[10]"_ostr, "space"_ostr, + u"preserve"_ustr); + // No whitespace preservation for a trailing space and SVG + assertXPathNoAttribute(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[11]"_ostr, + "space"_ostr); + + // Test import + + setImportFilterOptions("xhtmlns=reqif-xhtml"); + setImportFilterName("HTML (StarWriter)"); + load(maTempFile.GetURL()); + + CPPUNIT_ASSERT_EQUAL(u"No special spaces"_ustr, getParagraph(1)->getString()); + CPPUNIT_ASSERT_EQUAL(u" Leading space"_ustr, getParagraph(2)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Trailing space "_ustr, getParagraph(3)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Double space"_ustr, getParagraph(4)->getString()); + CPPUNIT_ASSERT_EQUAL(u"\nLeading/trailing breaks\n"_ustr, getParagraph(5)->getString()); + CPPUNIT_ASSERT_EQUAL(u"\n Leading break + space"_ustr, getParagraph(6)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Trailing space + break \n"_ustr, getParagraph(7)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Middle\nbreak"_ustr, getParagraph(8)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Middle space \n+ break"_ustr, getParagraph(9)->getString()); + CPPUNIT_ASSERT_EQUAL(u"Middle break\n + space"_ustr, getParagraph(10)->getString()); + // The SVG is replaced by a space in SwXParagraph::getString() + CPPUNIT_ASSERT_EQUAL(u"Trailing space and SVG "_ustr, getParagraph(11)->getString()); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_ExportFormulasAsPDF) +{ + // Given a document with a formula: + createSwDoc("embedded_formula.fodt"); + + // When exporting to reqif with ExportFormulasAsPDF=true: + uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY_THROW); + uno::Sequence<beans::PropertyValue> aStoreProperties = { + comphelper::makePropertyValue(u"FilterName"_ustr, u"HTML (StarWriter)"_ustr), + comphelper::makePropertyValue(u"FilterOptions"_ustr, u"xhtmlns=reqif-xhtml"_ustr), + comphelper::makePropertyValue(u"ExportFormulasAsPDF"_ustr, true), + }; + xStorable->storeToURL(maTempFile.GetURL(), aStoreProperties); + + // Make sure that the formula is exported as PDF: + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p[2]/reqif-xhtml:object"_ostr, + "type"_ostr, u"application/pdf"_ustr); + + css::uno::Sequence<css::beans::PropertyValue> descr{ + comphelper::makePropertyValue(u"URL"_ustr, GetObjectPath(u".pdf"_ustr)), + }; + + uno::Reference<lang::XMultiServiceFactory> xFactory( + comphelper::getProcessComponentContext()->getServiceManager(), uno::UNO_QUERY_THROW); + uno::Reference<document::XTypeDetection> xTypeDetection( + xFactory->createInstance(u"com.sun.star.document.TypeDetection"_ustr), + uno::UNO_QUERY_THROW); + + CPPUNIT_ASSERT_EQUAL(u"pdf_Portable_Document_Format"_ustr, + xTypeDetection->queryTypeByDescriptor(descr, true)); +} + +CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_NoBrClearForImageWrap) +{ + // Given a document with a paragraph-anchored image with "none" wrap: + createSwDoc("image_anchored_to_paragraph_no_wrap.fodt"); + // When exporting to reqif: + ExportToReqif(); + // Make sure that there's no 'br' elements in the 'object' (used to represent the wrapping + // in HTML export, using 'clear' attribute): + xmlDocUniquePtr pXmlDoc = WrapReqifFromTempFile(); + assertXPath(pXmlDoc, "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object"_ostr); + assertXPath( + pXmlDoc, + "/reqif-xhtml:html/reqif-xhtml:div/reqif-xhtml:p/reqif-xhtml:object/reqif-xhtml:br"_ostr, + 0); +} + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |