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 <config_folders.h>
21
22 #include <contentsink.hxx>
23 #include <pdfparse.hxx>
24 #include <pdfihelper.hxx>
25 #include <wrapper.hxx>
26
27 #include <o3tl/string_view.hxx>
28 #include <osl/file.h>
29 #include <osl/file.hxx>
30 #include <osl/thread.h>
31 #include <osl/process.h>
32 #include <osl/diagnose.h>
33 #include <rtl/bootstrap.hxx>
34 #include <rtl/ustring.hxx>
35 #include <rtl/strbuf.hxx>
36 #include <sal/log.hxx>
37
38 #include <comphelper/propertysequence.hxx>
39 #include <comphelper/string.hxx>
40 #include <com/sun/star/io/XInputStream.hpp>
41 #include <com/sun/star/uno/XComponentContext.hpp>
42 #include <com/sun/star/rendering/PathCapType.hpp>
43 #include <com/sun/star/rendering/PathJoinType.hpp>
44 #include <com/sun/star/rendering/XPolyPolygon2D.hpp>
45 #include <com/sun/star/geometry/Matrix2D.hpp>
46 #include <com/sun/star/geometry/AffineMatrix2D.hpp>
47 #include <com/sun/star/geometry/RealRectangle2D.hpp>
48 #include <com/sun/star/geometry/RealSize2D.hpp>
49 #include <com/sun/star/task/XInteractionHandler.hpp>
50
51 #include <basegfx/point/b2dpoint.hxx>
52 #include <basegfx/polygon/b2dpolypolygon.hxx>
53 #include <basegfx/polygon/b2dpolygon.hxx>
54 #include <basegfx/utils/unopolypolygon.hxx>
55
56 #include <vcl/metric.hxx>
57 #include <vcl/font.hxx>
58 #include <vcl/virdev.hxx>
59
60 #include <cstddef>
61 #include <memory>
62 #include <string_view>
63 #include <unordered_map>
64 #include <vector>
65 #include <string.h>
66
67 using namespace com::sun::star;
68
69 namespace pdfi
70 {
71
72 namespace
73 {
74
75 // identifier of the strings coming from the out-of-process xpdf
76 // converter
77 enum parseKey {
78 CLIPPATH,
79 DRAWCHAR,
80 DRAWIMAGE,
81 DRAWLINK,
82 DRAWMASK,
83 DRAWMASKEDIMAGE,
84 DRAWSOFTMASKEDIMAGE,
85 ENDPAGE,
86 ENDTEXTOBJECT,
87 EOCLIPPATH,
88 EOFILLPATH,
89 FILLPATH,
90 HYPERLINK,
91 INTERSECTCLIP,
92 INTERSECTEOCLIP,
93 POPSTATE,
94 PUSHSTATE,
95 RESTORESTATE,
96 SAVESTATE,
97 SETBLENDMODE,
98 SETFILLCOLOR,
99 SETFONT,
100 SETLINECAP,
101 SETLINEDASH,
102 SETLINEJOIN,
103 SETLINEWIDTH,
104 SETMITERLIMIT,
105 SETPAGENUM,
106 SETSTROKECOLOR,
107 SETTEXTRENDERMODE,
108 SETTRANSFORMATION,
109 STARTPAGE,
110 STROKEPATH,
111 TILINGPATTERNFILL,
112 UPDATEBLENDMODE,
113 UPDATECTM,
114 UPDATEFILLCOLOR,
115 UPDATEFILLOPACITY,
116 UPDATEFLATNESS,
117 UPDATEFONT,
118 UPDATELINECAP,
119 UPDATELINEDASH,
120 UPDATELINEJOIN,
121 UPDATELINEWIDTH,
122 UPDATEMITERLIMIT,
123 UPDATESTROKECOLOR,
124 UPDATESTROKEOPACITY,
125 NONE
126 };
127
128 #if defined _MSC_VER && defined __clang__
129 #pragma clang diagnostic push
130 #pragma clang diagnostic ignored "-Wdeprecated-register"
131 #pragma clang diagnostic ignored "-Wextra-tokens"
132 #endif
133 #include <hash.cxx>
134 #if defined _MSC_VER && defined __clang__
135 #pragma clang diagnostic pop
136 #endif
137
138 class Parser
139 {
140 friend class LineParser;
141
142 typedef std::unordered_map< sal_Int64,
143 FontAttributes > FontMapType;
144
145 ScopedVclPtr<VirtualDevice> m_xDev;
146 const uno::Reference<uno::XComponentContext> m_xContext;
147 const ContentSinkSharedPtr m_pSink;
148 const oslFileHandle m_pErr;
149 FontMapType m_aFontMap;
150
151 public:
Parser(const ContentSinkSharedPtr & rSink,oslFileHandle pErr,const uno::Reference<uno::XComponentContext> & xContext)152 Parser( const ContentSinkSharedPtr& rSink,
153 oslFileHandle pErr,
154 const uno::Reference<uno::XComponentContext>& xContext ) :
155 m_xContext(xContext),
156 m_pSink(rSink),
157 m_pErr(pErr),
158 m_aFontMap(101)
159 {}
160
161 void parseLine( std::string_view aLine );
162 };
163
164 class LineParser {
165 Parser & m_parser;
166 std::string_view m_aLine;
167
168 static void parseFontFamilyName( FontAttributes& aResult );
169 void readInt32( sal_Int32& o_Value );
170 void readInt64( sal_Int64& o_Value );
171 void readDouble( double& o_Value );
172 void readBinaryData( uno::Sequence<sal_Int8>& rBuf );
173
174 uno::Sequence<beans::PropertyValue> readImageImpl();
175
176 public:
177 std::size_t m_nCharIndex = 0;
178
LineParser(Parser & parser,std::string_view line)179 LineParser(Parser & parser, std::string_view line): m_parser(parser), m_aLine(line) {}
180
181 std::string_view readNextToken();
182 sal_Int32 readInt32();
183 double readDouble();
184
185 uno::Reference<rendering::XPolyPolygon2D> readPath();
186
187 void readChar();
188 void readLineCap();
189 void readLineDash();
190 void readLineJoin();
191 void readTransformation();
192 rendering::ARGBColor readColor();
193 void readFont();
194
195 void readImage();
196 void readMask();
197 void readLink();
198 void readMaskedImage();
199 void readSoftMaskedImage();
200 void readTilingPatternFill();
201 };
202
203 /** Unescapes line-ending characters in input string. These
204 characters are encoded as pairs of characters: '\\' 'n', resp.
205 '\\' 'r'. This function converts them back to '\n', resp. '\r'.
206 */
lcl_unescapeLineFeeds(std::string_view i_rStr)207 OString lcl_unescapeLineFeeds(std::string_view i_rStr)
208 {
209 const size_t nOrigLen(i_rStr.size());
210 const char* const pOrig(i_rStr.data());
211 std::unique_ptr<char[]> pBuffer(new char[nOrigLen + 1]);
212
213 const char* pRead(pOrig);
214 char* pWrite(pBuffer.get());
215 const char* pCur(pOrig);
216 while ((pCur = strchr(pCur, '\\')) != nullptr)
217 {
218 const char cNext(pCur[1]);
219 if (cNext == 'n' || cNext == 'r' || cNext == '\\')
220 {
221 const size_t nLen(pCur - pRead);
222 strncpy(pWrite, pRead, nLen);
223 pWrite += nLen;
224 *pWrite = cNext == 'n' ? '\n' : (cNext == 'r' ? '\r' : '\\');
225 ++pWrite;
226 pCur = pRead = pCur + 2;
227 }
228 else
229 {
230 // Just continue on the next character. The current
231 // block will be copied the next time it goes through the
232 // 'if' branch.
233 ++pCur;
234 }
235 }
236 // maybe there are some data to copy yet
237 if (sal::static_int_cast<size_t>(pRead - pOrig) < nOrigLen)
238 {
239 const size_t nLen(nOrigLen - (pRead - pOrig));
240 strncpy(pWrite, pRead, nLen);
241 pWrite += nLen;
242 }
243 *pWrite = '\0';
244
245 OString aResult(pBuffer.get());
246 return aResult;
247 }
248
readNextToken()249 std::string_view LineParser::readNextToken()
250 {
251 if (m_nCharIndex == std::string_view::npos) {
252 SAL_WARN("sdext.pdfimport", "insufficient input");
253 return {};
254 }
255 return o3tl::getToken(m_aLine,' ',m_nCharIndex);
256 }
257
readInt32(sal_Int32 & o_Value)258 void LineParser::readInt32( sal_Int32& o_Value )
259 {
260 std::string_view tok = readNextToken();
261 o_Value = o3tl::toInt32(tok);
262 }
263
readInt32()264 sal_Int32 LineParser::readInt32()
265 {
266 std::string_view tok = readNextToken();
267 return o3tl::toInt32(tok);
268 }
269
readInt64(sal_Int64 & o_Value)270 void LineParser::readInt64( sal_Int64& o_Value )
271 {
272 std::string_view tok = readNextToken();
273 o_Value = o3tl::toInt64(tok);
274 }
275
readDouble(double & o_Value)276 void LineParser::readDouble( double& o_Value )
277 {
278 std::string_view tok = readNextToken();
279 o_Value = rtl_math_stringToDouble(tok.data(), tok.data() + tok.size(), '.', 0,
280 nullptr, nullptr);
281 }
282
readDouble()283 double LineParser::readDouble()
284 {
285 std::string_view tok = readNextToken();
286 return rtl_math_stringToDouble(tok.data(), tok.data() + tok.size(), '.', 0,
287 nullptr, nullptr);
288 }
289
readBinaryData(uno::Sequence<sal_Int8> & rBuf)290 void LineParser::readBinaryData( uno::Sequence<sal_Int8>& rBuf )
291 {
292 sal_Int32 nFileLen( rBuf.getLength() );
293 sal_Int8* pBuf( rBuf.getArray() );
294 sal_uInt64 nBytesRead(0);
295 oslFileError nRes=osl_File_E_None;
296 while( nFileLen )
297 {
298 nRes = osl_readFile( m_parser.m_pErr, pBuf, nFileLen, &nBytesRead );
299 if (osl_File_E_None != nRes )
300 break;
301 pBuf += nBytesRead;
302 nFileLen -= sal::static_int_cast<sal_Int32>(nBytesRead);
303 }
304
305 OSL_PRECOND(nRes==osl_File_E_None, "inconsistent data");
306 }
307
readPath()308 uno::Reference<rendering::XPolyPolygon2D> LineParser::readPath()
309 {
310 static const std::string_view aSubPathMarker( "subpath" );
311
312 if( readNextToken() != aSubPathMarker )
313 OSL_PRECOND(false, "broken path");
314
315 basegfx::B2DPolyPolygon aResult;
316 while( m_nCharIndex != std::string_view::npos )
317 {
318 basegfx::B2DPolygon aSubPath;
319
320 sal_Int32 nClosedFlag;
321 readInt32( nClosedFlag );
322 aSubPath.setClosed( nClosedFlag != 0 );
323
324 sal_Int32 nContiguousControlPoints(0);
325
326 while( m_nCharIndex != std::string_view::npos )
327 {
328 std::size_t nDummy=m_nCharIndex;
329 if (o3tl::getToken(m_aLine,' ',nDummy) == aSubPathMarker) {
330 break;
331 }
332
333 sal_Int32 nCurveFlag;
334 double nX, nY;
335 readDouble( nX );
336 readDouble( nY );
337 readInt32( nCurveFlag );
338
339 aSubPath.append(basegfx::B2DPoint(nX,nY));
340 if( nCurveFlag )
341 {
342 ++nContiguousControlPoints;
343 }
344 else if( nContiguousControlPoints )
345 {
346 OSL_PRECOND(nContiguousControlPoints==2,"broken bezier path");
347
348 // have two control points before us. the current one
349 // is a normal point - thus, convert previous points
350 // into bezier segment
351 const sal_uInt32 nPoints( aSubPath.count() );
352 const basegfx::B2DPoint aCtrlA( aSubPath.getB2DPoint(nPoints-3) );
353 const basegfx::B2DPoint aCtrlB( aSubPath.getB2DPoint(nPoints-2) );
354 const basegfx::B2DPoint aEnd( aSubPath.getB2DPoint(nPoints-1) );
355 aSubPath.remove(nPoints-3, 3);
356 aSubPath.appendBezierSegment(aCtrlA, aCtrlB, aEnd);
357
358 nContiguousControlPoints=0;
359 }
360 }
361
362 aResult.append( aSubPath );
363 if( m_nCharIndex != std::string_view::npos )
364 readNextToken();
365 }
366
367 return static_cast<rendering::XLinePolyPolygon2D*>(
368 new basegfx::unotools::UnoPolyPolygon(std::move(aResult)));
369 }
370
readChar()371 void LineParser::readChar()
372 {
373 double fontSize;
374 geometry::Matrix2D aUnoMatrix;
375 geometry::RealRectangle2D aRect;
376
377 readDouble(aRect.X1);
378 readDouble(aRect.Y1);
379 readDouble(aRect.X2);
380 readDouble(aRect.Y2);
381 readDouble(aUnoMatrix.m00);
382 readDouble(aUnoMatrix.m01);
383 readDouble(aUnoMatrix.m10);
384 readDouble(aUnoMatrix.m11);
385 readDouble(fontSize);
386
387 OString aChars;
388
389 if (m_nCharIndex != std::string_view::npos)
390 aChars = lcl_unescapeLineFeeds( m_aLine.substr( m_nCharIndex ) );
391
392 // chars gobble up rest of line
393 m_nCharIndex = std::string_view::npos;
394
395 m_parser.m_pSink->drawGlyphs(OStringToOUString(aChars, RTL_TEXTENCODING_UTF8),
396 aRect, aUnoMatrix, fontSize);
397 }
398
readLineCap()399 void LineParser::readLineCap()
400 {
401 sal_Int8 nCap(rendering::PathCapType::BUTT);
402 switch( readInt32() )
403 {
404 default:
405 case 0: nCap = rendering::PathCapType::BUTT; break;
406 case 1: nCap = rendering::PathCapType::ROUND; break;
407 case 2: nCap = rendering::PathCapType::SQUARE; break;
408 }
409 m_parser.m_pSink->setLineCap(nCap);
410 }
411
readLineDash()412 void LineParser::readLineDash()
413 {
414 if( m_nCharIndex == std::string_view::npos )
415 {
416 m_parser.m_pSink->setLineDash( uno::Sequence<double>(), 0.0 );
417 return;
418 }
419
420 const double nOffset(readDouble());
421 const sal_Int32 nLen(readInt32());
422
423 uno::Sequence<double> aDashArray(nLen);
424 double* pArray=aDashArray.getArray();
425 for( sal_Int32 i=0; i<nLen; ++i )
426 *pArray++ = readDouble();
427
428 m_parser.m_pSink->setLineDash( aDashArray, nOffset );
429 }
430
readLineJoin()431 void LineParser::readLineJoin()
432 {
433 sal_Int8 nJoin(rendering::PathJoinType::MITER);
434 switch( readInt32() )
435 {
436 default:
437 case 0: nJoin = rendering::PathJoinType::MITER; break;
438 case 1: nJoin = rendering::PathJoinType::ROUND; break;
439 case 2: nJoin = rendering::PathJoinType::BEVEL; break;
440 }
441 m_parser.m_pSink->setLineJoin(nJoin);
442 }
443
readTransformation()444 void LineParser::readTransformation()
445 {
446 geometry::AffineMatrix2D aMat;
447 readDouble(aMat.m00);
448 readDouble(aMat.m10);
449 readDouble(aMat.m01);
450 readDouble(aMat.m11);
451 readDouble(aMat.m02);
452 readDouble(aMat.m12);
453 m_parser.m_pSink->setTransformation( aMat );
454 }
455
readColor()456 rendering::ARGBColor LineParser::readColor()
457 {
458 rendering::ARGBColor aRes;
459 readDouble(aRes.Red);
460 readDouble(aRes.Green);
461 readDouble(aRes.Blue);
462 readDouble(aRes.Alpha);
463 return aRes;
464 }
465
466 /* Parse and convert the font family name (passed from xpdfimport) to correct font names
467 e.g. TimesNewRomanPSMT -> TimesNewRoman
468 TimesNewRomanPS-BoldMT -> TimesNewRoman
469 TimesNewRomanPS-BoldItalicMT -> TimesNewRoman
470 During the conversion, also apply the font features (bold italic etc) to the result.
471
472 TODO: Further convert the font names to real font names in the system rather than the PS names.
473 e.g., TimesNewRoman -> Times New Roman
474 */
parseFontFamilyName(FontAttributes & rResult)475 void LineParser::parseFontFamilyName( FontAttributes& rResult )
476 {
477 SAL_INFO("sdext.pdfimport", "Processing " << rResult.familyName << " ---");
478 rResult.familyName = rResult.familyName.trim();
479 for (const OUString& fontAttributesSuffix: fontAttributesSuffixes)
480 {
481 if ( rResult.familyName.endsWith(fontAttributesSuffix) )
482 {
483 rResult.familyName = rResult.familyName.replaceAll(fontAttributesSuffix, "");
484 SAL_INFO("sdext.pdfimport", rResult.familyName);
485 if (fontAttributesSuffix == u"Heavy" || fontAttributesSuffix == u"Black")
486 {
487 rResult.fontWeight = u"900"_ustr;
488 }
489 else if (fontAttributesSuffix == u"ExtraBold" || fontAttributesSuffix == u"UltraBold")
490 {
491 rResult.fontWeight = u"800"_ustr;
492 }
493 else if (fontAttributesSuffix == u"Bold")
494 {
495 rResult.fontWeight = u"bold"_ustr;
496 }
497 else if (fontAttributesSuffix == u"Semibold")
498 {
499 rResult.fontWeight = u"600"_ustr;
500 }
501 else if (fontAttributesSuffix == u"Medium")
502 {
503 rResult.fontWeight = u"500"_ustr;
504 }
505 else if (fontAttributesSuffix == u"Normal" || fontAttributesSuffix == u"Regular" || fontAttributesSuffix == u"Book")
506 {
507 rResult.fontWeight = u"400"_ustr;
508 }
509 else if (fontAttributesSuffix == u"Light")
510 {
511 rResult.fontWeight = u"300"_ustr;
512 }
513 else if (fontAttributesSuffix == u"ExtraLight" || fontAttributesSuffix == u"UltraLight")
514 {
515 rResult.fontWeight = u"200"_ustr;
516 }
517 else if (fontAttributesSuffix == u"Thin")
518 {
519 rResult.fontWeight = u"100"_ustr;
520 }
521
522 if ( (fontAttributesSuffix == "Italic") or (fontAttributesSuffix == "Oblique") )
523 {
524 rResult.isItalic = true;
525 }
526 }
527 }
528 }
529
readFont()530 void LineParser::readFont()
531 {
532 /*
533 xpdf line is like (separated by space):
534 updateFont <FontID> <isEmbedded> <maFontWeight> <isItalic> <isUnderline> <TransformedFontSize> <nEmbedSize> <FontName>
535 updateFont 14 1 4 0 0 1200.000000 23068 TimesNewRomanPSMT
536
537 If nEmbedSize > 0, then a fontFile is followed as a stream.
538 */
539 sal_Int64 nFontID;
540 sal_Int32 nIsEmbedded;
541 sal_Int32 nFontWeight;
542 sal_Int32 nIsItalic;
543 sal_Int32 nIsUnderline;
544 double nSize;
545 sal_Int32 nFileLen;
546 OString aFontName;
547
548 readInt64(nFontID); // read FontID
549 readInt32(nIsEmbedded); // read isEmbedded
550 readInt32(nFontWeight); // read maFontWeight, see GfxFont enum Weight
551 readInt32(nIsItalic); // read isItalic
552 readInt32(nIsUnderline);// read isUnderline
553 readDouble(nSize); // read TransformedFontSize
554 readInt32(nFileLen); // read nEmbedSize
555
556 nSize = nSize < 0.0 ? -nSize : nSize;
557 // Read FontName. From the current position to the end (any white spaces will be included).
558 aFontName = lcl_unescapeLineFeeds(m_aLine.substr(m_nCharIndex));
559
560 // name gobbles up rest of line
561 m_nCharIndex = std::string_view::npos;
562
563 // Check if this font is already in our font map list.
564 // If yes, update the font size and skip.
565 Parser::FontMapType::const_iterator pFont( m_parser.m_aFontMap.find(nFontID) );
566 if( pFont != m_parser.m_aFontMap.end() )
567 {
568 OSL_PRECOND(nFileLen==0,"font data for known font");
569 FontAttributes aRes(pFont->second);
570 aRes.size = nSize;
571 m_parser.m_pSink->setFont( aRes );
572
573 return;
574 }
575
576 // The font is not yet in the map list - get info and add to map
577 OUString sFontWeight; // font weight name per ODF specifications
578 if (nFontWeight == 0 or nFontWeight == 4) // WeightNotDefined or W400, map to normal font
579 sFontWeight = u"normal"_ustr;
580 else if (nFontWeight == 1) // W100, Thin
581 sFontWeight = u"100"_ustr;
582 else if (nFontWeight == 2) // W200, Extra-Light
583 sFontWeight = u"200"_ustr;
584 else if (nFontWeight == 3) // W300, Light
585 sFontWeight = u"300"_ustr;
586 else if (nFontWeight == 5) // W500, Medium. Is this supported by ODF?
587 sFontWeight = u"500"_ustr;
588 else if (nFontWeight == 6) // W600, Semi-Bold
589 sFontWeight = u"600"_ustr;
590 else if (nFontWeight == 7) // W700, Bold
591 sFontWeight = u"bold"_ustr;
592 else if (nFontWeight == 8) // W800, Extra-Bold
593 sFontWeight = u"800"_ustr;
594 else if (nFontWeight == 9) // W900, Black
595 sFontWeight = u"900"_ustr;
596 SAL_INFO("sdext.pdfimport", "Font weight passed from xpdfimport is: " << sFontWeight);
597
598 FontAttributes aResult( OStringToOUString( aFontName, RTL_TEXTENCODING_UTF8 ),
599 sFontWeight,
600 nIsItalic != 0,
601 nIsUnderline != 0,
602 nSize,
603 1.0);
604
605 /* The above font attributes (fontName, fontWeight, italic) are based on
606 xpdf line output and may not be reliable. To get correct attributes,
607 we do the following:
608 1. Read the embedded font file and determine the attributes based on the
609 font file.
610 2. If we failed to read the font file, or empty result is returned, then
611 determine the font attributes from the font name.
612 3. If all these attempts have failed, then use a fallback font.
613 */
614 if (nFileLen > 0)
615 {
616 uno::Sequence<sal_Int8> aFontFile(nFileLen);
617 readBinaryData(aFontFile); // Read fontFile.
618
619 vcl::Font aFontReadResult = vcl::Font::identifyFont(aFontFile.getArray(), nFileLen);
620 SAL_INFO("sdext.pdfimport", "familyName: " << aFontReadResult.GetFamilyName());
621
622 if (!aFontReadResult.GetFamilyName().isEmpty()) // font detection successful
623 {
624 // Family name
625 aResult.familyName = aFontReadResult.GetFamilyName();
626 SAL_INFO("sdext.pdfimport", aResult.familyName);
627 // tdf#143959: there are cases when the family name returned by font descriptor
628 // is like "AAAAAA+TimesNewRoman,Bold". In this case, use the font name
629 // determined by parseFontFamilyName instead, but still determine the font
630 // attributes (bold italic etc) from the font descriptor.
631 if (aResult.familyName.getLength() > 7 and aResult.familyName.indexOf(u"+", 6) == 6)
632 {
633 aResult.familyName = aResult.familyName.copy(7, aResult.familyName.getLength() - 7);
634 parseFontFamilyName(aResult);
635 }
636 if (aResult.familyName.endsWithIgnoreAsciiCase("-VKana"))
637 {
638 parseFontFamilyName(aResult);
639 }
640
641 // Font weight
642 if (aFontReadResult.GetWeight() == WEIGHT_THIN)
643 aResult.fontWeight = u"100"_ustr;
644 else if (aFontReadResult.GetWeight() == WEIGHT_ULTRALIGHT)
645 aResult.fontWeight = u"200"_ustr;
646 else if (aFontReadResult.GetWeight() == WEIGHT_LIGHT)
647 aResult.fontWeight = u"300"_ustr;
648 else if (aFontReadResult.GetWeight() == WEIGHT_SEMILIGHT)
649 aResult.fontWeight = u"350"_ustr;
650 // no need to check "normal" here as this is default in nFontWeight above
651 else if (aFontReadResult.GetWeight() == WEIGHT_SEMIBOLD)
652 aResult.fontWeight = u"600"_ustr;
653 else if (aFontReadResult.GetWeight() == WEIGHT_BOLD)
654 aResult.fontWeight = u"bold"_ustr;
655 else if (aFontReadResult.GetWeight() == WEIGHT_ULTRABOLD)
656 aResult.fontWeight = u"800"_ustr;
657 else if (aFontReadResult.GetWeight() == WEIGHT_BLACK)
658 aResult.fontWeight = u"900"_ustr;
659 SAL_INFO("sdext.pdfimport", aResult.fontWeight);
660
661 // Italic
662 aResult.isItalic = (aFontReadResult.GetItalic() == ITALIC_OBLIQUE ||
663 aFontReadResult.GetItalic() == ITALIC_NORMAL);
664 } else // font detection failed
665 {
666 SAL_WARN("sdext.pdfimport",
667 "Font detection from fontFile returned empty result. Guessing font info from font name.");
668 parseFontFamilyName(aResult);
669 }
670
671 } else // no embedded font file - guess font attributes from font name
672 {
673 parseFontFamilyName(aResult);
674 }
675
676 // last fallback
677 if (aResult.familyName.isEmpty())
678 {
679 SAL_WARN("sdext.pdfimport", "Failed to determine the font, using a fallback font Arial.");
680 aResult.familyName = "Arial";
681 }
682
683 if (!m_parser.m_xDev)
684 m_parser.m_xDev.disposeAndReset(VclPtr<VirtualDevice>::Create());
685
686 vcl::Font font(aResult.familyName, Size(0, 1000));
687 m_parser.m_xDev->SetFont(font);
688 FontMetric metric(m_parser.m_xDev->GetFontMetric());
689 aResult.ascent = metric.GetAscent() / 1000.0;
690
691 m_parser.m_aFontMap[nFontID] = aResult;
692
693 aResult.size = nSize;
694 m_parser.m_pSink->setFont(aResult);
695 }
696
readImageImpl()697 uno::Sequence<beans::PropertyValue> LineParser::readImageImpl()
698 {
699 std::string_view aToken = readNextToken();
700 const sal_Int32 nImageSize( readInt32() );
701
702 OUString aFileName;
703 if( aToken == "PNG" )
704 aFileName = "DUMMY.PNG";
705 else if( aToken == "JPEG" )
706 aFileName = "DUMMY.JPEG";
707 else if( aToken == "PBM" )
708 aFileName = "DUMMY.PBM";
709 else
710 {
711 SAL_WARN_IF(aToken != "PPM","sdext.pdfimport","Invalid bitmap format");
712 aFileName = "DUMMY.PPM";
713 }
714
715 uno::Sequence<sal_Int8> aDataSequence(nImageSize);
716 readBinaryData( aDataSequence );
717
718 uno::Sequence< uno::Any > aStreamCreationArgs{ uno::Any(aDataSequence) };
719
720 uno::Reference< uno::XComponentContext > xContext( m_parser.m_xContext, uno::UNO_SET_THROW );
721 uno::Reference< lang::XMultiComponentFactory > xFactory( xContext->getServiceManager(), uno::UNO_SET_THROW );
722 uno::Reference< io::XInputStream > xDataStream(
723 xFactory->createInstanceWithArgumentsAndContext( u"com.sun.star.io.SequenceInputStream"_ustr, aStreamCreationArgs, m_parser.m_xContext ),
724 uno::UNO_QUERY_THROW );
725
726 uno::Sequence<beans::PropertyValue> aSequence( comphelper::InitPropertySequence({
727 { "URL", uno::Any(aFileName) },
728 { "InputStream", uno::Any( xDataStream ) },
729 { "InputSequence", uno::Any(aDataSequence) }
730 }));
731
732 return aSequence;
733 }
734
readImage()735 void LineParser::readImage()
736 {
737 sal_Int32 nWidth, nHeight,nMaskColors;
738 readInt32(nWidth);
739 readInt32(nHeight);
740 readInt32(nMaskColors);
741
742 uno::Sequence<beans::PropertyValue> aImg( readImageImpl() );
743
744 if( nMaskColors )
745 {
746 uno::Sequence<sal_Int8> aDataSequence(nMaskColors);
747 readBinaryData( aDataSequence );
748
749 uno::Sequence<double> aMinRange(nMaskColors/2);
750 auto pMinRange = aMinRange.getArray();
751 uno::Sequence<double> aMaxRange(nMaskColors/2);
752 auto pMaxRange = aMaxRange.getArray();
753 for( sal_Int32 i=0; i<nMaskColors/2; ++i )
754 {
755 pMinRange[i] = aDataSequence[i] / 255.0;
756 pMaxRange[i] = aDataSequence[i+nMaskColors/2] / 255.0;
757 }
758
759 uno::Sequence<uno::Any> aMaskRanges{ uno::Any(aMinRange), uno::Any(aMaxRange) };
760 m_parser.m_pSink->drawColorMaskedImage( aImg, aMaskRanges );
761 }
762 else
763 m_parser.m_pSink->drawImage( aImg );
764 }
765
readMask()766 void LineParser::readMask()
767 {
768 sal_Int32 nWidth, nHeight, nInvert;
769 readInt32(nWidth);
770 readInt32(nHeight);
771 readInt32(nInvert);
772
773 m_parser.m_pSink->drawMask( readImageImpl(), nInvert != 0);
774 }
775
readLink()776 void LineParser::readLink()
777 {
778 geometry::RealRectangle2D aBounds;
779 readDouble(aBounds.X1);
780 readDouble(aBounds.Y1);
781 readDouble(aBounds.X2);
782 readDouble(aBounds.Y2);
783
784 m_parser.m_pSink->hyperLink( aBounds,
785 OStringToOUString( lcl_unescapeLineFeeds(
786 m_aLine.substr(m_nCharIndex) ),
787 RTL_TEXTENCODING_UTF8 ) );
788 // name gobbles up rest of line
789 m_nCharIndex = std::string_view::npos;
790 }
791
readMaskedImage()792 void LineParser::readMaskedImage()
793 {
794 sal_Int32 nWidth, nHeight, nMaskWidth, nMaskHeight, nMaskInvert;
795 readInt32(nWidth);
796 readInt32(nHeight);
797 readInt32(nMaskWidth);
798 readInt32(nMaskHeight);
799 readInt32(nMaskInvert);
800
801 const uno::Sequence<beans::PropertyValue> aImage( readImageImpl() );
802 const uno::Sequence<beans::PropertyValue> aMask ( readImageImpl() );
803 m_parser.m_pSink->drawMaskedImage( aImage, aMask, nMaskInvert != 0 );
804 }
805
readSoftMaskedImage()806 void LineParser::readSoftMaskedImage()
807 {
808 sal_Int32 nWidth, nHeight, nMaskWidth, nMaskHeight;
809 readInt32(nWidth);
810 readInt32(nHeight);
811 readInt32(nMaskWidth);
812 readInt32(nMaskHeight);
813
814 const uno::Sequence<beans::PropertyValue> aImage( readImageImpl() );
815 const uno::Sequence<beans::PropertyValue> aMask ( readImageImpl() );
816 m_parser.m_pSink->drawAlphaMaskedImage( aImage, aMask );
817 }
818
readTilingPatternFill()819 void LineParser::readTilingPatternFill()
820 {
821 sal_Int32 nX0, nY0, nX1, nY1, nPaintType;
822 double nXStep, nYStep;
823 geometry::AffineMatrix2D aMat;
824 readInt32(nX0);
825 readInt32(nY0);
826 readInt32(nX1);
827 readInt32(nY1);
828
829 readDouble(nXStep);
830 readDouble(nYStep);
831
832 readInt32(nPaintType);
833
834 readDouble(aMat.m00);
835 readDouble(aMat.m10);
836 readDouble(aMat.m01);
837 readDouble(aMat.m11);
838 readDouble(aMat.m02);
839 readDouble(aMat.m12);
840
841 // The tile is an image with alpha
842 const uno::Sequence<beans::PropertyValue> aTile ( readImageImpl() );
843
844 m_parser.m_pSink->tilingPatternFill( nX0, nY0, nX1, nY1,
845 nXStep, nYStep,
846 nPaintType,
847 aMat,
848 aTile );
849 }
850
parseLine(std::string_view aLine)851 void Parser::parseLine( std::string_view aLine )
852 {
853 OSL_PRECOND( m_pSink, "Invalid sink" );
854 OSL_PRECOND( m_pErr, "Invalid filehandle" );
855 OSL_PRECOND( m_xContext.is(), "Invalid service factory" );
856
857 LineParser lp(*this, aLine);
858 const std::string_view rCmd = lp.readNextToken();
859 const hash_entry* pEntry = PdfKeywordHash::in_word_set( rCmd.data(),
860 rCmd.size() );
861 assert(pEntry);
862 switch( pEntry->eKey )
863 {
864 case CLIPPATH:
865 m_pSink->intersectClip(lp.readPath()); break;
866 case DRAWCHAR:
867 lp.readChar(); break;
868 case DRAWIMAGE:
869 lp.readImage(); break;
870 case DRAWLINK:
871 lp.readLink(); break;
872 case DRAWMASK:
873 lp.readMask(); break;
874 case DRAWMASKEDIMAGE:
875 lp.readMaskedImage(); break;
876 case DRAWSOFTMASKEDIMAGE:
877 lp.readSoftMaskedImage(); break;
878 case ENDPAGE:
879 m_pSink->endPage(); break;
880 case ENDTEXTOBJECT:
881 m_pSink->endText(); break;
882 case EOCLIPPATH:
883 m_pSink->intersectEoClip(lp.readPath()); break;
884 case EOFILLPATH:
885 m_pSink->eoFillPath(lp.readPath()); break;
886 case FILLPATH:
887 m_pSink->fillPath(lp.readPath()); break;
888 case RESTORESTATE:
889 m_pSink->popState(); break;
890 case SAVESTATE:
891 m_pSink->pushState(); break;
892 case SETPAGENUM:
893 m_pSink->setPageNum( lp.readInt32() ); break;
894 case STARTPAGE:
895 {
896 const double nWidth ( lp.readDouble() );
897 const double nHeight( lp.readDouble() );
898 m_pSink->startPage( geometry::RealSize2D( nWidth, nHeight ) );
899 break;
900 }
901 case STROKEPATH:
902 m_pSink->strokePath(lp.readPath()); break;
903 case TILINGPATTERNFILL:
904 lp.readTilingPatternFill(); break;
905 case UPDATECTM:
906 lp.readTransformation(); break;
907 case UPDATEFILLCOLOR:
908 m_pSink->setFillColor( lp.readColor() ); break;
909 case UPDATEFLATNESS:
910 m_pSink->setFlatness( lp.readDouble( ) ); break;
911 case UPDATEFONT:
912 lp.readFont(); break;
913 case UPDATELINECAP:
914 lp.readLineCap(); break;
915 case UPDATELINEDASH:
916 lp.readLineDash(); break;
917 case UPDATELINEJOIN:
918 lp.readLineJoin(); break;
919 case UPDATELINEWIDTH:
920 m_pSink->setLineWidth( lp.readDouble() );break;
921 case UPDATEMITERLIMIT:
922 m_pSink->setMiterLimit( lp.readDouble() ); break;
923 case UPDATESTROKECOLOR:
924 m_pSink->setStrokeColor( lp.readColor() ); break;
925 case UPDATESTROKEOPACITY:
926 break;
927 case SETTEXTRENDERMODE:
928 m_pSink->setTextRenderMode( lp.readInt32() ); break;
929
930 case NONE:
931 default:
932 OSL_PRECOND(false,"Unknown input");
933 break;
934 }
935
936 // all consumed?
937 SAL_WARN_IF(
938 lp.m_nCharIndex!=std::string_view::npos, "sdext.pdfimport", "leftover scanner input");
939 }
940
941 } // namespace
942
checkEncryption(std::u16string_view i_rPath,const uno::Reference<task::XInteractionHandler> & i_xIHdl,OUString & io_rPwd,bool & o_rIsEncrypted,const OUString & i_rDocName)943 static bool checkEncryption( std::u16string_view i_rPath,
944 const uno::Reference< task::XInteractionHandler >& i_xIHdl,
945 OUString& io_rPwd,
946 bool& o_rIsEncrypted,
947 const OUString& i_rDocName
948 )
949 {
950 bool bSuccess = false;
951
952 std::unique_ptr<pdfparse::PDFEntry> pEntry(pdfparse::PDFReader::read(i_rPath));
953 if( pEntry )
954 {
955 pdfparse::PDFFile* pPDFFile = dynamic_cast<pdfparse::PDFFile*>(pEntry.get());
956 if( pPDFFile )
957 {
958 o_rIsEncrypted = pPDFFile->isEncrypted();
959 if( o_rIsEncrypted )
960 {
961 if( pPDFFile->usesSupportedEncryptionFormat() )
962 {
963 bool bAuthenticated = false;
964 if( !io_rPwd.isEmpty() )
965 {
966 OString aIsoPwd = OUStringToOString( io_rPwd,
967 RTL_TEXTENCODING_ISO_8859_1 );
968 bAuthenticated = pPDFFile->setupDecryptionData( aIsoPwd );
969 }
970 if( bAuthenticated )
971 bSuccess = true;
972 else
973 {
974 if( i_xIHdl.is() )
975 {
976 bool bEntered = false;
977 do
978 {
979 bEntered = getPassword( i_xIHdl, io_rPwd, ! bEntered, i_rDocName );
980 OString aIsoPwd = OUStringToOString( io_rPwd,
981 RTL_TEXTENCODING_ISO_8859_1 );
982 bAuthenticated = pPDFFile->setupDecryptionData( aIsoPwd );
983 } while( bEntered && ! bAuthenticated );
984 }
985
986 bSuccess = bAuthenticated;
987 }
988 }
989 else if( i_xIHdl.is() )
990 {
991 reportUnsupportedEncryptionFormat( i_xIHdl );
992 //TODO: this should either be handled further down the
993 // call stack, or else information that this has already
994 // been handled should be passed down the call stack, so
995 // that SfxBaseModel::load does not show an additional
996 // "General Error" message box
997 }
998 }
999 else
1000 bSuccess = true;
1001 }
1002 }
1003 return bSuccess;
1004 }
1005
1006 namespace {
1007
1008 class Buffering
1009 {
1010 static const int SIZE = 64*1024;
1011 std::unique_ptr<char[]> aBuffer;
1012 oslFileHandle& pOut;
1013 size_t pos;
1014 sal_uInt64 left;
1015
1016 public:
Buffering(oslFileHandle & out)1017 explicit Buffering(oslFileHandle& out) : aBuffer(new char[SIZE]), pOut(out), pos(0), left(0) {}
1018
read(char * pChar,short count,sal_uInt64 * pBytesRead)1019 oslFileError read(char *pChar, short count, sal_uInt64* pBytesRead)
1020 {
1021 oslFileError nRes = osl_File_E_None;
1022 sal_uInt64 nBytesRead = 0;
1023 while (count > 0)
1024 {
1025 if (left == 0)
1026 {
1027 nRes = osl_readFile(pOut, aBuffer.get(), SIZE, &left);
1028 if (nRes != osl_File_E_None || left == 0)
1029 {
1030 *pBytesRead = nBytesRead;
1031 return nRes;
1032 }
1033 pos = 0;
1034 }
1035 *pChar = aBuffer.get()[pos];
1036 --count;
1037 ++pos;
1038 --left;
1039 ++pChar;
1040 ++nBytesRead;
1041 }
1042 *pBytesRead = nBytesRead;
1043 return osl_File_E_None;
1044 }
1045 };
1046
1047 }
1048
xpdf_ImportFromFile(const OUString & rURL,const ContentSinkSharedPtr & rSink,const uno::Reference<task::XInteractionHandler> & xIHdl,const OUString & rPwd,const uno::Reference<uno::XComponentContext> & xContext,const OUString & rFilterOptions)1049 bool xpdf_ImportFromFile(const OUString& rURL,
1050 const ContentSinkSharedPtr& rSink,
1051 const uno::Reference<task::XInteractionHandler>& xIHdl,
1052 const OUString& rPwd,
1053 const uno::Reference<uno::XComponentContext>& xContext,
1054 const OUString& rFilterOptions)
1055 {
1056 OSL_ASSERT(rSink);
1057
1058 OUString aSysUPath;
1059 if( osl_getSystemPathFromFileURL( rURL.pData, &aSysUPath.pData ) != osl_File_E_None )
1060 {
1061 SAL_WARN(
1062 "sdext.pdfimport",
1063 "getSystemPathFromFileURL(" << rURL << ") failed");
1064 return false;
1065 }
1066 OUString aDocName( rURL.copy( rURL.lastIndexOf( '/' )+1 ) );
1067
1068 // check for encryption, if necessary get password
1069 OUString aPwd( rPwd );
1070 bool bIsEncrypted = false;
1071 if( !checkEncryption( aSysUPath, xIHdl, aPwd, bIsEncrypted, aDocName ) )
1072 {
1073 SAL_INFO(
1074 "sdext.pdfimport",
1075 "checkEncryption(" << aSysUPath << ") failed");
1076 return false;
1077 }
1078
1079 // Determine xpdfimport executable URL:
1080 OUString converterURL(u"$BRAND_BASE_DIR/" LIBO_BIN_FOLDER "/xpdfimport"_ustr);
1081 rtl::Bootstrap::expandMacros(converterURL); //TODO: detect failure
1082
1083 // spawn separate process to keep LGPL/GPL code apart.
1084
1085 constexpr OUString aOptFlag(u"-o"_ustr);
1086 std::vector<rtl_uString*> args({ aSysUPath.pData });
1087 if (!rFilterOptions.isEmpty())
1088 {
1089 args.push_back(aOptFlag.pData);
1090 args.push_back(rFilterOptions.pData);
1091 }
1092
1093 oslProcess aProcess;
1094 oslFileHandle pIn = nullptr;
1095 oslFileHandle pOut = nullptr;
1096 oslFileHandle pErr = nullptr;
1097 oslSecurity pSecurity = osl_getCurrentSecurity ();
1098 oslProcessError eErr =
1099 osl_executeProcess_WithRedirectedIO(converterURL.pData,
1100 args.data(),
1101 args.size(),
1102 osl_Process_SEARCHPATH|osl_Process_HIDDEN,
1103 pSecurity,
1104 nullptr, nullptr, 0,
1105 &aProcess, &pIn, &pOut, &pErr);
1106 osl_freeSecurityHandle(pSecurity);
1107
1108 bool bRet=true;
1109 try
1110 {
1111 if( eErr!=osl_Process_E_None )
1112 {
1113 SAL_WARN(
1114 "sdext.pdfimport",
1115 "executeProcess of " << converterURL << " failed with "
1116 << +eErr);
1117 return false;
1118 }
1119
1120 if( pIn )
1121 {
1122 OStringBuffer aBuf(256);
1123 if( bIsEncrypted )
1124 aBuf.append( OUStringToOString( aPwd, RTL_TEXTENCODING_ISO_8859_1 ) );
1125 aBuf.append( '\n' );
1126
1127 sal_uInt64 nWritten = 0;
1128 osl_writeFile( pIn, aBuf.getStr(), sal_uInt64(aBuf.getLength()), &nWritten );
1129 }
1130
1131 if( pOut && pErr )
1132 {
1133 // read results of PDF parser. One line - one call to
1134 // OutputDev. stderr is used for alternate streams, like
1135 // embedded fonts and bitmaps
1136 Parser aParser(rSink,pErr,xContext);
1137 Buffering aBuffering(pOut);
1138 OStringBuffer line;
1139 for( ;; )
1140 {
1141 char aChar('\n');
1142 sal_uInt64 nBytesRead;
1143 oslFileError nRes;
1144
1145 // skip garbage \r \n at start of line
1146 for (;;)
1147 {
1148 nRes = aBuffering.read(&aChar, 1, &nBytesRead);
1149 if (osl_File_E_None != nRes || nBytesRead != 1 || (aChar != '\n' && aChar != '\r') )
1150 break;
1151 }
1152 if ( osl_File_E_None != nRes )
1153 break;
1154
1155 if( aChar != '\n' && aChar != '\r' )
1156 line.append( aChar );
1157
1158 for (;;)
1159 {
1160 nRes = aBuffering.read(&aChar, 1, &nBytesRead);
1161 if ( osl_File_E_None != nRes || nBytesRead != 1 || aChar == '\n' || aChar == '\r' )
1162 break;
1163 line.append( aChar );
1164 }
1165 if ( osl_File_E_None != nRes )
1166 break;
1167 if ( line.isEmpty() )
1168 break;
1169
1170 aParser.parseLine(line);
1171 line.setLength(0);
1172 }
1173 }
1174 }
1175 catch( uno::Exception& )
1176 {
1177 // crappy C file interface. need manual resource dealloc
1178 bRet = false;
1179 }
1180
1181 if( pIn )
1182 osl_closeFile(pIn);
1183 if( pOut )
1184 osl_closeFile(pOut);
1185 if( pErr )
1186 osl_closeFile(pErr);
1187 eErr = osl_joinProcess(aProcess);
1188 if (eErr == osl_Process_E_None)
1189 {
1190 oslProcessInfo info;
1191 info.Size = sizeof info;
1192 eErr = osl_getProcessInfo(aProcess, osl_Process_EXITCODE, &info);
1193 if (eErr == osl_Process_E_None)
1194 {
1195 if (info.Code != 0)
1196 {
1197 SAL_WARN(
1198 "sdext.pdfimport",
1199 "getProcessInfo of " << converterURL
1200 << " failed with exit code " << info.Code);
1201 // TODO: use xIHdl and/or exceptions to inform the user; see poppler/ErrorCodes.h
1202 bRet = false;
1203 }
1204 }
1205 else
1206 {
1207 SAL_WARN(
1208 "sdext.pdfimport",
1209 "getProcessInfo of " << converterURL << " failed with "
1210 << +eErr);
1211 bRet = false;
1212 }
1213 }
1214 else
1215 {
1216 SAL_WARN(
1217 "sdext.pdfimport",
1218 "joinProcess of " << converterURL << " failed with " << +eErr);
1219 bRet = false;
1220 }
1221 osl_freeProcessHandle(aProcess);
1222 return bRet;
1223 }
1224
1225
xpdf_ImportFromStream(const uno::Reference<io::XInputStream> & xInput,const ContentSinkSharedPtr & rSink,const uno::Reference<task::XInteractionHandler> & xIHdl,const OUString & rPwd,const uno::Reference<uno::XComponentContext> & xContext,const OUString & rFilterOptions)1226 bool xpdf_ImportFromStream( const uno::Reference< io::XInputStream >& xInput,
1227 const ContentSinkSharedPtr& rSink,
1228 const uno::Reference<task::XInteractionHandler >& xIHdl,
1229 const OUString& rPwd,
1230 const uno::Reference< uno::XComponentContext >& xContext,
1231 const OUString& rFilterOptions )
1232 {
1233 OSL_ASSERT(xInput.is());
1234 OSL_ASSERT(rSink);
1235
1236 // convert XInputStream to local temp file
1237 oslFileHandle aFile = nullptr;
1238 OUString aURL;
1239 if( osl_createTempFile( nullptr, &aFile, &aURL.pData ) != osl_File_E_None )
1240 return false;
1241
1242 // copy content, buffered...
1243 const sal_uInt32 nBufSize = 4096;
1244 uno::Sequence<sal_Int8> aBuf( nBufSize );
1245 sal_uInt64 nBytes = 0;
1246 sal_uInt64 nWritten = 0;
1247 bool bSuccess = true;
1248 do
1249 {
1250 try
1251 {
1252 nBytes = xInput->readBytes( aBuf, nBufSize );
1253 }
1254 catch( css::uno::Exception& )
1255 {
1256 osl_closeFile( aFile );
1257 throw;
1258 }
1259 if( nBytes > 0 )
1260 {
1261 osl_writeFile( aFile, aBuf.getConstArray(), nBytes, &nWritten );
1262 if( nWritten != nBytes )
1263 {
1264 bSuccess = false;
1265 break;
1266 }
1267 }
1268 }
1269 while( nBytes == nBufSize );
1270
1271 osl_closeFile( aFile );
1272
1273 if ( bSuccess )
1274 bSuccess = xpdf_ImportFromFile( aURL, rSink, xIHdl, rPwd, xContext, rFilterOptions );
1275 osl_removeFile( aURL.pData );
1276
1277 return bSuccess;
1278 }
1279
1280 }
1281
1282 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1283