1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 
23 
24 // MARKER(update_precomp.py): autogen include statement, do not remove
25 #include "precompiled_sdext.hxx"
26 
27 #ifdef SYSTEM_ZLIB
28 #include "zlib.h"
29 #else
30 #include <zlib/zlib.h>
31 #endif
32 
33 #include "outputwrap.hxx"
34 #include "contentsink.hxx"
35 #include "pdfihelper.hxx"
36 #include "wrapper.hxx"
37 #include "pdfparse.hxx"
38 #include "../pdfiadaptor.hxx"
39 
40 #include <rtl/math.hxx>
41 #include <osl/file.hxx>
42 #include <osl/process.h>
43 #include <gtest/gtest.h>
44 #include <cppuhelper/compbase1.hxx>
45 #include <cppuhelper/bootstrap.hxx>
46 #include <cppuhelper/basemutex.hxx>
47 #include <comphelper/sequence.hxx>
48 
49 
50 #include <com/sun/star/rendering/XCanvas.hpp>
51 #include <com/sun/star/rendering/XColorSpace.hpp>
52 #include <com/sun/star/rendering/PathJoinType.hpp>
53 #include <com/sun/star/rendering/PathCapType.hpp>
54 #include <com/sun/star/rendering/BlendMode.hpp>
55 #include <com/sun/star/lang/XMultiServiceFactory.hpp>
56 #include <com/sun/star/lang/XInitialization.hpp>
57 #include <com/sun/star/registry/XSimpleRegistry.hpp>
58 
59 #include <basegfx/matrix/b2dhommatrix.hxx>
60 #include <basegfx/tools/canvastools.hxx>
61 #include <basegfx/polygon/b2dpolygon.hxx>
62 #include <basegfx/polygon/b2dpolypolygon.hxx>
63 #include <basegfx/polygon/b2dpolypolygontools.hxx>
64 #include <basegfx/polygon/b2dpolygonclipper.hxx>
65 
66 #include <vector>
67 #include <hash_map>
68 
69 
70 using namespace ::pdfparse;
71 using namespace ::pdfi;
72 using namespace ::com::sun::star;
73 
74 namespace
75 {
76     class TestSink : public ContentSink
77     {
78     public:
79         TestSink() :
80             m_nNextFontId( 1 ),
81             m_aIdToFont(),
82             m_aFontToId(),
83             m_aGCStack(1),
84             m_aPageSize(),
85             m_aHyperlinkBounds(),
86             m_aURI(),
87             m_aTextOut(),
88             m_nNumPages(0),
89             m_bPageEnded(false),
90             m_bRedCircleSeen(false),
91             m_bGreenStrokeSeen(false),
92             m_bDashedLineSeen(false)
93         {}
94 
95         ~TestSink()
96         {
97             ASSERT_TRUE(m_aPageSize.Width == 79400 && m_aPageSize.Height == 59500) << "A4 page size (in 100th of points)";
98             ASSERT_TRUE(m_bPageEnded) << "endPage() called";
99             ASSERT_TRUE(m_nNumPages == 1) << "Num pages equal one";
100             ASSERT_TRUE(rtl::math::approxEqual(m_aHyperlinkBounds.X1,34.7 ) &&
101                                     rtl::math::approxEqual(m_aHyperlinkBounds.Y1,386.0) &&
102                                     rtl::math::approxEqual(m_aHyperlinkBounds.X2,166.7) &&
103                                     rtl::math::approxEqual(m_aHyperlinkBounds.Y2,406.2)) << "Correct hyperlink bounding box";
104             ASSERT_TRUE(m_aURI == ::rtl::OUString::createFromAscii( "http://download.openoffice.org/" )) << "Correct hyperlink URI";
105 
106             const char* sText = " \n \nThis is a testtext\nNew paragraph,\nnew line\n"
107                 "Hyperlink, this is\n?\nThis is more text\noutline mode\n?\nNew paragraph\n";
108             ::rtl::OString aTmp;
109             m_aTextOut.makeStringAndClear().convertToString( &aTmp,
110                                                              RTL_TEXTENCODING_ASCII_US,
111                                                              OUSTRING_TO_OSTRING_CVTFLAGS );
112             ASSERT_TRUE( sText == aTmp ) << "Imported text is \"This is a testtext New paragraph, new line"
113                                     " Hyperlink, this is * This is more text outline mode * New paragraph\"";
114 
115             ASSERT_TRUE(m_bRedCircleSeen) << "red circle seen in input";
116             ASSERT_TRUE(m_bGreenStrokeSeen) << "green stroke seen in input";
117             ASSERT_TRUE(m_bDashedLineSeen) << "dashed line seen in input";
118         }
119 
120     private:
121         GraphicsContext& getCurrentContext() { return m_aGCStack.back(); }
122 
123         // ContentSink interface implementation
124         virtual void setPageNum( sal_Int32 nNumPages )
125         {
126             m_nNumPages = nNumPages;
127         }
128 
129         virtual void startPage( const geometry::RealSize2D& rSize )
130         {
131             m_aPageSize = rSize;
132         }
133 
134         virtual void endPage()
135         {
136             m_bPageEnded = true;
137         }
138 
139         virtual void hyperLink( const geometry::RealRectangle2D& rBounds,
140                                 const ::rtl::OUString&             rURI )
141         {
142             m_aHyperlinkBounds = rBounds;
143             m_aURI = rURI;
144         }
145 
146         virtual void pushState()
147         {
148             m_aGCStack.push_back( m_aGCStack.back() );
149         }
150 
151         virtual void popState()
152         {
153             m_aGCStack.pop_back();
154         }
155 
156         virtual void setTransformation( const geometry::AffineMatrix2D& rMatrix )
157         {
158             basegfx::unotools::homMatrixFromAffineMatrix(
159                 getCurrentContext().Transformation,
160                 rMatrix );
161         }
162 
163         virtual void setLineDash( const uno::Sequence<double>& dashes,
164                                   double                       start )
165         {
166             GraphicsContext& rContext( getCurrentContext() );
167             if( dashes.getLength() )
168                 comphelper::sequenceToContainer(rContext.DashArray,dashes);
169             ASSERT_TRUE(start == 0.0) << "line dashing start offset";
170         }
171 
172         virtual void setFlatness( double nFlatness )
173         {
174             getCurrentContext().Flatness = nFlatness;
175         }
176 
177         virtual void setLineJoin(sal_Int8 nJoin)
178         {
179             getCurrentContext().LineJoin = nJoin;
180         }
181 
182         virtual void setLineCap(sal_Int8 nCap)
183         {
184             getCurrentContext().LineCap = nCap;
185         }
186 
187         virtual void setMiterLimit(double nVal)
188         {
189             getCurrentContext().MiterLimit = nVal;
190         }
191 
192         virtual void setLineWidth(double nVal)
193         {
194             getCurrentContext().LineWidth = nVal;
195         }
196 
197         virtual void setFillColor( const rendering::ARGBColor& rColor )
198         {
199             getCurrentContext().FillColor = rColor;
200         }
201 
202         virtual void setStrokeColor( const rendering::ARGBColor& rColor )
203         {
204             getCurrentContext().LineColor = rColor;
205         }
206 
207         virtual void setBlendMode(sal_Int8 nMode)
208         {
209             getCurrentContext().BlendMode = nMode;
210         }
211 
212         virtual void setFont( const FontAttributes& rFont )
213         {
214             FontToIdMap::const_iterator it = m_aFontToId.find( rFont );
215             if( it != m_aFontToId.end() )
216                 getCurrentContext().FontId = it->second;
217             else
218             {
219                 m_aFontToId[ rFont ] = m_nNextFontId;
220                 m_aIdToFont[ m_nNextFontId ] = rFont;
221                 getCurrentContext().FontId = m_nNextFontId;
222                 m_nNextFontId++;
223             }
224         }
225 
226         virtual void strokePath( const uno::Reference<rendering::XPolyPolygon2D>& rPath )
227         {
228             GraphicsContext& rContext( getCurrentContext() );
229             basegfx::B2DPolyPolygon aPath = basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(rPath);
230             aPath.transform( rContext.Transformation );
231 
232             if( rContext.DashArray.empty() )
233             {
234                 ASSERT_TRUE(rContext.LineColor.Alpha == 1.0 &&
235                                         rContext.LineColor.Red == 0.0 &&
236                                         rContext.LineColor.Green == 1.0 &&
237                                         rContext.LineColor.Blue == 0.0) << "Line color is green";
238 
239                 ASSERT_TRUE(rtl::math::approxEqual(rContext.LineWidth, 28.3)) << "Line width is 0";
240 
241                 const char* sExportString = "m53570 7650-35430 24100";
242                 ASSERT_TRUE(basegfx::tools::exportToSvgD( aPath, true, true, false ).compareToAscii(sExportString) == 0) << "Stroke is m535.7 518.5-354.3-241";
243 
244                 m_bGreenStrokeSeen = true;
245             }
246             else
247             {
248                 ASSERT_TRUE(rContext.DashArray.size() == 4 &&
249                                         rtl::math::approxEqual(rContext.DashArray[0],14.3764) &&
250                                         rContext.DashArray[0] == rContext.DashArray[1] &&
251                                         rContext.DashArray[1] == rContext.DashArray[2] &&
252                                         rContext.DashArray[2] == rContext.DashArray[3]) << "Dash array cons  ists of four entries";
253 
254                 ASSERT_TRUE(rContext.LineColor.Alpha == 1.0 &&
255                                         rContext.LineColor.Red == 0.0 &&
256                                         rContext.LineColor.Green == 0.0 &&
257                                         rContext.LineColor.Blue == 0.0) << "Line color is black";
258 
259                 ASSERT_TRUE(rContext.LineWidth == 0) << "Line width is 0";
260 
261                 const char* sExportString = "m49890 5670.00000000001-35430 24090";
262                 ASSERT_TRUE(basegfx::tools::exportToSvgD( aPath, true, true, false ).compareToAscii(sExportString) == 0) << "Stroke is m49890 5670.00000000001-35430 24090";
263 
264                 m_bDashedLineSeen = true;
265             }
266             ASSERT_TRUE(rContext.BlendMode == rendering::BlendMode::NORMAL) << "Blend mode is normal";
267             ASSERT_TRUE(rContext.LineJoin == rendering::PathJoinType::ROUND) << "Join type is round";
268             ASSERT_TRUE(rContext.LineCap == rendering::PathCapType::BUTT) << "Cap type is butt";
269             ASSERT_TRUE(rContext.MiterLimit == 10) << "Line miter limit is 10";
270             ASSERT_TRUE(rContext.Flatness == 1) << "Flatness is 0";
271             ASSERT_TRUE(rContext.FontId == 0) << "Font id is 0";
272         }
273 
274         virtual void fillPath( const uno::Reference<rendering::XPolyPolygon2D>& rPath )
275         {
276             GraphicsContext& rContext( getCurrentContext() );
277             basegfx::B2DPolyPolygon aPath = basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(rPath);
278             aPath.transform( rContext.Transformation );
279 
280             ASSERT_TRUE(rContext.FillColor.Alpha == 1.0 &&
281                                     rContext.FillColor.Red == 0.0 &&
282                                     rContext.FillColor.Green == 0.0 &&
283                                     rContext.FillColor.Blue == 0.0) << "Fill color is black";
284             ASSERT_TRUE(rContext.BlendMode == rendering::BlendMode::NORMAL) << "Blend mode is normal";
285             ASSERT_TRUE(rContext.Flatness == 10) << "Flatness is 10";
286             ASSERT_TRUE(rContext.FontId == 0) << "Font id is 0";
287         }
288 
289         virtual void eoFillPath( const uno::Reference<rendering::XPolyPolygon2D>& rPath )
290         {
291             GraphicsContext& rContext( getCurrentContext() );
292             basegfx::B2DPolyPolygon aPath = basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(rPath);
293             aPath.transform( rContext.Transformation );
294 
295             ASSERT_TRUE(rContext.FillColor.Alpha == 1.0 &&
296                                     rContext.FillColor.Red == 1.0 &&
297                                     rContext.FillColor.Green == 0.0 &&
298                                     rContext.FillColor.Blue == 0.0) << "Fill color is black";
299             ASSERT_TRUE(rContext.BlendMode == rendering::BlendMode::NORMAL) << "Blend mode is normal";
300             ASSERT_TRUE(rContext.Flatness == 1) << "Flatness is 0";
301             ASSERT_TRUE(rContext.FontId == 0) << "Font id is 0";
302 
303             const char* sExportString = "m12050 49610c-4310 0-7800-3490-7800-7800 0-4300 "
304                 "3490-7790 7800-7790 4300 0 7790 3490 7790 7790 0 4310-3490 7800-7790 7800z";
305             ASSERT_TRUE(basegfx::tools::exportToSvgD( aPath, true, true, false ).compareToAscii(sExportString) == 0) << "Stroke is a 4-bezier circle";
306 
307             m_bRedCircleSeen = true;
308         }
309 
310         virtual void intersectClip(const uno::Reference<rendering::XPolyPolygon2D>& rPath)
311         {
312             basegfx::B2DPolyPolygon aNewClip = basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(rPath);
313             basegfx::B2DPolyPolygon aCurClip = getCurrentContext().Clip;
314 
315             if( aCurClip.count() )  // #i92985# adapted API from (..., false, false) to (..., true, false)
316                 aNewClip = basegfx::tools::clipPolyPolygonOnPolyPolygon( aCurClip, aNewClip, true, false );
317 
318             getCurrentContext().Clip = aNewClip;
319         }
320 
321         virtual void intersectEoClip(const uno::Reference<rendering::XPolyPolygon2D>& rPath)
322         {
323             basegfx::B2DPolyPolygon aNewClip = basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(rPath);
324             basegfx::B2DPolyPolygon aCurClip = getCurrentContext().Clip;
325 
326             if( aCurClip.count() )  // #i92985# adapted API from (..., false, false) to (..., true, false)
327                 aNewClip = basegfx::tools::clipPolyPolygonOnPolyPolygon( aCurClip, aNewClip, true, false );
328 
329             getCurrentContext().Clip = aNewClip;
330         }
331 
332         virtual void drawGlyphs( const rtl::OUString&             rGlyphs,
333                                  const geometry::RealRectangle2D& /*rRect*/,
334                                  const geometry::Matrix2D&        /*rFontMatrix*/ )
335         {
336             m_aTextOut.append(rGlyphs);
337         }
338 
339         virtual void endText()
340         {
341             m_aTextOut.append( ::rtl::OUString::createFromAscii("\n") );
342         }
343 
344         virtual void drawMask(const uno::Sequence<beans::PropertyValue>& xBitmap,
345                               bool                                       /*bInvert*/ )
346         {
347             ASSERT_TRUE(xBitmap.getLength()==3) << "drawMask received two properties";
348             ASSERT_TRUE(xBitmap[0].Name.compareToAscii( "URL" ) == 0) << "drawMask got URL param";
349             ASSERT_TRUE(xBitmap[1].Name.compareToAscii( "InputStream" ) == 0) << "drawMask got InputStream param";
350         }
351 
352         virtual void drawImage(const uno::Sequence<beans::PropertyValue>& xBitmap )
353         {
354             ASSERT_TRUE(xBitmap.getLength()==3) << "drawImage received two properties";
355             ASSERT_TRUE(xBitmap[0].Name.compareToAscii( "URL" ) == 0) << "drawImage got URL param";
356             ASSERT_TRUE(xBitmap[1].Name.compareToAscii( "InputStream" ) == 0) << "drawImage got InputStream param";
357         }
358 
359         virtual void drawColorMaskedImage(const uno::Sequence<beans::PropertyValue>& xBitmap,
360                                           const uno::Sequence<uno::Any>&             /*xMaskColors*/ )
361         {
362             ASSERT_TRUE(xBitmap.getLength()==3) << "drawColorMaskedImage received two properties";
363             ASSERT_TRUE(xBitmap[0].Name.compareToAscii( "URL" ) == 0) << "drawColorMaskedImage got URL param";
364             ASSERT_TRUE(xBitmap[1].Name.compareToAscii( "InputStream" ) == 0) << "drawColorMaskedImage got InputStream param";
365         }
366 
367         virtual void drawMaskedImage(const uno::Sequence<beans::PropertyValue>& xBitmap,
368                                      const uno::Sequence<beans::PropertyValue>& xMask,
369                                      bool                                       /*bInvertMask*/)
370         {
371             ASSERT_TRUE(xBitmap.getLength()==3) << "drawMaskedImage received two properties #1";
372             ASSERT_TRUE(xBitmap[0].Name.compareToAscii( "URL" ) == 0) << "drawMaskedImage got URL param #1";
373             ASSERT_TRUE(xBitmap[1].Name.compareToAscii( "InputStream" ) == 0) << "drawMaskedImage got InputStream param #1";
374 
375             ASSERT_TRUE(xMask.getLength()==3) << "drawMaskedImage received two properties #2";
376             ASSERT_TRUE(xMask[0].Name.compareToAscii( "URL" ) == 0) << "drawMaskedImage got URL param #2";
377             ASSERT_TRUE(xMask[1].Name.compareToAscii( "InputStream" ) == 0) << "drawMaskedImage got InputStream param #2";
378         }
379 
380         virtual void drawAlphaMaskedImage(const uno::Sequence<beans::PropertyValue>& xBitmap,
381                                           const uno::Sequence<beans::PropertyValue>& xMask)
382         {
383             ASSERT_TRUE(xBitmap.getLength()==3) << "drawAlphaMaskedImage received two properties #1";
384             ASSERT_TRUE(xBitmap[0].Name.compareToAscii( "URL" ) == 0) << "drawAlphaMaskedImage got URL param #1";
385             ASSERT_TRUE(xBitmap[1].Name.compareToAscii( "InputStream" ) == 0) << "drawAlphaMaskedImage got InputStream param #1";
386 
387             ASSERT_TRUE(xMask.getLength()==3) << "drawAlphaMaskedImage received two properties #2";
388             ASSERT_TRUE(xMask[0].Name.compareToAscii( "URL" ) == 0) << "drawAlphaMaskedImage got URL param #2";
389             ASSERT_TRUE(xMask[1].Name.compareToAscii( "InputStream" ) == 0) << "drawAlphaMaskedImage got InputStream param #2";
390         }
391 
392         virtual void setTextRenderMode( sal_Int32 )
393         {
394         }
395 
396         typedef std::hash_map<sal_Int32,FontAttributes> IdToFontMap;
397         typedef std::hash_map<FontAttributes,sal_Int32,FontAttrHash> FontToIdMap;
398 
399         typedef std::hash_map<sal_Int32,GraphicsContext> IdToGCMap;
400         typedef std::hash_map<GraphicsContext,sal_Int32,GraphicsContextHash> GCToIdMap;
401 
402         typedef std::vector<GraphicsContext> GraphicsContextStack;
403 
404         sal_Int32                 m_nNextFontId;
405         IdToFontMap               m_aIdToFont;
406         FontToIdMap               m_aFontToId;
407 
408         GraphicsContextStack      m_aGCStack;
409         geometry::RealSize2D      m_aPageSize;
410         geometry::RealRectangle2D m_aHyperlinkBounds;
411         ::rtl::OUString           m_aURI;
412         ::rtl::OUStringBuffer     m_aTextOut;
413         sal_Int32                 m_nNumPages;
414         bool                      m_bPageEnded;
415         bool                      m_bRedCircleSeen;
416         bool                      m_bGreenStrokeSeen;
417         bool                      m_bDashedLineSeen;
418     };
419 
420     class PDFITest : public ::testing::Test
421     {
422     protected:
423         uno::Reference<uno::XComponentContext> mxCtx;
424         rtl::OUString                          msBaseDir;
425         bool                                   mbUnoInitialized;
426 
427     public:
428         PDFITest() : mxCtx(),msBaseDir(),mbUnoInitialized(false)
429         {}
430 
431         void SetUp()
432         {
433             if( !mbUnoInitialized )
434             {
435                 const char* pArgs( getenv("TESTS_FORWARD_STRING") );
436                 ASSERT_TRUE(pArgs) << "Test file parameter";
437 
438                 msBaseDir = rtl::OUString::createFromAscii(pArgs);
439 
440                 // bootstrap UNO
441                 try
442                 {
443                     ::rtl::OUString aIniUrl;
444                     ASSERT_TRUE(
445                         osl_getFileURLFromSystemPath(
446                             (msBaseDir+rtl::OUString::createFromAscii("pdfi_unittest_test.ini")).pData,
447                             &aIniUrl.pData ) == osl_File_E_None )
448                         << "Converting ini file to URL";
449 
450                     mxCtx = ::cppu::defaultBootstrap_InitialComponentContext(aIniUrl);
451                     ASSERT_TRUE(mxCtx.is()) << "Getting component context";
452                 }
453                 catch( uno::Exception& )
454                 {
455                     FAIL() << "Bootstrapping UNO";
456                 }
457 
458                 mbUnoInitialized = true;
459             }
460         }
461         void TearDown()
462         {
463         }
464     };
465 
466     TEST_F(PDFITest, testXPDFParser)
467     {
468         pdfi::ContentSinkSharedPtr pSink( new TestSink() );
469         pdfi::xpdf_ImportFromFile( msBaseDir + rtl::OUString::createFromAscii("pdfi_unittest_test.pdf"),
470                                    pSink,
471                                    uno::Reference< task::XInteractionHandler >(),
472                                    rtl::OUString(),
473                                    mxCtx );
474 
475         // make destruction explicit, a bunch of things are
476         // checked in the destructor
477         pSink.reset();
478     }
479 
480     TEST_F(PDFITest, testOdfDrawExport)
481     {
482         pdfi::PDFIRawAdaptor aAdaptor( mxCtx );
483         aAdaptor.setTreeVisitorFactory( createDrawTreeVisitorFactory() );
484 
485         ::rtl::OUString aURL, aAbsURL, aBaseURL;
486         osl_getFileURLFromSystemPath( (msBaseDir + rtl::OUString::createFromAscii("pdfi_unittest_draw.xml")).pData,
487                                       &aURL.pData );
488         osl_getProcessWorkingDir(&aBaseURL.pData);
489         osl_getAbsoluteFileURL(aBaseURL.pData,aURL.pData,&aAbsURL.pData);
490         ASSERT_TRUE(aAdaptor.odfConvert( msBaseDir + rtl::OUString::createFromAscii("pdfi_unittest_test.pdf"),
491                                          new OutputWrap(aAbsURL),
492                                          NULL )) << "Exporting to ODF";
493     }
494 
495     TEST_F(PDFITest, testOdfWriterExport)
496     {
497         pdfi::PDFIRawAdaptor aAdaptor( mxCtx );
498         aAdaptor.setTreeVisitorFactory( createWriterTreeVisitorFactory() );
499 
500         ::rtl::OUString aURL, aAbsURL, aBaseURL;
501         osl_getFileURLFromSystemPath( (msBaseDir + rtl::OUString::createFromAscii("pdfi_unittest_writer.xml")).pData,
502                                       &aURL.pData );
503         osl_getProcessWorkingDir(&aBaseURL.pData);
504         osl_getAbsoluteFileURL(aBaseURL.pData,aURL.pData,&aAbsURL.pData);
505         ASSERT_TRUE(aAdaptor.odfConvert( msBaseDir + rtl::OUString::createFromAscii("pdfi_unittest_test.pdf"),
506                                          new OutputWrap(aAbsURL),
507                                          NULL )) << "Exporting to ODF";
508     }
509 
510 }
511 
512 int main(int argc, char **argv)
513 {
514     ::testing::InitGoogleTest(&argc, argv);
515     return RUN_ALL_TESTS();
516 }
517