1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include <sal/config.h>
21 #include <sal/log.hxx>
22 #include <string_view>
23 
24 #include <pdfiprocessor.hxx>
25 #include <xmlemitter.hxx>
26 #include <pdfihelper.hxx>
27 #include <imagecontainer.hxx>
28 #include "style.hxx"
29 #include "writertreevisiting.hxx"
30 #include <genericelements.hxx>
31 
32 #include <basegfx/polygon/b2dpolypolygontools.hxx>
33 #include <osl/diagnose.h>
34 
35 using namespace ::com::sun::star;
36 
37 namespace pdfi
38 {
39 
40 void WriterXmlEmitter::visit( HyperlinkElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&   )
41 {
42     if( elem.Children.empty() )
43         return;
44 
45     const char* pType = dynamic_cast<DrawElement*>(elem.Children.front().get()) ? "draw:a" : "text:a";
46 
47     PropertyMap aProps;
48     aProps[ "xlink:type" ] = "simple";
49     aProps[ "xlink:href" ] = elem.URI;
50     aProps[ "office:target-frame-name" ] = "_blank";
51     aProps[ "xlink:show" ] = "new";
52 
53     m_rEmitContext.rEmitter.beginTag( pType, aProps );
54     auto this_it = elem.Children.begin();
55     while( this_it != elem.Children.end() && this_it->get() != &elem )
56     {
57         (*this_it)->visitedBy( *this, this_it );
58         ++this_it;
59     }
60     m_rEmitContext.rEmitter.endTag( pType );
61 }
62 
63 void WriterXmlEmitter::visit( TextElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&   )
64 {
65     if( elem.Text.isEmpty() )
66         return;
67 
68     PropertyMap aProps;
69     if( elem.StyleId != -1 )
70     {
71         aProps[ OUString( "text:style-name" ) ] =
72             m_rEmitContext.rStyles.getStyleName( elem.StyleId );
73     }
74 
75     m_rEmitContext.rEmitter.beginTag( "text:span", aProps );
76     m_rEmitContext.rEmitter.write( elem.Text.makeStringAndClear() );
77     auto this_it = elem.Children.begin();
78     while( this_it != elem.Children.end() && this_it->get() != &elem )
79     {
80         (*this_it)->visitedBy( *this, this_it );
81         ++this_it;
82     }
83 
84     m_rEmitContext.rEmitter.endTag( "text:span" );
85 }
86 
87 void WriterXmlEmitter::visit( ParagraphElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&   )
88 {
89     PropertyMap aProps;
90     if( elem.StyleId != -1 )
91     {
92         aProps[ "text:style-name" ] = m_rEmitContext.rStyles.getStyleName( elem.StyleId );
93     }
94     const char* pTagType = "text:p";
95     if( elem.Type == ParagraphElement::Headline )
96         pTagType = "text:h";
97     m_rEmitContext.rEmitter.beginTag( pTagType, aProps );
98 
99     auto this_it = elem.Children.begin();
100     while( this_it != elem.Children.end() && this_it->get() != &elem )
101     {
102         (*this_it)->visitedBy( *this, this_it );
103         ++this_it;
104     }
105 
106     m_rEmitContext.rEmitter.endTag( pTagType );
107 }
108 
109 void WriterXmlEmitter::fillFrameProps( DrawElement&       rElem,
110                                        PropertyMap&       rProps,
111                                        const EmitContext& rEmitContext )
112 {
113     double rel_x = rElem.x, rel_y = rElem.y;
114 
115     // find anchor type by recursing though parents
116     Element* pAnchor = &rElem;
117     ParagraphElement* pParaElt = nullptr;
118     PageElement* pPage = nullptr;
119     while ((pAnchor = pAnchor->Parent))
120     {
121         if ((pParaElt = dynamic_cast<ParagraphElement*>(pAnchor)))
122             break;
123         if ((pPage = dynamic_cast<PageElement*>(pAnchor)))
124             break;
125     }
126     if( pAnchor )
127     {
128         if (pParaElt)
129         {
130             rProps[ "text:anchor-type" ] = rElem.isCharacter
131                 ? std::u16string_view(u"character") : std::u16string_view(u"paragraph");
132         }
133         else
134         {
135             assert(pPage); // guaranteed by the while loop above
136             rProps[ "text:anchor-type" ] = "page";
137             rProps[ "text:anchor-page-number" ] = OUString::number(pPage->PageNumber);
138         }
139         rel_x -= pAnchor->x;
140         rel_y -= pAnchor->y;
141     }
142 
143     rProps[ "draw:z-index" ] = OUString::number( rElem.ZOrder );
144     rProps[ "draw:style-name"] = rEmitContext.rStyles.getStyleName( rElem.StyleId );
145     rProps[ "svg:width" ]   = convertPixelToUnitString( rElem.w );
146     rProps[ "svg:height" ]  = convertPixelToUnitString( rElem.h );
147 
148     const GraphicsContext& rGC =
149         rEmitContext.rProcessor.getGraphicsContext( rElem.GCId );
150     if( rGC.Transformation.isIdentity() )
151     {
152         if( !rElem.isCharacter )
153         {
154             rProps[ "svg:x" ]       = convertPixelToUnitString( rel_x );
155             rProps[ "svg:y" ]       = convertPixelToUnitString( rel_y );
156         }
157     }
158     else
159     {
160         basegfx::B2DTuple aScale, aTranslation;
161         double fRotate, fShearX;
162 
163         rGC.Transformation.decompose( aScale, aTranslation, fRotate, fShearX );
164 
165         OUStringBuffer aBuf( 256 );
166 
167         // TODO(F2): general transformation case missing; if implemented, note
168         // that ODF rotation is oriented the other way
169 
170         // build transformation string
171         if (rElem.MirrorVertical)
172         {
173             // At some point, rElem.h may start arriving positive,
174             // so use robust adjusting math
175             rel_y -= std::abs(rElem.h);
176             if (!aBuf.isEmpty())
177                 aBuf.append(' ');
178             aBuf.append("scale( 1.0 -1.0 )");
179         }
180         if( fShearX != 0.0 )
181         {
182             aBuf.append( "skewX( " );
183             aBuf.append( fShearX );
184             aBuf.append( " )" );
185         }
186         if( fRotate != 0.0 )
187         {
188             if( !aBuf.isEmpty() )
189                 aBuf.append( ' ' );
190             aBuf.append( "rotate( " );
191             aBuf.append( -fRotate );
192             aBuf.append( " )" );
193 
194         }
195         if( ! rElem.isCharacter )
196         {
197             if( !aBuf.isEmpty() )
198                 aBuf.append( ' ' );
199             aBuf.append( "translate( " );
200             aBuf.append( convertPixelToUnitString( rel_x ) );
201             aBuf.append( ' ' );
202             aBuf.append( convertPixelToUnitString( rel_y ) );
203             aBuf.append( " )" );
204         }
205 
206         rProps[ "draw:transform" ] = aBuf.makeStringAndClear();
207     }
208 }
209 
210 void WriterXmlEmitter::visit( FrameElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&   )
211 {
212     if( elem.Children.empty() )
213         return;
214 
215     bool bTextBox = (dynamic_cast<ParagraphElement*>(elem.Children.front().get()) != nullptr);
216     PropertyMap aFrameProps;
217     fillFrameProps( elem, aFrameProps, m_rEmitContext );
218     m_rEmitContext.rEmitter.beginTag( "draw:frame", aFrameProps );
219     if( bTextBox )
220         m_rEmitContext.rEmitter.beginTag( "draw:text-box", PropertyMap() );
221 
222     auto this_it = elem.Children.begin();
223     while( this_it != elem.Children.end() && this_it->get() != &elem )
224     {
225         (*this_it)->visitedBy( *this, this_it );
226         ++this_it;
227     }
228 
229     if( bTextBox )
230         m_rEmitContext.rEmitter.endTag( "draw:text-box" );
231     m_rEmitContext.rEmitter.endTag( "draw:frame" );
232 }
233 
234 void WriterXmlEmitter::visit( PolyPolyElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
235 {
236     elem.updateGeometry();
237     /* note:
238      *   aw recommends using 100dth of mm in all respects since the xml import
239      *   (a) is buggy (see issue 37213)
240      *   (b) is optimized for 100dth of mm and does not scale itself then,
241      *       this does not gain us speed but makes for smaller rounding errors since
242      *       the xml importer coordinates are integer based
243      */
244     for (sal_uInt32 i = 0; i< elem.PolyPoly.count(); i++)
245     {
246         basegfx::B2DPolygon b2dPolygon =  elem.PolyPoly.getB2DPolygon( i );
247 
248         for ( sal_uInt32 j = 0; j< b2dPolygon.count(); j++ )
249         {
250             basegfx::B2DPoint point;
251             basegfx::B2DPoint nextPoint;
252             point = b2dPolygon.getB2DPoint( j );
253 
254             basegfx::B2DPoint prevPoint = b2dPolygon.getPrevControlPoint( j ) ;
255 
256             point.setX( convPx2mmPrec2( point.getX() )*100.0 );
257             point.setY( convPx2mmPrec2( point.getY() )*100.0 );
258 
259             if ( b2dPolygon.isPrevControlPointUsed( j ) )
260             {
261                 prevPoint.setX( convPx2mmPrec2( prevPoint.getX() )*100.0 );
262                 prevPoint.setY( convPx2mmPrec2( prevPoint.getY() )*100.0 );
263             }
264 
265             if ( b2dPolygon.isNextControlPointUsed( j ) )
266             {
267                 nextPoint = b2dPolygon.getNextControlPoint( j ) ;
268                 nextPoint.setX( convPx2mmPrec2( nextPoint.getX() )*100.0 );
269                 nextPoint.setY( convPx2mmPrec2( nextPoint.getY() )*100.0 );
270             }
271 
272             b2dPolygon.setB2DPoint( j, point );
273 
274             if ( b2dPolygon.isPrevControlPointUsed( j ) )
275                 b2dPolygon.setPrevControlPoint( j , prevPoint ) ;
276 
277             if ( b2dPolygon.isNextControlPointUsed( j ) )
278                 b2dPolygon.setNextControlPoint( j , nextPoint ) ;
279         }
280 
281         elem.PolyPoly.setB2DPolygon( i, b2dPolygon );
282     }
283 
284     PropertyMap aProps;
285     fillFrameProps( elem, aProps, m_rEmitContext );
286     OUStringBuffer aBuf( 64 );
287     aBuf.append( "0 0 " );
288     aBuf.append( convPx2mmPrec2(elem.w)*100.0 );
289     aBuf.append( ' ' );
290     aBuf.append( convPx2mmPrec2(elem.h)*100.0 );
291     aProps[ "svg:viewBox" ] = aBuf.makeStringAndClear();
292     aProps[ "svg:d" ]       = basegfx::utils::exportToSvgD( elem.PolyPoly, true, true, false );
293 
294     m_rEmitContext.rEmitter.beginTag( "draw:path", aProps );
295     m_rEmitContext.rEmitter.endTag( "draw:path" );
296 }
297 
298 void WriterXmlEmitter::visit( ImageElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
299 {
300     PropertyMap aImageProps;
301     m_rEmitContext.rEmitter.beginTag( "draw:image", aImageProps );
302     m_rEmitContext.rEmitter.beginTag( "office:binary-data", PropertyMap() );
303     m_rEmitContext.rImages.writeBase64EncodedStream( elem.Image, m_rEmitContext);
304     m_rEmitContext.rEmitter.endTag( "office:binary-data" );
305     m_rEmitContext.rEmitter.endTag( "draw:image" );
306 }
307 
308 void WriterXmlEmitter::visit( PageElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&   )
309 {
310     if( m_rEmitContext.xStatusIndicator.is() )
311         m_rEmitContext.xStatusIndicator->setValue( elem.PageNumber );
312 
313     auto this_it =  elem.Children.begin();
314     while( this_it != elem.Children.end() && this_it->get() != &elem )
315     {
316         (*this_it)->visitedBy( *this, this_it );
317         ++this_it;
318     }
319 }
320 
321 void WriterXmlEmitter::visit( DocumentElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&)
322 {
323     m_rEmitContext.rEmitter.beginTag( "office:body", PropertyMap() );
324     m_rEmitContext.rEmitter.beginTag( "office:text", PropertyMap() );
325 
326     for( const auto& rxChild : elem.Children )
327     {
328         PageElement* pPage = dynamic_cast<PageElement*>(rxChild.get());
329         if( pPage )
330         {
331             // emit only page anchored objects
332             // currently these are only DrawElement types
333             for( auto child_it = pPage->Children.begin(); child_it != pPage->Children.end(); ++child_it )
334             {
335                 if( dynamic_cast<DrawElement*>(child_it->get()) != nullptr )
336                     (*child_it)->visitedBy( *this, child_it );
337             }
338         }
339     }
340 
341     // do not emit page anchored objects, they are emitted before
342     // (must precede all pages in writer document) currently these are
343     // only DrawElement types
344     for( auto it = elem.Children.begin(); it != elem.Children.end(); ++it )
345     {
346         if( dynamic_cast<DrawElement*>(it->get()) == nullptr )
347             (*it)->visitedBy( *this, it );
348     }
349 
350     m_rEmitContext.rEmitter.endTag( "office:text" );
351     m_rEmitContext.rEmitter.endTag( "office:body" );
352 }
353 
354 
355 void WriterXmlOptimizer::visit( HyperlinkElement&, const std::list< std::unique_ptr<Element> >::const_iterator& )
356 {
357 }
358 
359 void WriterXmlOptimizer::visit( TextElement&, const std::list< std::unique_ptr<Element> >::const_iterator&)
360 {
361 }
362 
363 void WriterXmlOptimizer::visit( FrameElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
364 {
365     elem.applyToChildren(*this);
366 }
367 
368 void WriterXmlOptimizer::visit( ImageElement&, const std::list< std::unique_ptr<Element> >::const_iterator& )
369 {
370 }
371 
372 void WriterXmlOptimizer::visit( PolyPolyElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& elemIt )
373 {
374     /* note: optimize two consecutive PolyPolyElements that
375      *  have the same path but one of which is a stroke while
376      *     the other is a fill
377      */
378     if( !elem.Parent )
379         return;
380     // find following PolyPolyElement in parent's children list
381     if( elemIt == elem.Parent->Children.end() )
382         return;
383     auto next_it = elemIt;
384     ++next_it;
385     if( next_it == elem.Parent->Children.end() )
386         return;
387 
388     PolyPolyElement* pNext = dynamic_cast<PolyPolyElement*>(next_it->get());
389     if( !pNext || pNext->PolyPoly != elem.PolyPoly )
390         return;
391 
392     const GraphicsContext& rNextGC =
393                   m_rProcessor.getGraphicsContext( pNext->GCId );
394     const GraphicsContext& rThisGC =
395                   m_rProcessor.getGraphicsContext( elem.GCId );
396 
397     if( !(rThisGC.BlendMode      == rNextGC.BlendMode &&
398         rThisGC.Flatness       == rNextGC.Flatness &&
399         rThisGC.Transformation == rNextGC.Transformation &&
400         rThisGC.Clip           == rNextGC.Clip &&
401         pNext->Action          == PATH_STROKE &&
402         (elem.Action == PATH_FILL || elem.Action == PATH_EOFILL)) )
403         return;
404 
405     GraphicsContext aGC = rThisGC;
406     aGC.LineJoin  = rNextGC.LineJoin;
407     aGC.LineCap   = rNextGC.LineCap;
408     aGC.LineWidth = rNextGC.LineWidth;
409     aGC.MiterLimit= rNextGC.MiterLimit;
410     aGC.DashArray = rNextGC.DashArray;
411     aGC.LineColor = rNextGC.LineColor;
412     elem.GCId = m_rProcessor.getGCId( aGC );
413 
414     elem.Action |= pNext->Action;
415 
416     elem.Children.splice( elem.Children.end(), pNext->Children );
417     elem.Parent->Children.erase(next_it);
418 }
419 
420 void WriterXmlOptimizer::visit( ParagraphElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& rParentIt)
421 {
422     optimizeTextElements( elem );
423 
424     elem.applyToChildren(*this);
425 
426     if( !(elem.Parent && rParentIt != elem.Parent->Children.end()) )
427         return;
428 
429     // find if there is a previous paragraph that might be a heading for this one
430     auto prev = rParentIt;
431     ParagraphElement* pPrevPara = nullptr;
432     while( prev != elem.Parent->Children.begin() )
433     {
434         --prev;
435         pPrevPara = dynamic_cast< ParagraphElement* >(prev->get());
436         if( pPrevPara )
437         {
438             /* What constitutes a heading ? current hints are:
439              * - one line only
440              * - not too far away from this paragraph (two heading height max ?)
441              * - font larger or bold
442              * this is of course incomplete
443              * FIXME: improve hints for heading
444              */
445             // check for single line
446             if( pPrevPara->isSingleLined( m_rProcessor ) )
447             {
448                 double head_line_height = pPrevPara->getLineHeight( m_rProcessor );
449                 if( pPrevPara->y + pPrevPara->h + 2*head_line_height > elem.y )
450                 {
451                     // check for larger font
452                     if( head_line_height > elem.getLineHeight( m_rProcessor ) )
453                     {
454                         pPrevPara->Type = ParagraphElement::Headline;
455                     }
456                     else
457                     {
458                         // check whether text of pPrevPara is bold (at least first text element)
459                         // and this para is not bold (ditto)
460                         TextElement* pPrevText = pPrevPara->getFirstTextChild();
461                         TextElement* pThisText = elem.getFirstTextChild();
462                         if( pPrevText && pThisText )
463                         {
464                             const FontAttributes& rPrevFont = m_rProcessor.getFont( pPrevText->FontId );
465                             const FontAttributes& rThisFont = m_rProcessor.getFont( pThisText->FontId );
466                             if ( (rPrevFont.fontWeight ==  u"600" ||
467                                   rPrevFont.fontWeight ==  u"bold" ||
468                                   rPrevFont.fontWeight ==  u"800" ||
469                                   rPrevFont.fontWeight ==  u"900" )  &&
470                                  (rThisFont.fontWeight ==  u"600" ||
471                                   rThisFont.fontWeight ==  u"bold" ||
472                                   rThisFont.fontWeight ==  u"800" ||
473                                   rThisFont.fontWeight ==  u"900" ) )
474                             {
475                                 pPrevPara->Type = ParagraphElement::Headline;
476                             }
477                         }
478                     }
479                 }
480             }
481             break;
482         }
483     }
484 }
485 
486 void WriterXmlOptimizer::visit( PageElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
487 {
488     if( m_rProcessor.getStatusIndicator().is() )
489         m_rProcessor.getStatusIndicator()->setValue( elem.PageNumber );
490 
491     // resolve hyperlinks
492     elem.resolveHyperlinks();
493 
494     elem.resolveFontStyles( m_rProcessor ); // underlines and such
495 
496     // FIXME: until hyperlinks and font effects are adjusted for
497     // geometrical search handle them before sorting
498     PDFIProcessor::sortElements( &elem );
499 
500     // find paragraphs in text
501     ParagraphElement* pCurPara = nullptr;
502     std::list< std::unique_ptr<Element> >::iterator page_element, next_page_element;
503     next_page_element = elem.Children.begin();
504     double fCurLineHeight = 0.0; // average height of text items in current para
505     int nCurLineElements = 0; // number of line contributing elements in current para
506     double line_left = elem.w, line_right = 0.0;
507     double column_width = elem.w*0.75; // estimate text width
508     // TODO: guess columns
509     while( next_page_element != elem.Children.end() )
510     {
511         page_element = next_page_element++;
512         ParagraphElement* pPagePara = dynamic_cast<ParagraphElement*>(page_element->get());
513         if( pPagePara )
514         {
515             pCurPara = pPagePara;
516             // adjust line height and text items
517             fCurLineHeight = 0.0;
518             nCurLineElements = 0;
519             for( const auto& rxChild : pCurPara->Children )
520             {
521                 TextElement* pTestText = rxChild->dynCastAsTextElement();
522                 if( pTestText )
523                 {
524                     fCurLineHeight = (fCurLineHeight*double(nCurLineElements) + pTestText->h)/double(nCurLineElements+1);
525                     nCurLineElements++;
526                 }
527             }
528             continue;
529         }
530 
531         HyperlinkElement* pLink = dynamic_cast<HyperlinkElement*>(page_element->get());
532         DrawElement* pDraw = dynamic_cast<DrawElement*>(page_element->get());
533         if( ! pDraw && pLink && ! pLink->Children.empty() )
534             pDraw = dynamic_cast<DrawElement*>(pLink->Children.front().get() );
535         if( pDraw )
536         {
537             // insert small drawing objects as character, else leave them page bound
538 
539             bool bInsertToParagraph = false;
540             // first check if this is either inside the paragraph
541             if( pCurPara && pDraw->y < pCurPara->y + pCurPara->h )
542             {
543                 if( pDraw->h < fCurLineHeight * 1.5 )
544                 {
545                     bInsertToParagraph = true;
546                     fCurLineHeight = (fCurLineHeight*double(nCurLineElements) + pDraw->h)/double(nCurLineElements+1);
547                     nCurLineElements++;
548                     // mark draw element as character
549                     pDraw->isCharacter = true;
550                 }
551             }
552             // or perhaps the draw element begins a new paragraph
553             else if( next_page_element != elem.Children.end() )
554             {
555                 TextElement* pText = (*next_page_element)->dynCastAsTextElement();
556                 if( ! pText )
557                 {
558                     ParagraphElement* pPara = dynamic_cast<ParagraphElement*>(next_page_element->get());
559                     if( pPara && ! pPara->Children.empty() )
560                         pText = pPara->Children.front()->dynCastAsTextElement();
561                 }
562                 if( pText && // check there is a text
563                     pDraw->h < pText->h*1.5 && // and it is approx the same height
564                     // and either upper or lower edge of pDraw is inside text's vertical range
565                     ( ( pDraw->y >= pText->y && pDraw->y <= pText->y+pText->h ) ||
566                       ( pDraw->y+pDraw->h >= pText->y && pDraw->y+pDraw->h <= pText->y+pText->h )
567                       )
568                     )
569                 {
570                     bInsertToParagraph = true;
571                     fCurLineHeight = pDraw->h;
572                     nCurLineElements = 1;
573                     line_left = pDraw->x;
574                     line_right = pDraw->x + pDraw->w;
575                     // begin a new paragraph
576                     pCurPara = nullptr;
577                     // mark draw element as character
578                     pDraw->isCharacter = true;
579                 }
580             }
581 
582             if( ! bInsertToParagraph )
583             {
584                 pCurPara = nullptr;
585                 continue;
586             }
587         }
588 
589         TextElement* pText = (*page_element)->dynCastAsTextElement();
590         if( ! pText && pLink && ! pLink->Children.empty() )
591             pText = pLink->Children.front()->dynCastAsTextElement();
592         if( pText )
593         {
594             Element* pGeo = pLink ? static_cast<Element*>(pLink) :
595                                     static_cast<Element*>(pText);
596             if( pCurPara )
597             {
598                 // there was already a text element, check for a new paragraph
599                 if( nCurLineElements > 0 )
600                 {
601                     // if the new text is significantly distant from the paragraph
602                     // begin a new paragraph
603                     if( pGeo->y > pCurPara->y+pCurPara->h + fCurLineHeight*0.5 )
604                         pCurPara = nullptr; // insert new paragraph
605                     else if( pGeo->y > (pCurPara->y+pCurPara->h - fCurLineHeight*0.05) )
606                     {
607                         // new paragraph if either the last line of the paragraph
608                         // was significantly shorter than the paragraph as a whole
609                         if( (line_right - line_left) < pCurPara->w*0.75 )
610                             pCurPara = nullptr;
611                         // or the last line was significantly smaller than the column width
612                         else if( (line_right - line_left) < column_width*0.75 )
613                             pCurPara = nullptr;
614                     }
615                 }
616             }
617             // update line height/width
618             if( pCurPara )
619             {
620                 fCurLineHeight = (fCurLineHeight*double(nCurLineElements) + pGeo->h)/double(nCurLineElements+1);
621                 nCurLineElements++;
622                 if( pGeo->x < line_left )
623                     line_left = pGeo->x;
624                 if( pGeo->x+pGeo->w > line_right )
625                     line_right = pGeo->x+pGeo->w;
626             }
627             else
628             {
629                 fCurLineHeight = pGeo->h;
630                 nCurLineElements = 1;
631                 line_left = pGeo->x;
632                 line_right = pGeo->x + pGeo->w;
633             }
634         }
635 
636         // move element to current paragraph
637         if( ! pCurPara ) // new paragraph, insert one
638         {
639             pCurPara = ElementFactory::createParagraphElement( nullptr );
640             // set parent
641             pCurPara->Parent = &elem;
642             //insert new paragraph before current element
643             page_element = elem.Children.insert( page_element, std::unique_ptr<Element>(pCurPara) );
644             // forward iterator to current element again
645             ++ page_element;
646             // update next_element which is now invalid
647             next_page_element = page_element;
648             ++ next_page_element;
649         }
650         Element* pCurEle = page_element->get();
651         Element::setParent( page_element, pCurPara );
652         OSL_ENSURE( !pText || pCurEle == pText || pCurEle == pLink, "paragraph child list in disorder" );
653         if( pText || pDraw )
654             pCurPara->updateGeometryWith( pCurEle );
655     }
656 
657     // process children
658     elem.applyToChildren(*this);
659 
660     // find possible header and footer
661     checkHeaderAndFooter( elem );
662 }
663 
664 void WriterXmlOptimizer::checkHeaderAndFooter( PageElement& rElem )
665 {
666     /* indicators for a header:
667      *  - single line paragraph at top of page (inside 15% page height)
668      *  - at least lineheight above the next paragraph
669      *
670      *  indicators for a footer likewise:
671      *  - single line paragraph at bottom of page (inside 15% page height)
672      *  - at least lineheight below the previous paragraph
673      */
674 
675     auto isParagraphElement = [](std::unique_ptr<Element>& rxChild) -> bool {
676         return dynamic_cast<ParagraphElement*>(rxChild.get()) != nullptr;
677     };
678 
679     // detect header
680     // Note: the following assumes that the pages' children have been
681     // sorted geometrically
682     auto it = std::find_if(rElem.Children.begin(), rElem.Children.end(), isParagraphElement);
683     if (it != rElem.Children.end())
684     {
685         ParagraphElement& rPara = dynamic_cast<ParagraphElement&>(**it);
686         if( rPara.y+rPara.h < rElem.h*0.15 && rPara.isSingleLined( m_rProcessor ) )
687         {
688             auto next_it = it;
689             ParagraphElement* pNextPara = nullptr;
690             while( ++next_it != rElem.Children.end() && pNextPara == nullptr )
691             {
692                 pNextPara = dynamic_cast<ParagraphElement*>(next_it->get());
693             }
694             if( pNextPara && pNextPara->y > rPara.y+rPara.h*2 )
695             {
696                 rElem.HeaderElement = std::move(*it);
697                 rPara.Parent = nullptr;
698                 rElem.Children.erase( it );
699             }
700         }
701     }
702 
703     // detect footer
704     auto rit = std::find_if(rElem.Children.rbegin(), rElem.Children.rend(), isParagraphElement);
705     if (rit == rElem.Children.rend())
706         return;
707 
708     ParagraphElement& rPara = dynamic_cast<ParagraphElement&>(**rit);
709     if( !(rPara.y > rElem.h*0.85 && rPara.isSingleLined( m_rProcessor )) )
710         return;
711 
712     std::list< std::unique_ptr<Element> >::reverse_iterator next_it = rit;
713     ParagraphElement* pNextPara = nullptr;
714     while( ++next_it != rElem.Children.rend() && pNextPara == nullptr )
715     {
716         pNextPara = dynamic_cast<ParagraphElement*>(next_it->get());
717     }
718     if( pNextPara && pNextPara->y < rPara.y-rPara.h*2 )
719     {
720         rElem.FooterElement = std::move(*rit);
721         rPara.Parent = nullptr;
722         rElem.Children.erase( std::next(rit).base() );
723     }
724 }
725 
726 void WriterXmlOptimizer::optimizeTextElements(Element& rParent)
727 {
728     if( rParent.Children.empty() ) // this should not happen
729     {
730         OSL_FAIL( "empty paragraph optimized" );
731         return;
732     }
733 
734     // concatenate child elements with same font id
735     auto next = rParent.Children.begin();
736     auto it = next++;
737     FrameElement* pFrame = dynamic_cast<FrameElement*>(rParent.Parent);
738     bool bRotatedFrame = false;
739     if( pFrame )
740     {
741         const GraphicsContext& rFrameGC = m_rProcessor.getGraphicsContext( pFrame->GCId );
742         if( rFrameGC.isRotatedOrSkewed() )
743             bRotatedFrame = true;
744     }
745     while( next != rParent.Children.end() )
746     {
747         bool bConcat = false;
748         TextElement* pCur = (*it)->dynCastAsTextElement();
749         if( pCur )
750         {
751             TextElement* pNext = dynamic_cast<TextElement*>(next->get());
752             if( pNext )
753             {
754                 const GraphicsContext& rCurGC = m_rProcessor.getGraphicsContext( pCur->GCId );
755                 const GraphicsContext& rNextGC = m_rProcessor.getGraphicsContext( pNext->GCId );
756 
757                 // line and space optimization; works only in strictly horizontal mode
758 
759                 if( !bRotatedFrame
760                     && ! rCurGC.isRotatedOrSkewed()
761                     && ! rNextGC.isRotatedOrSkewed()
762                     && ! pNext->Text.isEmpty()
763                     && pNext->Text[0] != ' '
764                     && ! pCur->Text.isEmpty()
765                     && pCur->Text[pCur->Text.getLength() - 1] != ' '
766                     )
767                 {
768                     // check for new line in paragraph
769                     if( pNext->y > pCur->y+pCur->h )
770                     {
771                         // new line begins
772                         // check whether a space would should be inserted or a hyphen removed
773                         sal_Unicode aLastCode = pCur->Text[pCur->Text.getLength() - 1];
774                         if( aLastCode == '-'
775                             || aLastCode == 0x2010
776                             || (aLastCode >= 0x2012 && aLastCode <= 0x2015)
777                             || aLastCode == 0xff0d
778                         )
779                         {
780                             // cut a hyphen
781                             pCur->Text.setLength( pCur->Text.getLength()-1 );
782                         }
783                         // append a space unless there is a non breaking hyphen
784                         else if( aLastCode != 0x2011 )
785                         {
786                             pCur->Text.append( ' ' );
787                         }
788                     }
789                     else // we're continuing the same line
790                     {
791                         // check whether a space would should be inserted
792                         // check for a small horizontal offset
793                         if( pCur->x + pCur->w + pNext->h*0.15 < pNext->x )
794                         {
795                             pCur->Text.append( ' ' );
796                         }
797                     }
798                 }
799                 // concatenate consecutive text elements unless there is a
800                 // font or text color or matrix change, leave a new span in that case
801                 if( pCur->FontId == pNext->FontId &&
802                     rCurGC.FillColor.Red == rNextGC.FillColor.Red &&
803                     rCurGC.FillColor.Green == rNextGC.FillColor.Green &&
804                     rCurGC.FillColor.Blue == rNextGC.FillColor.Blue &&
805                     rCurGC.FillColor.Alpha == rNextGC.FillColor.Alpha &&
806                     rCurGC.Transformation == rNextGC.Transformation
807                     )
808                 {
809                     pCur->updateGeometryWith( pNext );
810                     // append text to current element
811                     pCur->Text.append( pNext->Text );
812                     // append eventual children to current element
813                     // and clear children (else the children just
814                     // appended to pCur would be destroyed)
815                     pCur->Children.splice( pCur->Children.end(), pNext->Children );
816                     // get rid of the now useless element
817                     rParent.Children.erase( next );
818                     bConcat = true;
819                 }
820             }
821         }
822         else if( dynamic_cast<HyperlinkElement*>(it->get()) )
823             optimizeTextElements( **it );
824         if( bConcat )
825         {
826             next = it;
827             ++next;
828         }
829         else
830         {
831             ++it;
832             ++next;
833         }
834     }
835 }
836 
837 void WriterXmlOptimizer::visit( DocumentElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&)
838 {
839     elem.applyToChildren(*this);
840 }
841 
842 
843 void WriterXmlFinalizer::visit( PolyPolyElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
844 {
845     // xxx TODO copied from DrawElement
846     const GraphicsContext& rGC = m_rProcessor.getGraphicsContext(elem.GCId );
847     PropertyMap aProps;
848     aProps[ "style:family" ] = "graphic";
849 
850     PropertyMap aGCProps;
851     if (elem.Action & PATH_STROKE)
852     {
853         double scale = GetAverageTransformationScale(rGC.Transformation);
854         if (rGC.DashArray.size() < 2)
855         {
856             aGCProps[ "draw:stroke" ] = "solid";
857         }
858         else
859         {
860             PropertyMap props;
861             FillDashStyleProps(props, rGC.DashArray, scale);
862             StyleContainer::Style style("draw:stroke-dash", std::move(props));
863 
864             aGCProps[ "draw:stroke" ] = "dash";
865             aGCProps[ "draw:stroke-dash" ] =
866                 m_rStyleContainer.getStyleName(
867                 m_rStyleContainer.getStyleId(style));
868         }
869 
870         aGCProps[ "svg:stroke-color" ] = getColorString(rGC.LineColor);
871         aGCProps[ "svg:stroke-width" ] = convertPixelToUnitString(rGC.LineWidth * scale);
872         aGCProps[ "draw:stroke-linejoin" ] = rGC.GetLineJoinString();
873         aGCProps[ "svg:stroke-linecap" ] = rGC.GetLineCapString();
874     }
875     else
876     {
877         aGCProps[ "draw:stroke" ] = "none";
878     }
879 
880     // TODO(F1): check whether stuff could be emulated by gradient/bitmap/hatch
881     if( elem.Action & (PATH_FILL | PATH_EOFILL) )
882     {
883         aGCProps[ "draw:fill" ]   = "solid";
884         aGCProps[ "draw:fill-color" ] = getColorString( rGC.FillColor );
885     }
886     else
887     {
888         aGCProps[ "draw:fill" ] = "none";
889     }
890 
891     StyleContainer::Style aStyle( "style:style", std::move(aProps) );
892     StyleContainer::Style aSubStyle( "style:graphic-properties", std::move(aGCProps) );
893     aStyle.SubStyles.push_back( &aSubStyle );
894 
895     elem.StyleId = m_rStyleContainer.getStyleId( aStyle );
896 }
897 
898 void WriterXmlFinalizer::visit( HyperlinkElement&, const std::list< std::unique_ptr<Element> >::const_iterator& )
899 {
900 }
901 
902 void WriterXmlFinalizer::visit( TextElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
903 {
904     const FontAttributes& rFont = m_rProcessor.getFont( elem.FontId );
905     PropertyMap aProps;
906     aProps[ "style:family" ] = "text";
907 
908     PropertyMap aFontProps;
909 
910     // family name
911     // TODO: tdf#143095: use system font name rather than PSName
912     SAL_INFO("sdext.pdfimport", "The font used in xml is: " << rFont.familyName);
913     aFontProps[ "fo:font-family" ] = rFont.familyName;
914     aFontProps[ "style:font-family-asia" ] = rFont.familyName;
915     aFontProps[ "style:font-family-complex" ] = rFont.familyName;
916 
917     // bold
918     aFontProps[ "fo:font-weight" ]            = rFont.fontWeight;
919     aFontProps[ "style:font-weight-asian" ]   = rFont.fontWeight;
920     aFontProps[ "style:font-weight-complex" ] = rFont.fontWeight;
921 
922     // italic
923     if( rFont.isItalic )
924     {
925         aFontProps[ "fo:font-style" ]            = "italic";
926         aFontProps[ "style:font-style-asian" ]   = "italic";
927         aFontProps[ "style:font-style-complex" ] = "italic";
928     }
929 
930     // underline
931     if( rFont.isUnderline )
932     {
933         aFontProps[ "style:text-underline-style" ]  = "solid";
934         aFontProps[ "style:text-underline-width" ]  = "auto";
935         aFontProps[ "style:text-underline-color" ]  = "font-color";
936     }
937 
938     // outline
939     if( rFont.isOutline )
940         aFontProps[ "style:text-outline" ]  = "true";
941 
942     // size
943     OUString aFSize = OUString::number( rFont.size*72/PDFI_OUTDEV_RESOLUTION ) + "pt";
944     aFontProps[ "fo:font-size" ]            = aFSize;
945     aFontProps[ "style:font-size-asian" ]   = aFSize;
946     aFontProps[ "style:font-size-complex" ] = aFSize;
947 
948     // color
949     const GraphicsContext& rGC = m_rProcessor.getGraphicsContext( elem.GCId );
950     aFontProps[ "fo:color" ] = getColorString( rFont.isOutline ? rGC.LineColor : rGC.FillColor );
951 
952     StyleContainer::Style aStyle( "style:style", std::move(aProps) );
953     StyleContainer::Style aSubStyle( "style:text-properties", std::move(aFontProps) );
954     aStyle.SubStyles.push_back( &aSubStyle );
955     elem.StyleId = m_rStyleContainer.getStyleId( aStyle );
956 }
957 
958 void WriterXmlFinalizer::visit( ParagraphElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& rParentIt )
959 {
960     PropertyMap aParaProps;
961 
962     if( elem.Parent )
963     {
964         // check for center alignment
965         // criterion: paragraph is small relative to parent and distributed around its center
966         double p_x = elem.Parent->x;
967         double p_w = elem.Parent->w;
968 
969         PageElement* pPage = dynamic_cast<PageElement*>(elem.Parent);
970         if( pPage )
971         {
972             p_x += pPage->LeftMargin;
973             p_w -= pPage->LeftMargin+pPage->RightMargin;
974         }
975         bool bIsCenter = false;
976         if( elem.w < ( p_w/2) )
977         {
978             double delta = elem.w/4;
979             // allow very small paragraphs to deviate a little more
980             // relative to parent's center
981             if( elem.w <  p_w/8 )
982                 delta = elem.w;
983             if( fabs( elem.x+elem.w/2 - ( p_x+ p_w/2) ) <  delta ||
984                 (pPage && fabs( elem.x+elem.w/2 - (pPage->x + pPage->w/2) ) <  delta) )
985             {
986                 bIsCenter = true;
987                 aParaProps[ "fo:text-align" ] = "center";
988             }
989         }
990         if( ! bIsCenter && elem.x > p_x + p_w/10 )
991         {
992             // indent
993             OUStringBuffer aBuf( 32 );
994             aBuf.append( convPx2mm( elem.x - p_x ) );
995             aBuf.append( "mm" );
996             aParaProps[ "fo:margin-left" ] = aBuf.makeStringAndClear();
997         }
998 
999         // check whether to leave some space to next paragraph
1000         // find whether there is a next paragraph
1001         auto it = rParentIt;
1002         const ParagraphElement* pNextPara = nullptr;
1003         while( ++it != elem.Parent->Children.end() && ! pNextPara )
1004             pNextPara = dynamic_cast< const ParagraphElement* >(it->get());
1005         if( pNextPara )
1006         {
1007             if( pNextPara->y - (elem.y+elem.h) > convmm2Px( 10 ) )
1008             {
1009                 OUStringBuffer aBuf( 32 );
1010                 aBuf.append( convPx2mm( pNextPara->y - (elem.y+elem.h) ) );
1011                 aBuf.append( "mm" );
1012                 aParaProps[ "fo:margin-bottom" ] = aBuf.makeStringAndClear();
1013             }
1014         }
1015     }
1016 
1017     if( ! aParaProps.empty() )
1018     {
1019         PropertyMap aProps;
1020         aProps[ "style:family" ] = "paragraph";
1021         StyleContainer::Style aStyle( "style:style", std::move(aProps) );
1022         StyleContainer::Style aSubStyle( "style:paragraph-properties", std::move(aParaProps) );
1023         aStyle.SubStyles.push_back( &aSubStyle );
1024         elem.StyleId = m_rStyleContainer.getStyleId( aStyle );
1025     }
1026 
1027     elem.applyToChildren(*this);
1028 }
1029 
1030 void WriterXmlFinalizer::visit( FrameElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator&)
1031 {
1032     PropertyMap aProps;
1033     aProps[ "style:family" ] = "graphic";
1034 
1035     PropertyMap aGCProps;
1036 
1037     aGCProps[ "draw:stroke" ]                    = "none";
1038     aGCProps[ "draw:fill" ]                      = "none";
1039     aGCProps[ "draw:auto-grow-height" ]          = "true";
1040     aGCProps[ "draw:auto-grow-width" ]           = "true";
1041     aGCProps[ "draw:textarea-horizontal-align" ] = "left";
1042     aGCProps[ "draw:textarea-vertical-align" ]   = "top";
1043     aGCProps[ "fo:min-height"]                   = "0cm";
1044     aGCProps[ "fo:min-width"]                    = "0cm";
1045     aGCProps[ "fo:padding-top" ]                 = "0cm";
1046     aGCProps[ "fo:padding-left" ]                = "0cm";
1047     aGCProps[ "fo:padding-right" ]               = "0cm";
1048     aGCProps[ "fo:padding-bottom" ]              = "0cm";
1049 
1050     StyleContainer::Style aStyle( "style:style", std::move(aProps) );
1051     StyleContainer::Style aSubStyle( "style:graphic-properties", std::move(aGCProps) );
1052     aStyle.SubStyles.push_back( &aSubStyle );
1053 
1054     elem.StyleId = m_rStyleContainer.getStyleId( aStyle );
1055     elem.applyToChildren(*this);
1056 }
1057 
1058 void WriterXmlFinalizer::visit( ImageElement&, const std::list< std::unique_ptr<Element> >::const_iterator& )
1059 {
1060 }
1061 
1062 void WriterXmlFinalizer::setFirstOnPage( ParagraphElement&    rElem,
1063                                          StyleContainer&      rStyles,
1064                                          const OUString& rMasterPageName )
1065 {
1066     PropertyMap aProps;
1067     if( rElem.StyleId != -1 )
1068     {
1069         const PropertyMap* pProps = rStyles.getProperties( rElem.StyleId );
1070         if( pProps )
1071             aProps = *pProps;
1072     }
1073 
1074     aProps[ "style:family" ] = "paragraph";
1075     aProps[ "style:master-page-name" ] = rMasterPageName;
1076 
1077     if( rElem.StyleId != -1 )
1078         rElem.StyleId = rStyles.setProperties( rElem.StyleId, std::move(aProps) );
1079     else
1080     {
1081         StyleContainer::Style aStyle( "style:style", std::move(aProps) );
1082         rElem.StyleId = rStyles.getStyleId( aStyle );
1083     }
1084 }
1085 
1086 void WriterXmlFinalizer::visit( PageElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
1087 {
1088     if( m_rProcessor.getStatusIndicator().is() )
1089         m_rProcessor.getStatusIndicator()->setValue( elem.PageNumber );
1090 
1091     // transform from pixel to mm
1092     double page_width = convPx2mm( elem.w ), page_height = convPx2mm( elem.h );
1093 
1094     // calculate page margins out of the relevant children (paragraphs)
1095     elem.TopMargin = elem.h;
1096     elem.BottomMargin = 0;
1097     elem.LeftMargin = elem.w;
1098     elem.RightMargin = 0;
1099     // first element should be a paragraph
1100     ParagraphElement* pFirstPara = nullptr;
1101     for( const auto& rxChild : elem.Children )
1102     {
1103         if( dynamic_cast<ParagraphElement*>( rxChild.get() ) )
1104         {
1105             if( rxChild->x < elem.LeftMargin )
1106                 elem.LeftMargin = rxChild->x;
1107             if( rxChild->y < elem.TopMargin )
1108                 elem.TopMargin = rxChild->y;
1109             if( rxChild->x + rxChild->w > elem.w - elem.RightMargin )
1110                 elem.RightMargin = elem.w - (rxChild->x + rxChild->w);
1111             if( rxChild->y + rxChild->h > elem.h - elem.BottomMargin )
1112                 elem.BottomMargin = elem.h - (rxChild->y + rxChild->h);
1113             if( ! pFirstPara )
1114                 pFirstPara = dynamic_cast<ParagraphElement*>( rxChild.get() );
1115         }
1116     }
1117     if( elem.HeaderElement && elem.HeaderElement->y < elem.TopMargin )
1118         elem.TopMargin = elem.HeaderElement->y;
1119     if( elem.FooterElement && elem.FooterElement->y+elem.FooterElement->h > elem.h - elem.BottomMargin )
1120         elem.BottomMargin = elem.h - (elem.FooterElement->y + elem.FooterElement->h);
1121 
1122     // transform margins to mm
1123     double left_margin     = convPx2mm( elem.LeftMargin );
1124     double right_margin    = convPx2mm( elem.RightMargin );
1125     double top_margin      = convPx2mm( elem.TopMargin );
1126     double bottom_margin   = convPx2mm( elem.BottomMargin );
1127     if( ! pFirstPara )
1128     {
1129         // use default page margins
1130         left_margin     = 10;
1131         right_margin    = 10;
1132         top_margin      = 10;
1133         bottom_margin   = 10;
1134     }
1135 
1136     // round left/top margin to nearest mm
1137     left_margin     = rtl_math_round( left_margin, 0, rtl_math_RoundingMode_Floor );
1138     top_margin      = rtl_math_round( top_margin, 0, rtl_math_RoundingMode_Floor );
1139     // round (fuzzy) right/bottom margin to nearest cm
1140     right_margin    = rtl_math_round( right_margin, right_margin >= 10 ? -1 : 0, rtl_math_RoundingMode_Floor );
1141     bottom_margin   = rtl_math_round( bottom_margin, bottom_margin >= 10 ? -1 : 0, rtl_math_RoundingMode_Floor );
1142 
1143     // set reasonable default in case of way too large margins
1144     // e.g. no paragraph case
1145     if( left_margin > page_width/2.0 - 10 )
1146         left_margin = 10;
1147     if( right_margin > page_width/2.0 - 10 )
1148         right_margin = 10;
1149     if( top_margin > page_height/2.0 - 10 )
1150         top_margin = 10;
1151     if( bottom_margin > page_height/2.0 - 10 )
1152         bottom_margin = 10;
1153 
1154     // catch the weird cases
1155     if( left_margin < 0 )
1156         left_margin = 0;
1157     if( right_margin < 0 )
1158         right_margin = 0;
1159     if( top_margin < 0 )
1160         top_margin = 0;
1161     if( bottom_margin < 0 )
1162         bottom_margin = 0;
1163 
1164     // widely differing margins are unlikely to be correct
1165     if( right_margin > left_margin*1.5 )
1166         right_margin = left_margin;
1167 
1168     elem.LeftMargin      = convmm2Px( left_margin );
1169     elem.RightMargin     = convmm2Px( right_margin );
1170     elem.TopMargin       = convmm2Px( top_margin );
1171     elem.BottomMargin    = convmm2Px( bottom_margin );
1172 
1173     // get styles for paragraphs
1174     PropertyMap aPageProps;
1175     PropertyMap aPageLayoutProps;
1176     aPageLayoutProps[ "fo:page-width" ]     = unitMMString( page_width );
1177     aPageLayoutProps[ "fo:page-height" ]    = unitMMString( page_height );
1178     aPageLayoutProps[ "style:print-orientation" ]
1179         = elem.w < elem.h ? std::u16string_view(u"portrait") : std::u16string_view(u"landscape");
1180     aPageLayoutProps[ "fo:margin-top" ]     = unitMMString( top_margin );
1181     aPageLayoutProps[ "fo:margin-bottom" ]  = unitMMString( bottom_margin );
1182     aPageLayoutProps[ "fo:margin-left" ]    = unitMMString( left_margin );
1183     aPageLayoutProps[ "fo:margin-right" ]   = unitMMString( right_margin );
1184     aPageLayoutProps[ "style:writing-mode" ]= "lr-tb";
1185 
1186     StyleContainer::Style aStyle( "style:page-layout", std::move(aPageProps));
1187     StyleContainer::Style aSubStyle( "style:page-layout-properties", std::move(aPageLayoutProps));
1188     aStyle.SubStyles.push_back(&aSubStyle);
1189     sal_Int32 nPageStyle = m_rStyleContainer.impl_getStyleId( aStyle, false );
1190 
1191     // create master page
1192     OUString aMasterPageLayoutName = m_rStyleContainer.getStyleName( nPageStyle );
1193     aPageProps[ "style:page-layout-name" ] = aMasterPageLayoutName;
1194     StyleContainer::Style aMPStyle( "style:master-page", std::move(aPageProps) );
1195     StyleContainer::Style aHeaderStyle( "style:header", PropertyMap() );
1196     StyleContainer::Style aFooterStyle( "style:footer", PropertyMap() );
1197     if( elem.HeaderElement )
1198     {
1199         elem.HeaderElement->visitedBy( *this, std::list<std::unique_ptr<Element>>::iterator() );
1200         aHeaderStyle.ContainedElement = elem.HeaderElement.get();
1201         aMPStyle.SubStyles.push_back( &aHeaderStyle );
1202     }
1203     if( elem.FooterElement )
1204     {
1205         elem.FooterElement->visitedBy( *this, std::list<std::unique_ptr<Element>>::iterator() );
1206         aFooterStyle.ContainedElement = elem.FooterElement.get();
1207         aMPStyle.SubStyles.push_back( &aFooterStyle );
1208     }
1209     elem.StyleId = m_rStyleContainer.impl_getStyleId( aMPStyle,false );
1210 
1211 
1212     OUString aMasterPageName = m_rStyleContainer.getStyleName( elem.StyleId );
1213 
1214     // create styles for children
1215     elem.applyToChildren(*this);
1216 
1217     // no paragraph or other elements before the first paragraph
1218     if( ! pFirstPara )
1219     {
1220         pFirstPara = ElementFactory::createParagraphElement( nullptr );
1221         pFirstPara->Parent = &elem;
1222         elem.Children.push_front( std::unique_ptr<Element>(pFirstPara) );
1223     }
1224     setFirstOnPage(*pFirstPara, m_rStyleContainer, aMasterPageName);
1225 }
1226 
1227 void WriterXmlFinalizer::visit( DocumentElement& elem, const std::list< std::unique_ptr<Element> >::const_iterator& )
1228 {
1229     elem.applyToChildren(*this);
1230 }
1231 
1232 }
1233 
1234 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1235