diff options
Diffstat (limited to 'vcl/source/gdi/pdfwriter_impl.cxx')
-rw-r--r-- | vcl/source/gdi/pdfwriter_impl.cxx | 11212 |
1 files changed, 11212 insertions, 0 deletions
diff --git a/vcl/source/gdi/pdfwriter_impl.cxx b/vcl/source/gdi/pdfwriter_impl.cxx new file mode 100644 index 000000000..7e02762d6 --- /dev/null +++ b/vcl/source/gdi/pdfwriter_impl.cxx @@ -0,0 +1,11212 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_features.h> + +#include <sal/types.h> + +#include <math.h> +#include <algorithm> + +#include <lcms2.h> + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <memory> +#include <com/sun/star/io/XOutputStream.hpp> +#include <com/sun/star/util/URL.hpp> +#include <com/sun/star/util/URLTransformer.hpp> +#include <comphelper/processfactory.hxx> +#include <comphelper/string.hxx> +#include <cppuhelper/implbase.hxx> +#include <i18nlangtag/languagetag.hxx> +#include <o3tl/numeric.hxx> +#include <officecfg/Office/Common.hxx> +#include <osl/file.hxx> +#include <osl/thread.h> +#include <rtl/digest.h> +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> +#include <svl/urihelper.hxx> +#include <tools/fract.hxx> +#include <tools/helpers.hxx> +#include <tools/stream.hxx> +#include <tools/urlobj.hxx> +#include <tools/zcodec.hxx> +#include <svl/cryptosign.hxx> +#include <vcl/bitmapex.hxx> +#include <vcl/bitmapaccess.hxx> +#include <vcl/canvastools.hxx> +#include <vcl/cvtgrf.hxx> +#include <vcl/fontcharmap.hxx> +#include <vcl/lineinfo.hxx> +#include <vcl/metric.hxx> +#include <vcl/settings.hxx> +#include <strhelper.hxx> +#include <vcl/svapp.hxx> +#include <vcl/virdev.hxx> +#include <vcl/filter/pdfdocument.hxx> +#include <comphelper/hash.hxx> + +#include <fontsubset.hxx> +#include <PhysicalFontFace.hxx> +#include <salgdi.hxx> +#include <textlayout.hxx> +#include <textlineinfo.hxx> +#include <bitmapwriteaccess.hxx> +#include <impglyphitem.hxx> +#include <pdf/XmpMetadata.hxx> + +#include "pdfwriter_impl.hxx" + +#ifdef _WIN32 +// WinCrypt headers for PDF signing +// Note: this uses Windows 7 APIs and requires the relevant data types +#include <prewin.h> +#include <wincrypt.h> +#include <postwin.h> +#endif + +#include <config_eot.h> + +#if ENABLE_EOT +#include <libeot/libeot.h> +#endif + +using namespace vcl; +using namespace::com::sun::star; + +static bool g_bDebugDisableCompression = getenv("VCL_DEBUG_DISABLE_PDFCOMPRESSION"); + +#if HAVE_FEATURE_NSS +// Is this length truly the maximum possible, or just a number that +// seemed large enough when the author tested this (with some type of +// certificates)? I suspect the latter. + +// Used to be 0x4000 = 16384, but a sample signed PDF (produced by +// some other software) provided by the customer has a signature +// content that is 30000 bytes. The SampleSignedPDFDocument.pdf from +// Adobe has one that is 21942 bytes. So let's be careful. Pity this +// can't be dynamic, at least not without restructuring the code. Also +// note that the checks in the code for this being too small +// apparently are broken, if this overflows you end up with an invalid +// PDF. Need to fix that. + +#define MAX_SIGNATURE_CONTENT_LENGTH 50000 +#endif + +static const sal_Int32 nLog10Divisor = 3; +static const double fDivisor = 1000.0; + +static double pixelToPoint( double px ) { return px/fDivisor; } +static sal_Int32 pointToPixel( double pt ) { return sal_Int32(pt*fDivisor); } + +const sal_uInt8 PDFWriterImpl::s_nPadString[32] = +{ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A +}; + +static void appendHex( sal_Int8 nInt, OStringBuffer& rBuffer ) +{ + static const char pHexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + rBuffer.append( pHexDigits[ (nInt >> 4) & 15 ] ); + rBuffer.append( pHexDigits[ nInt & 15 ] ); +} + +static void appendName( const OUString& rStr, OStringBuffer& rBuffer ) +{ +// FIXME i59651 add a check for max length of 127 chars? Per PDF spec 1.4, appendix C.1 +// I guess than when reading the #xx sequence it will count for a single character. + OString aStr( OUStringToOString( rStr, RTL_TEXTENCODING_UTF8 ) ); + int nLen = aStr.getLength(); + for( int i = 0; i < nLen; i++ ) + { + /* #i16920# PDF recommendation: output UTF8, any byte + * outside the interval [33(=ASCII'!');126(=ASCII'~')] + * should be escaped hexadecimal + * for the sake of ghostscript which also reads PDF + * but has a narrower acceptance rate we only pass + * alphanumerics and '-' literally. + */ + if( (aStr[i] >= 'A' && aStr[i] <= 'Z' ) || + (aStr[i] >= 'a' && aStr[i] <= 'z' ) || + (aStr[i] >= '0' && aStr[i] <= '9' ) || + aStr[i] == '-' ) + { + rBuffer.append( aStr[i] ); + } + else + { + rBuffer.append( '#' ); + appendHex( static_cast<sal_Int8>(aStr[i]), rBuffer ); + } + } +} + +static void appendName( const char* pStr, OStringBuffer& rBuffer ) +{ + // FIXME i59651 see above + while( pStr && *pStr ) + { + if( (*pStr >= 'A' && *pStr <= 'Z' ) || + (*pStr >= 'a' && *pStr <= 'z' ) || + (*pStr >= '0' && *pStr <= '9' ) || + *pStr == '-' ) + { + rBuffer.append( *pStr ); + } + else + { + rBuffer.append( '#' ); + appendHex( static_cast<sal_Int8>(*pStr), rBuffer ); + } + pStr++; + } +} + +//used only to emit encoded passwords +static void appendLiteralString( const char* pStr, sal_Int32 nLength, OStringBuffer& rBuffer ) +{ + while( nLength ) + { + switch( *pStr ) + { + case '\n' : + rBuffer.append( "\\n" ); + break; + case '\r' : + rBuffer.append( "\\r" ); + break; + case '\t' : + rBuffer.append( "\\t" ); + break; + case '\b' : + rBuffer.append( "\\b" ); + break; + case '\f' : + rBuffer.append( "\\f" ); + break; + case '(' : + case ')' : + case '\\' : + rBuffer.append( "\\" ); + rBuffer.append( static_cast<char>(*pStr) ); + break; + default: + rBuffer.append( static_cast<char>(*pStr) ); + break; + } + pStr++; + nLength--; + } +} + +/* + * Convert a string before using it. + * + * This string conversion function is needed because the destination name + * in a PDF file seen through an Internet browser should be + * specially crafted, in order to be used directly by the browser. + * In this way the fragment part of a hyperlink to a PDF file (e.g. something + * as 'test1/test2/a-file.pdf\#thefragment) will be (hopefully) interpreted by the + * PDF reader (currently only Adobe Reader plug-in seems to be working that way) called + * from inside the Internet browser as: 'open the file test1/test2/a-file.pdf + * and go to named destination thefragment using default zoom'. + * The conversion is needed because in case of a fragment in the form: Slide%201 + * (meaning Slide 1) as it is converted obeying the Inet rules, it will become Slide25201 + * using this conversion, in both the generated named destinations, fragment and GoToR + * destination. + * + * The names for destinations are name objects and so they don't need to be encrypted + * even though they expose the content of PDF file (e.g. guessing the PDF content from the + * destination name). + * + * Further limitation: it is advisable to use standard ASCII characters for + * OOo bookmarks. +*/ +static void appendDestinationName( const OUString& rString, OStringBuffer& rBuffer ) +{ + const sal_Unicode* pStr = rString.getStr(); + sal_Int32 nLen = rString.getLength(); + for( int i = 0; i < nLen; i++ ) + { + sal_Unicode aChar = pStr[i]; + if( (aChar >= '0' && aChar <= '9' ) || + (aChar >= 'a' && aChar <= 'z' ) || + (aChar >= 'A' && aChar <= 'Z' ) || + aChar == '-' ) + { + rBuffer.append(static_cast<char>(aChar)); + } + else + { + sal_Int8 aValueHigh = sal_Int8(aChar >> 8); + if(aValueHigh > 0) + appendHex( aValueHigh, rBuffer ); + appendHex( static_cast<sal_Int8>(aChar & 255 ), rBuffer ); + } + } +} + +void PDFWriter::AppendUnicodeTextString(const OUString& rString, OStringBuffer& rBuffer) +{ + rBuffer.append( "FEFF" ); + const sal_Unicode* pStr = rString.getStr(); + sal_Int32 nLen = rString.getLength(); + for( int i = 0; i < nLen; i++ ) + { + sal_Unicode aChar = pStr[i]; + appendHex( static_cast<sal_Int8>(aChar >> 8), rBuffer ); + appendHex( static_cast<sal_Int8>(aChar & 255 ), rBuffer ); + } +} + +void PDFWriterImpl::createWidgetFieldName( sal_Int32 i_nWidgetIndex, const PDFWriter::AnyWidget& i_rControl ) +{ + /* #i80258# previously we use appendName here + however we need a slightly different coding scheme than the normal + name encoding for field names + */ + const OUString& rName = (m_aContext.Version > PDFWriter::PDFVersion::PDF_1_2) ? i_rControl.Name : i_rControl.Text; + OString aStr( OUStringToOString( rName, RTL_TEXTENCODING_UTF8 ) ); + int nLen = aStr.getLength(); + + OStringBuffer aBuffer( rName.getLength()+64 ); + for( int i = 0; i < nLen; i++ ) + { + /* #i16920# PDF recommendation: output UTF8, any byte + * outside the interval [32(=ASCII' ');126(=ASCII'~')] + * should be escaped hexadecimal + */ + if( aStr[i] >= 32 && aStr[i] <= 126 ) + aBuffer.append( aStr[i] ); + else + { + aBuffer.append( '#' ); + appendHex( static_cast<sal_Int8>(aStr[i]), aBuffer ); + } + } + + OString aFullName( aBuffer.makeStringAndClear() ); + + /* #i82785# create hierarchical fields down to the for each dot in i_rName */ + sal_Int32 nTokenIndex = 0, nLastTokenIndex = 0; + OString aPartialName; + OString aDomain; + do + { + nLastTokenIndex = nTokenIndex; + aPartialName = aFullName.getToken( 0, '.', nTokenIndex ); + if( nTokenIndex != -1 ) + { + // find or create a hierarchical field + // first find the fully qualified name up to this field + aDomain = aFullName.copy( 0, nTokenIndex-1 ); + std::unordered_map< OString, sal_Int32 >::const_iterator it = m_aFieldNameMap.find( aDomain ); + if( it == m_aFieldNameMap.end() ) + { + // create new hierarchy field + sal_Int32 nNewWidget = m_aWidgets.size(); + m_aWidgets.emplace_back( ); + m_aWidgets[nNewWidget].m_nObject = createObject(); + m_aWidgets[nNewWidget].m_eType = PDFWriter::Hierarchy; + m_aWidgets[nNewWidget].m_aName = aPartialName; + m_aWidgets[i_nWidgetIndex].m_nParent = m_aWidgets[nNewWidget].m_nObject; + m_aFieldNameMap[aDomain] = nNewWidget; + m_aWidgets[i_nWidgetIndex].m_nParent = m_aWidgets[nNewWidget].m_nObject; + if( nLastTokenIndex > 0 ) + { + // this field is not a root field and + // needs to be inserted to its parent + OString aParentDomain( aDomain.copy( 0, nLastTokenIndex-1 ) ); + it = m_aFieldNameMap.find( aParentDomain ); + OSL_ENSURE( it != m_aFieldNameMap.end(), "field name not found" ); + if( it != m_aFieldNameMap.end() ) + { + OSL_ENSURE( it->second < sal_Int32(m_aWidgets.size()), "invalid field number entry" ); + if( it->second < sal_Int32(m_aWidgets.size()) ) + { + PDFWidget& rParentField( m_aWidgets[it->second] ); + rParentField.m_aKids.push_back( m_aWidgets[nNewWidget].m_nObject ); + rParentField.m_aKidsIndex.push_back( nNewWidget ); + m_aWidgets[nNewWidget].m_nParent = rParentField.m_nObject; + } + } + } + } + else if( m_aWidgets[it->second].m_eType != PDFWriter::Hierarchy ) + { + // this is invalid, someone tries to have a terminal field as parent + // example: a button with the name foo.bar exists and + // another button is named foo.bar.no + // workaround: put the second terminal field as much up in the hierarchy as + // necessary to have a non-terminal field as parent (or none at all) + // since it->second already is terminal, we just need to use its parent + aDomain.clear(); + aPartialName = aFullName.copy( aFullName.lastIndexOf( '.' )+1 ); + if( nLastTokenIndex > 0 ) + { + aDomain = aFullName.copy( 0, nLastTokenIndex-1 ); + aFullName = aDomain + "." + aPartialName; + } + else + aFullName = aPartialName; + break; + } + } + } while( nTokenIndex != -1 ); + + // insert widget into its hierarchy field + if( !aDomain.isEmpty() ) + { + std::unordered_map< OString, sal_Int32 >::const_iterator it = m_aFieldNameMap.find( aDomain ); + if( it != m_aFieldNameMap.end() ) + { + OSL_ENSURE( it->second >= 0 && it->second < sal_Int32( m_aWidgets.size() ), "invalid field index" ); + if( it->second >= 0 && it->second < sal_Int32(m_aWidgets.size()) ) + { + m_aWidgets[i_nWidgetIndex].m_nParent = m_aWidgets[it->second].m_nObject; + m_aWidgets[it->second].m_aKids.push_back( m_aWidgets[i_nWidgetIndex].m_nObject); + m_aWidgets[it->second].m_aKidsIndex.push_back( i_nWidgetIndex ); + } + } + } + + if( aPartialName.isEmpty() ) + { + // how funny, an empty field name + if( i_rControl.getType() == PDFWriter::RadioButton ) + { + aPartialName = "RadioGroup" + + OString::number( static_cast<const PDFWriter::RadioButtonWidget&>(i_rControl).RadioGroup ); + } + else + aPartialName = OString( "Widget" ); + } + + if( ! m_aContext.AllowDuplicateFieldNames ) + { + std::unordered_map<OString, sal_Int32>::iterator it = m_aFieldNameMap.find( aFullName ); + + if( it != m_aFieldNameMap.end() ) // not unique + { + std::unordered_map< OString, sal_Int32 >::const_iterator check_it; + OString aTry; + sal_Int32 nTry = 2; + do + { + OStringBuffer aUnique( aFullName.getLength() + 16 ); + aUnique.append( aFullName ); + aUnique.append( '_' ); + aUnique.append( nTry++ ); + aTry = aUnique.makeStringAndClear(); + check_it = m_aFieldNameMap.find( aTry ); + } while( check_it != m_aFieldNameMap.end() ); + aFullName = aTry; + m_aFieldNameMap[ aFullName ] = i_nWidgetIndex; + aPartialName = aFullName.copy( aFullName.lastIndexOf( '.' )+1 ); + } + else + m_aFieldNameMap[ aFullName ] = i_nWidgetIndex; + } + + // finally + m_aWidgets[i_nWidgetIndex].m_aName = aPartialName; +} + +static void appendFixedInt( sal_Int32 nValue, OStringBuffer& rBuffer ) +{ + if( nValue < 0 ) + { + rBuffer.append( '-' ); + nValue = -nValue; + } + sal_Int32 nFactor = 1, nDiv = nLog10Divisor; + while( nDiv-- ) + nFactor *= 10; + + sal_Int32 nInt = nValue / nFactor; + rBuffer.append( nInt ); + if (nFactor > 1 && nValue % nFactor) + { + rBuffer.append( '.' ); + do + { + nFactor /= 10; + rBuffer.append((nValue / nFactor) % 10); + } + while (nFactor > 1 && nValue % nFactor); // omit trailing zeros + } +} + +// appends a double. PDF does not accept exponential format, only fixed point +static void appendDouble( double fValue, OStringBuffer& rBuffer, sal_Int32 nPrecision = 5 ) +{ + bool bNeg = false; + if( fValue < 0.0 ) + { + bNeg = true; + fValue=-fValue; + } + + sal_Int64 nInt = static_cast<sal_Int64>(fValue); + fValue -= static_cast<double>(nInt); + // optimizing hardware may lead to a value of 1.0 after the subtraction + if( rtl::math::approxEqual(fValue, 1.0) || log10( 1.0-fValue ) <= -nPrecision ) + { + nInt++; + fValue = 0.0; + } + sal_Int64 nFrac = 0; + if( fValue ) + { + fValue *= pow( 10.0, static_cast<double>(nPrecision) ); + nFrac = static_cast<sal_Int64>(fValue); + } + if( bNeg && ( nInt || nFrac ) ) + rBuffer.append( '-' ); + rBuffer.append( nInt ); + if( nFrac ) + { + int i; + rBuffer.append( '.' ); + sal_Int64 nBound = static_cast<sal_Int64>(pow( 10.0, nPrecision - 1.0 )+0.5); + for ( i = 0; ( i < nPrecision ) && nFrac; i++ ) + { + sal_Int64 nNumb = nFrac / nBound; + nFrac -= nNumb * nBound; + rBuffer.append( nNumb ); + nBound /= 10; + } + } +} + +static void appendColor( const Color& rColor, OStringBuffer& rBuffer, bool bConvertToGrey ) +{ + + if( rColor != COL_TRANSPARENT ) + { + if( bConvertToGrey ) + { + sal_uInt8 cByte = rColor.GetLuminance(); + appendDouble( static_cast<double>(cByte) / 255.0, rBuffer ); + } + else + { + appendDouble( static_cast<double>(rColor.GetRed()) / 255.0, rBuffer ); + rBuffer.append( ' ' ); + appendDouble( static_cast<double>(rColor.GetGreen()) / 255.0, rBuffer ); + rBuffer.append( ' ' ); + appendDouble( static_cast<double>(rColor.GetBlue()) / 255.0, rBuffer ); + } + } +} + +void PDFWriterImpl::appendStrokingColor( const Color& rColor, OStringBuffer& rBuffer ) +{ + if( rColor != COL_TRANSPARENT ) + { + bool bGrey = m_aContext.ColorMode == PDFWriter::DrawGreyscale; + appendColor( rColor, rBuffer, bGrey ); + rBuffer.append( bGrey ? " G" : " RG" ); + } +} + +void PDFWriterImpl::appendNonStrokingColor( const Color& rColor, OStringBuffer& rBuffer ) +{ + if( rColor != COL_TRANSPARENT ) + { + bool bGrey = m_aContext.ColorMode == PDFWriter::DrawGreyscale; + appendColor( rColor, rBuffer, bGrey ); + rBuffer.append( bGrey ? " g" : " rg" ); + } +} + +PDFPage::PDFPage( PDFWriterImpl* pWriter, double nPageWidth, double nPageHeight, PDFWriter::Orientation eOrientation ) + : + m_pWriter( pWriter ), + m_nPageWidth( nPageWidth ), + m_nPageHeight( nPageHeight ), + m_nUserUnit( 1 ), + m_eOrientation( eOrientation ), + m_nPageObject( 0 ), // invalid object number + m_nStreamLengthObject( 0 ), + m_nBeginStreamPos( 0 ), + m_eTransition( PDFWriter::PageTransition::Regular ), + m_nTransTime( 0 ) +{ + // object ref must be only ever updated in emit() + m_nPageObject = m_pWriter->createObject(); + + switch (m_pWriter->m_aContext.Version) + { + case PDFWriter::PDFVersion::PDF_1_6: + m_nUserUnit = std::ceil(std::max(nPageWidth, nPageHeight) / 14400.0); + break; + default: + // 1.2 -> 1.5 + break; + } +} + +void PDFPage::beginStream() +{ + if (g_bDebugDisableCompression) + { + m_pWriter->emitComment("PDFWriterImpl::PDFPage::beginStream, +"); + } + m_aStreamObjects.push_back(m_pWriter->createObject()); + if( ! m_pWriter->updateObject( m_aStreamObjects.back() ) ) + return; + + m_nStreamLengthObject = m_pWriter->createObject(); + // write content stream header + OStringBuffer aLine; + aLine.append( m_aStreamObjects.back() ); + aLine.append( " 0 obj\n<</Length " ); + aLine.append( m_nStreamLengthObject ); + aLine.append( " 0 R" ); + if (!g_bDebugDisableCompression) + aLine.append( "/Filter/FlateDecode" ); + aLine.append( ">>\nstream\n" ); + if( ! m_pWriter->writeBuffer( aLine.getStr(), aLine.getLength() ) ) + return; + if (osl::File::E_None != m_pWriter->m_aFile.getPos(m_nBeginStreamPos)) + { + m_pWriter->m_aFile.close(); + m_pWriter->m_bOpen = false; + } + if (!g_bDebugDisableCompression) + m_pWriter->beginCompression(); + m_pWriter->checkAndEnableStreamEncryption( m_aStreamObjects.back() ); +} + +void PDFPage::endStream() +{ + if (!g_bDebugDisableCompression) + m_pWriter->endCompression(); + sal_uInt64 nEndStreamPos; + if (osl::File::E_None != m_pWriter->m_aFile.getPos(nEndStreamPos)) + { + m_pWriter->m_aFile.close(); + m_pWriter->m_bOpen = false; + return; + } + m_pWriter->disableStreamEncryption(); + if( ! m_pWriter->writeBuffer( "\nendstream\nendobj\n\n", 19 ) ) + return; + // emit stream length object + if( ! m_pWriter->updateObject( m_nStreamLengthObject ) ) + return; + OString aLine = + OString::number( m_nStreamLengthObject ) + + " 0 obj\n" + + OString::number( static_cast<sal_Int64>(nEndStreamPos-m_nBeginStreamPos) ) + + "\nendobj\n\n"; + m_pWriter->writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +bool PDFPage::emit(sal_Int32 nParentObject ) +{ + m_pWriter->MARK("PDFPage::emit"); + // emit page object + if( ! m_pWriter->updateObject( m_nPageObject ) ) + return false; + OStringBuffer aLine; + + aLine.append( m_nPageObject ); + aLine.append( " 0 obj\n" + "<</Type/Page/Parent " ); + aLine.append( nParentObject ); + aLine.append( " 0 R" ); + aLine.append( "/Resources " ); + aLine.append( m_pWriter->getResourceDictObj() ); + aLine.append( " 0 R" ); + if( m_nPageWidth && m_nPageHeight ) + { + aLine.append( "/MediaBox[0 0 " ); + aLine.append(m_nPageWidth / m_nUserUnit); + aLine.append( ' ' ); + aLine.append(m_nPageHeight / m_nUserUnit); + aLine.append( "]" ); + if (m_nUserUnit > 1) + { + aLine.append("\n/UserUnit "); + aLine.append(m_nUserUnit); + } + } + switch( m_eOrientation ) + { + case PDFWriter::Orientation::Portrait: aLine.append( "/Rotate 0\n" );break; + case PDFWriter::Orientation::Inherit: break; + } + int nAnnots = m_aAnnotations.size(); + if( nAnnots > 0 ) + { + aLine.append( "/Annots[\n" ); + for( int i = 0; i < nAnnots; i++ ) + { + aLine.append( m_aAnnotations[i] ); + aLine.append( " 0 R" ); + aLine.append( ((i+1)%15) ? " " : "\n" ); + } + aLine.append( "]\n" ); + } + if( !m_aMCIDParents.empty() ) + { + OStringBuffer aStructParents( 1024 ); + aStructParents.append( "[ " ); + int nParents = m_aMCIDParents.size(); + for( int i = 0; i < nParents; i++ ) + { + aStructParents.append( m_aMCIDParents[i] ); + aStructParents.append( " 0 R" ); + aStructParents.append( ((i%10) == 9) ? "\n" : " " ); + } + aStructParents.append( "]" ); + m_pWriter->m_aStructParentTree.push_back( aStructParents.makeStringAndClear() ); + + aLine.append( "/StructParents " ); + aLine.append( sal_Int32(m_pWriter->m_aStructParentTree.size()-1) ); + aLine.append( "\n" ); + } + if( m_eTransition != PDFWriter::PageTransition::Regular && m_nTransTime > 0 ) + { + // transition duration + aLine.append( "/Trans<</D " ); + appendDouble( static_cast<double>(m_nTransTime)/1000.0, aLine, 3 ); + aLine.append( "\n" ); + const char *pStyle = nullptr, *pDm = nullptr, *pM = nullptr, *pDi = nullptr; + switch( m_eTransition ) + { + case PDFWriter::PageTransition::SplitHorizontalInward: + pStyle = "Split"; pDm = "H"; pM = "I"; break; + case PDFWriter::PageTransition::SplitHorizontalOutward: + pStyle = "Split"; pDm = "H"; pM = "O"; break; + case PDFWriter::PageTransition::SplitVerticalInward: + pStyle = "Split"; pDm = "V"; pM = "I"; break; + case PDFWriter::PageTransition::SplitVerticalOutward: + pStyle = "Split"; pDm = "V"; pM = "O"; break; + case PDFWriter::PageTransition::BlindsHorizontal: + pStyle = "Blinds"; pDm = "H"; break; + case PDFWriter::PageTransition::BlindsVertical: + pStyle = "Blinds"; pDm = "V"; break; + case PDFWriter::PageTransition::BoxInward: + pStyle = "Box"; pM = "I"; break; + case PDFWriter::PageTransition::BoxOutward: + pStyle = "Box"; pM = "O"; break; + case PDFWriter::PageTransition::WipeLeftToRight: + pStyle = "Wipe"; pDi = "0"; break; + case PDFWriter::PageTransition::WipeBottomToTop: + pStyle = "Wipe"; pDi = "90"; break; + case PDFWriter::PageTransition::WipeRightToLeft: + pStyle = "Wipe"; pDi = "180"; break; + case PDFWriter::PageTransition::WipeTopToBottom: + pStyle = "Wipe"; pDi = "270"; break; + case PDFWriter::PageTransition::Dissolve: + pStyle = "Dissolve"; break; + case PDFWriter::PageTransition::Regular: + break; + } + // transition style + if( pStyle ) + { + aLine.append( "/S/" ); + aLine.append( pStyle ); + aLine.append( "\n" ); + } + if( pDm ) + { + aLine.append( "/Dm/" ); + aLine.append( pDm ); + aLine.append( "\n" ); + } + if( pM ) + { + aLine.append( "/M/" ); + aLine.append( pM ); + aLine.append( "\n" ); + } + if( pDi ) + { + aLine.append( "/Di " ); + aLine.append( pDi ); + aLine.append( "\n" ); + } + aLine.append( ">>\n" ); + } + if( m_pWriter->getVersion() > PDFWriter::PDFVersion::PDF_1_3 && ! m_pWriter->m_bIsPDF_A1 ) + { + aLine.append( "/Group<</S/Transparency/CS/DeviceRGB/I true>>" ); + } + aLine.append( "/Contents" ); + unsigned int nStreamObjects = m_aStreamObjects.size(); + if( nStreamObjects > 1 ) + aLine.append( '[' ); + for(sal_Int32 i : m_aStreamObjects) + { + aLine.append( ' ' ); + aLine.append( i ); + aLine.append( " 0 R" ); + } + if( nStreamObjects > 1 ) + aLine.append( ']' ); + aLine.append( ">>\nendobj\n\n" ); + return m_pWriter->writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +namespace vcl +{ +template < class GEOMETRY > +static GEOMETRY lcl_convert( const MapMode& _rSource, const MapMode& _rDest, OutputDevice* _pPixelConversion, const GEOMETRY& _rObject ) +{ + GEOMETRY aPoint; + if ( MapUnit::MapPixel == _rSource.GetMapUnit() ) + { + aPoint = _pPixelConversion->PixelToLogic( _rObject, _rDest ); + } + else + { + aPoint = OutputDevice::LogicToLogic( _rObject, _rSource, _rDest ); + } + return aPoint; +} +} + +void PDFPage::appendPoint( const Point& rPoint, OStringBuffer& rBuffer ) const +{ + Point aPoint( lcl_convert( m_pWriter->m_aGraphicsStack.front().m_aMapMode, + m_pWriter->m_aMapMode, + m_pWriter, + rPoint ) ); + + sal_Int32 nValue = aPoint.X(); + + appendFixedInt( nValue, rBuffer ); + + rBuffer.append( ' ' ); + + nValue = pointToPixel(getHeight()) - aPoint.Y(); + + appendFixedInt( nValue, rBuffer ); +} + +void PDFPage::appendPixelPoint( const basegfx::B2DPoint& rPoint, OStringBuffer& rBuffer ) const +{ + double fValue = pixelToPoint(rPoint.getX()); + + appendDouble( fValue, rBuffer, nLog10Divisor ); + rBuffer.append( ' ' ); + fValue = getHeight() - pixelToPoint(rPoint.getY()); + appendDouble( fValue, rBuffer, nLog10Divisor ); +} + +void PDFPage::appendRect( const tools::Rectangle& rRect, OStringBuffer& rBuffer ) const +{ + appendPoint( rRect.BottomLeft() + Point( 0, 1 ), rBuffer ); + rBuffer.append( ' ' ); + appendMappedLength( static_cast<sal_Int32>(rRect.GetWidth()), rBuffer, false ); + rBuffer.append( ' ' ); + appendMappedLength( static_cast<sal_Int32>(rRect.GetHeight()), rBuffer ); + rBuffer.append( " re" ); +} + +void PDFPage::convertRect( tools::Rectangle& rRect ) const +{ + Point aLL = lcl_convert( m_pWriter->m_aGraphicsStack.front().m_aMapMode, + m_pWriter->m_aMapMode, + m_pWriter, + rRect.BottomLeft() + Point( 0, 1 ) + ); + Size aSize = lcl_convert( m_pWriter->m_aGraphicsStack.front().m_aMapMode, + m_pWriter->m_aMapMode, + m_pWriter, + rRect.GetSize() ); + rRect.SetLeft( aLL.X() ); + rRect.SetRight( aLL.X() + aSize.Width() ); + rRect.SetTop( pointToPixel(getHeight()) - aLL.Y() ); + rRect.SetBottom( rRect.Top() + aSize.Height() ); +} + +void PDFPage::appendPolygon( const tools::Polygon& rPoly, OStringBuffer& rBuffer, bool bClose ) const +{ + sal_uInt16 nPoints = rPoly.GetSize(); + /* + * #108582# applications do weird things + */ + sal_uInt32 nBufLen = rBuffer.getLength(); + if( nPoints > 0 ) + { + const PolyFlags* pFlagArray = rPoly.GetConstFlagAry(); + appendPoint( rPoly[0], rBuffer ); + rBuffer.append( " m\n" ); + for( sal_uInt16 i = 1; i < nPoints; i++ ) + { + if( pFlagArray && pFlagArray[i] == PolyFlags::Control && nPoints-i > 2 ) + { + // bezier + SAL_WARN_IF( pFlagArray[i+1] != PolyFlags::Control || pFlagArray[i+2] == PolyFlags::Control, "vcl.pdfwriter", "unexpected sequence of control points" ); + appendPoint( rPoly[i], rBuffer ); + rBuffer.append( " " ); + appendPoint( rPoly[i+1], rBuffer ); + rBuffer.append( " " ); + appendPoint( rPoly[i+2], rBuffer ); + rBuffer.append( " c" ); + i += 2; // add additionally consumed points + } + else + { + // line + appendPoint( rPoly[i], rBuffer ); + rBuffer.append( " l" ); + } + if( (rBuffer.getLength() - nBufLen) > 65 ) + { + rBuffer.append( "\n" ); + nBufLen = rBuffer.getLength(); + } + else + rBuffer.append( " " ); + } + if( bClose ) + rBuffer.append( "h\n" ); + } +} + +void PDFPage::appendPolygon( const basegfx::B2DPolygon& rPoly, OStringBuffer& rBuffer ) const +{ + basegfx::B2DPolygon aPoly( lcl_convert( m_pWriter->m_aGraphicsStack.front().m_aMapMode, + m_pWriter->m_aMapMode, + m_pWriter, + rPoly ) ); + + if( basegfx::utils::isRectangle( aPoly ) ) + { + basegfx::B2DRange aRange( aPoly.getB2DRange() ); + basegfx::B2DPoint aBL( aRange.getMinX(), aRange.getMaxY() ); + appendPixelPoint( aBL, rBuffer ); + rBuffer.append( ' ' ); + appendMappedLength( aRange.getWidth(), rBuffer, false, nLog10Divisor ); + rBuffer.append( ' ' ); + appendMappedLength( aRange.getHeight(), rBuffer, true, nLog10Divisor ); + rBuffer.append( " re\n" ); + return; + } + sal_uInt32 nPoints = aPoly.count(); + if( nPoints > 0 ) + { + sal_uInt32 nBufLen = rBuffer.getLength(); + basegfx::B2DPoint aLastPoint( aPoly.getB2DPoint( 0 ) ); + appendPixelPoint( aLastPoint, rBuffer ); + rBuffer.append( " m\n" ); + for( sal_uInt32 i = 1; i <= nPoints; i++ ) + { + if( i != nPoints || aPoly.isClosed() ) + { + sal_uInt32 nCurPoint = i % nPoints; + sal_uInt32 nLastPoint = i-1; + basegfx::B2DPoint aPoint( aPoly.getB2DPoint( nCurPoint ) ); + if( aPoly.isNextControlPointUsed( nLastPoint ) && + aPoly.isPrevControlPointUsed( nCurPoint ) ) + { + appendPixelPoint( aPoly.getNextControlPoint( nLastPoint ), rBuffer ); + rBuffer.append( ' ' ); + appendPixelPoint( aPoly.getPrevControlPoint( nCurPoint ), rBuffer ); + rBuffer.append( ' ' ); + appendPixelPoint( aPoint, rBuffer ); + rBuffer.append( " c" ); + } + else if( aPoly.isNextControlPointUsed( nLastPoint ) ) + { + appendPixelPoint( aPoly.getNextControlPoint( nLastPoint ), rBuffer ); + rBuffer.append( ' ' ); + appendPixelPoint( aPoint, rBuffer ); + rBuffer.append( " y" ); + } + else if( aPoly.isPrevControlPointUsed( nCurPoint ) ) + { + appendPixelPoint( aPoly.getPrevControlPoint( nCurPoint ), rBuffer ); + rBuffer.append( ' ' ); + appendPixelPoint( aPoint, rBuffer ); + rBuffer.append( " v" ); + } + else + { + appendPixelPoint( aPoint, rBuffer ); + rBuffer.append( " l" ); + } + if( (rBuffer.getLength() - nBufLen) > 65 ) + { + rBuffer.append( "\n" ); + nBufLen = rBuffer.getLength(); + } + else + rBuffer.append( " " ); + } + } + rBuffer.append( "h\n" ); + } +} + +void PDFPage::appendPolyPolygon( const tools::PolyPolygon& rPolyPoly, OStringBuffer& rBuffer ) const +{ + sal_uInt16 nPolygons = rPolyPoly.Count(); + for( sal_uInt16 n = 0; n < nPolygons; n++ ) + appendPolygon( rPolyPoly[n], rBuffer ); +} + +void PDFPage::appendPolyPolygon( const basegfx::B2DPolyPolygon& rPolyPoly, OStringBuffer& rBuffer ) const +{ + for(auto const& rPolygon : rPolyPoly) + appendPolygon( rPolygon, rBuffer ); +} + +void PDFPage::appendMappedLength( sal_Int32 nLength, OStringBuffer& rBuffer, bool bVertical, sal_Int32* pOutLength ) const +{ + sal_Int32 nValue = nLength; + if ( nLength < 0 ) + { + rBuffer.append( '-' ); + nValue = -nLength; + } + Size aSize( lcl_convert( m_pWriter->m_aGraphicsStack.front().m_aMapMode, + m_pWriter->m_aMapMode, + m_pWriter, + Size( nValue, nValue ) ) ); + nValue = bVertical ? aSize.Height() : aSize.Width(); + if( pOutLength ) + *pOutLength = ((nLength < 0 ) ? -nValue : nValue); + + appendFixedInt( nValue, rBuffer ); +} + +void PDFPage::appendMappedLength( double fLength, OStringBuffer& rBuffer, bool bVertical, sal_Int32 nPrecision ) const +{ + Size aSize( lcl_convert( m_pWriter->m_aGraphicsStack.front().m_aMapMode, + m_pWriter->m_aMapMode, + m_pWriter, + Size( 1000, 1000 ) ) ); + fLength *= pixelToPoint(static_cast<double>(bVertical ? aSize.Height() : aSize.Width()) / 1000.0); + appendDouble( fLength, rBuffer, nPrecision ); +} + +bool PDFPage::appendLineInfo( const LineInfo& rInfo, OStringBuffer& rBuffer ) const +{ + if(LineStyle::Dash == rInfo.GetStyle() && rInfo.GetDashLen() != rInfo.GetDotLen()) + { + // dashed and non-degraded case, check for implementation limits of dash array + // in PDF reader apps (e.g. acroread) + if(2 * (rInfo.GetDashCount() + rInfo.GetDotCount()) > 10) + { + return false; + } + } + + if(basegfx::B2DLineJoin::NONE != rInfo.GetLineJoin()) + { + // LineJoin used, ExtLineInfo required + return false; + } + + if(css::drawing::LineCap_BUTT != rInfo.GetLineCap()) + { + // LineCap used, ExtLineInfo required + return false; + } + + if( rInfo.GetStyle() == LineStyle::Dash ) + { + rBuffer.append( "[ " ); + if( rInfo.GetDashLen() == rInfo.GetDotLen() ) // degraded case + { + appendMappedLength( rInfo.GetDashLen(), rBuffer ); + rBuffer.append( ' ' ); + appendMappedLength( rInfo.GetDistance(), rBuffer ); + rBuffer.append( ' ' ); + } + else + { + for( int n = 0; n < rInfo.GetDashCount(); n++ ) + { + appendMappedLength( rInfo.GetDashLen(), rBuffer ); + rBuffer.append( ' ' ); + appendMappedLength( rInfo.GetDistance(), rBuffer ); + rBuffer.append( ' ' ); + } + for( int m = 0; m < rInfo.GetDotCount(); m++ ) + { + appendMappedLength( rInfo.GetDotLen(), rBuffer ); + rBuffer.append( ' ' ); + appendMappedLength( rInfo.GetDistance(), rBuffer ); + rBuffer.append( ' ' ); + } + } + rBuffer.append( "] 0 d\n" ); + } + + if( rInfo.GetWidth() > 1 ) + { + appendMappedLength( rInfo.GetWidth(), rBuffer ); + rBuffer.append( " w\n" ); + } + else if( rInfo.GetWidth() == 0 ) + { + // "pixel" line + appendDouble( 72.0/double(m_pWriter->GetDPIX()), rBuffer ); + rBuffer.append( " w\n" ); + } + + return true; +} + +void PDFPage::appendWaveLine( sal_Int32 nWidth, sal_Int32 nY, sal_Int32 nDelta, OStringBuffer& rBuffer ) const +{ + if( nWidth <= 0 ) + return; + if( nDelta < 1 ) + nDelta = 1; + + rBuffer.append( "0 " ); + appendMappedLength( nY, rBuffer ); + rBuffer.append( " m\n" ); + for( sal_Int32 n = 0; n < nWidth; ) + { + n += nDelta; + appendMappedLength( n, rBuffer, false ); + rBuffer.append( ' ' ); + appendMappedLength( nDelta+nY, rBuffer ); + rBuffer.append( ' ' ); + n += nDelta; + appendMappedLength( n, rBuffer, false ); + rBuffer.append( ' ' ); + appendMappedLength( nY, rBuffer ); + rBuffer.append( " v " ); + if( n < nWidth ) + { + n += nDelta; + appendMappedLength( n, rBuffer, false ); + rBuffer.append( ' ' ); + appendMappedLength( nY-nDelta, rBuffer ); + rBuffer.append( ' ' ); + n += nDelta; + appendMappedLength( n, rBuffer, false ); + rBuffer.append( ' ' ); + appendMappedLength( nY, rBuffer ); + rBuffer.append( " v\n" ); + } + } + rBuffer.append( "S\n" ); +} + +void PDFPage::appendMatrix3(Matrix3 const & rMatrix, OStringBuffer& rBuffer) +{ + appendDouble(rMatrix.get(0), rBuffer); + rBuffer.append(' '); + appendDouble(rMatrix.get(1), rBuffer); + rBuffer.append(' '); + appendDouble(rMatrix.get(2), rBuffer); + rBuffer.append(' '); + appendDouble(rMatrix.get(3), rBuffer); + rBuffer.append(' '); + appendPoint(Point(long(rMatrix.get(4)), long(rMatrix.get(5))), rBuffer); +} + +double PDFPage::getHeight() const +{ + double fRet = m_nPageHeight ? m_nPageHeight : vcl::pdf::g_nInheritedPageHeight; + + if (m_nUserUnit > 1) + { + fRet /= m_nUserUnit; + } + + return fRet; +} + +PDFWriterImpl::PDFWriterImpl( const PDFWriter::PDFWriterContext& rContext, + const css::uno::Reference< css::beans::XMaterialHolder >& xEnc, + PDFWriter& i_rOuterFace) + : VirtualDevice(Application::GetDefaultDevice(), DeviceFormat::DEFAULT, DeviceFormat::NONE, OUTDEV_PDF), + m_aMapMode( MapUnit::MapPoint, Point(), Fraction( 1, pointToPixel(1) ), Fraction( 1, pointToPixel(1) ) ), + m_nCurrentStructElement( 0 ), + m_bEmitStructure( true ), + m_nNextFID( 1 ), + m_aPDFBmpCache( + officecfg::Office::Common::VCL::PDFExportImageCacheSize::get() ), + m_nCurrentPage( -1 ), + m_nCatalogObject(0), + m_nSignatureObject( -1 ), + m_nSignatureContentOffset( 0 ), + m_nSignatureLastByteRangeNoOffset( 0 ), + m_nResourceDict( -1 ), + m_nFontDictObject( -1 ), + m_aContext(rContext), + m_aFile(m_aContext.URL), + m_bOpen(false), + m_DocDigest(::comphelper::HashType::MD5), + m_aCipher( nullptr ), + m_nKeyLength(0), + m_nRC4KeyLength(0), + m_bEncryptThisStream( false ), + m_nAccessPermissions(0), + m_bIsPDF_A1( false ), + m_bIsPDF_A2( false ), + m_bIsPDF_UA( false ), + m_bIsPDF_A3( false ), + m_rOuterFace( i_rOuterFace ) +{ + m_aStructure.emplace_back( ); + m_aStructure[0].m_nOwnElement = 0; + m_aStructure[0].m_nParentElement = 0; + + Font aFont; + aFont.SetFamilyName( "Times" ); + aFont.SetFontSize( Size( 0, 12 ) ); + + GraphicsState aState; + aState.m_aMapMode = m_aMapMode; + aState.m_aFont = aFont; + m_aGraphicsStack.push_front( aState ); + + osl::File::RC aError = m_aFile.open(osl_File_OpenFlag_Write | osl_File_OpenFlag_Create); + if (aError != osl::File::E_None) + { + if (aError == osl::File::E_EXIST) + { + aError = m_aFile.open(osl_File_OpenFlag_Write); + if (aError == osl::File::E_None) + aError = m_aFile.setSize(0); + } + } + if (aError != osl::File::E_None) + return; + + m_bOpen = true; + + // setup DocInfo + setupDocInfo(); + + /* prepare the cypher engine, can be done in CTOR, free in DTOR */ + m_aCipher = rtl_cipher_createARCFOUR( rtl_Cipher_ModeStream ); + + if( xEnc.is() ) + prepareEncryption( xEnc ); + + if( m_aContext.Encryption.Encrypt() ) + { + // sanity check + if( m_aContext.Encryption.OValue.size() != ENCRYPTED_PWD_SIZE || + m_aContext.Encryption.UValue.size() != ENCRYPTED_PWD_SIZE || + m_aContext.Encryption.EncryptionKey.size() != MAXIMUM_RC4_KEY_LENGTH + ) + { + // the field lengths are invalid ? This was not setup by initEncryption. + // do not encrypt after all + m_aContext.Encryption.OValue.clear(); + m_aContext.Encryption.UValue.clear(); + OSL_ENSURE( false, "encryption data failed sanity check, encryption disabled" ); + } + else // setup key lengths + m_nAccessPermissions = computeAccessPermissions( m_aContext.Encryption, m_nKeyLength, m_nRC4KeyLength ); + } + + // write header + OStringBuffer aBuffer( 20 ); + aBuffer.append( "%PDF-" ); + switch( m_aContext.Version ) + { + case PDFWriter::PDFVersion::PDF_1_2: aBuffer.append( "1.2" );break; + case PDFWriter::PDFVersion::PDF_1_3: aBuffer.append( "1.3" );break; + case PDFWriter::PDFVersion::PDF_A_1: + case PDFWriter::PDFVersion::PDF_1_4: aBuffer.append( "1.4" );break; + case PDFWriter::PDFVersion::PDF_1_5: aBuffer.append( "1.5" );break; + default: + case PDFWriter::PDFVersion::PDF_1_6: aBuffer.append( "1.6" );break; + } + // append something binary as comment (suggested in PDF Reference) + aBuffer.append( "\n%\303\244\303\274\303\266\303\237\n" ); + if( !writeBuffer( aBuffer.getStr(), aBuffer.getLength() ) ) + { + m_aFile.close(); + m_bOpen = false; + return; + } + + // insert outline root + m_aOutline.emplace_back( ); + + m_bIsPDF_A1 = (m_aContext.Version == PDFWriter::PDFVersion::PDF_A_1); + if( m_bIsPDF_A1 ) + m_aContext.Version = PDFWriter::PDFVersion::PDF_1_4; //meaning we need PDF 1.4, PDF/A flavour + + m_bIsPDF_A2 = (m_aContext.Version == PDFWriter::PDFVersion::PDF_A_2); + if( m_bIsPDF_A2 ) + m_aContext.Version = PDFWriter::PDFVersion::PDF_1_6; //we could even use 1.7 features + + m_bIsPDF_A3 = (m_aContext.Version == PDFWriter::PDFVersion::PDF_A_3); + if( m_bIsPDF_A3 ) + m_aContext.Version = PDFWriter::PDFVersion::PDF_1_6; //we could even use 1.7 features + + if (m_aContext.UniversalAccessibilityCompliance) + { + m_bIsPDF_UA = true; + m_aContext.Tagged = true; + } + + if( m_aContext.DPIx == 0 || m_aContext.DPIy == 0 ) + SetReferenceDevice( VirtualDevice::RefDevMode::PDF1 ); + else + SetReferenceDevice( m_aContext.DPIx, m_aContext.DPIy ); + + SetOutputSizePixel( Size( 640, 480 ) ); + SetMapMode(MapMode(MapUnit::MapMM)); +} + +PDFWriterImpl::~PDFWriterImpl() +{ + disposeOnce(); +} + +void PDFWriterImpl::dispose() +{ + if( m_aCipher ) + rtl_cipher_destroyARCFOUR( m_aCipher ); + m_aPages.clear(); + VirtualDevice::dispose(); +} + +void PDFWriterImpl::setupDocInfo() +{ + std::vector< sal_uInt8 > aId; + m_aCreationDateString = PDFWriter::GetDateTime(); + computeDocumentIdentifier( aId, m_aContext.DocumentInfo, m_aCreationDateString, m_aCreationMetaDateString ); + if( m_aContext.Encryption.DocumentIdentifier.empty() ) + m_aContext.Encryption.DocumentIdentifier = aId; +} + +OString PDFWriter::GetDateTime() +{ + OStringBuffer aRet; + + TimeValue aTVal, aGMT; + oslDateTime aDT; + osl_getSystemTime(&aGMT); + osl_getLocalTimeFromSystemTime(&aGMT, &aTVal); + osl_getDateTimeFromTimeValue(&aTVal, &aDT); + aRet.append("D:"); + aRet.append(static_cast<char>('0' + ((aDT.Year / 1000) % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Year / 100) % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Year / 10) % 10))); + aRet.append(static_cast<char>('0' + (aDT.Year % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Month / 10) % 10))); + aRet.append(static_cast<char>('0' + (aDT.Month % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Day / 10) % 10))); + aRet.append(static_cast<char>('0' + (aDT.Day % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Hours / 10) % 10))); + aRet.append(static_cast<char>('0' + (aDT.Hours % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Minutes / 10) % 10))); + aRet.append(static_cast<char>('0' + (aDT.Minutes % 10))); + aRet.append(static_cast<char>('0' + ((aDT.Seconds / 10) % 10))); + aRet.append(static_cast<char>('0' + (aDT.Seconds % 10))); + + sal_uInt32 nDelta = 0; + if (aGMT.Seconds > aTVal.Seconds) + { + aRet.append("-"); + nDelta = aGMT.Seconds-aTVal.Seconds; + } + else if (aGMT.Seconds < aTVal.Seconds) + { + aRet.append("+"); + nDelta = aTVal.Seconds-aGMT.Seconds; + } + else + aRet.append("Z"); + + if (nDelta) + { + aRet.append(static_cast<char>('0' + ((nDelta / 36000) % 10))); + aRet.append(static_cast<char>('0' + ((nDelta / 3600) % 10))); + aRet.append("'"); + aRet.append(static_cast<char>('0' + ((nDelta / 600) % 6))); + aRet.append(static_cast<char>('0' + ((nDelta / 60) % 10))); + } + aRet.append( "'" ); + + return aRet.makeStringAndClear(); +} + +void PDFWriterImpl::computeDocumentIdentifier( std::vector< sal_uInt8 >& o_rIdentifier, + const vcl::PDFWriter::PDFDocInfo& i_rDocInfo, + const OString& i_rCString1, + OString& o_rCString2 + ) +{ + o_rIdentifier.clear(); + + //build the document id + OString aInfoValuesOut; + OStringBuffer aID( 1024 ); + if( !i_rDocInfo.Title.isEmpty() ) + PDFWriter::AppendUnicodeTextString(i_rDocInfo.Title, aID); + if( !i_rDocInfo.Author.isEmpty() ) + PDFWriter::AppendUnicodeTextString(i_rDocInfo.Author, aID); + if( !i_rDocInfo.Subject.isEmpty() ) + PDFWriter::AppendUnicodeTextString(i_rDocInfo.Subject, aID); + if( !i_rDocInfo.Keywords.isEmpty() ) + PDFWriter::AppendUnicodeTextString(i_rDocInfo.Keywords, aID); + if( !i_rDocInfo.Creator.isEmpty() ) + PDFWriter::AppendUnicodeTextString(i_rDocInfo.Creator, aID); + if( !i_rDocInfo.Producer.isEmpty() ) + PDFWriter::AppendUnicodeTextString(i_rDocInfo.Producer, aID); + + TimeValue aTVal, aGMT; + oslDateTime aDT; + osl_getSystemTime( &aGMT ); + osl_getLocalTimeFromSystemTime( &aGMT, &aTVal ); + osl_getDateTimeFromTimeValue( &aTVal, &aDT ); + OStringBuffer aCreationMetaDateString(64); + + // i59651: we fill the Metadata date string as well, if PDF/A is requested + // according to ISO 19005-1:2005 6.7.3 the date is corrected for + // local time zone offset UTC only, whereas Acrobat 8 seems + // to use the localtime notation only + // according to a recommendation in XMP Specification (Jan 2004, page 75) + // the Acrobat way seems the right approach + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Year/1000)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Year/100)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Year/10)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Year)%10)) ); + aCreationMetaDateString.append( "-" ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Month/10)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Month)%10)) ); + aCreationMetaDateString.append( "-" ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Day/10)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Day)%10)) ); + aCreationMetaDateString.append( "T" ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Hours/10)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Hours)%10)) ); + aCreationMetaDateString.append( ":" ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Minutes/10)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Minutes)%10)) ); + aCreationMetaDateString.append( ":" ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Seconds/10)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((aDT.Seconds)%10)) ); + + sal_uInt32 nDelta = 0; + if( aGMT.Seconds > aTVal.Seconds ) + { + nDelta = aGMT.Seconds-aTVal.Seconds; + aCreationMetaDateString.append( "-" ); + } + else if( aGMT.Seconds < aTVal.Seconds ) + { + nDelta = aTVal.Seconds-aGMT.Seconds; + aCreationMetaDateString.append( "+" ); + } + else + { + aCreationMetaDateString.append( "Z" ); + + } + if( nDelta ) + { + aCreationMetaDateString.append( static_cast<char>('0' + ((nDelta/36000)%10)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((nDelta/3600)%10)) ); + aCreationMetaDateString.append( ":" ); + aCreationMetaDateString.append( static_cast<char>('0' + ((nDelta/600)%6)) ); + aCreationMetaDateString.append( static_cast<char>('0' + ((nDelta/60)%10)) ); + } + aID.append( i_rCString1.getStr(), i_rCString1.getLength() ); + + aInfoValuesOut = aID.makeStringAndClear(); + o_rCString2 = aCreationMetaDateString.makeStringAndClear(); + + ::comphelper::Hash aDigest(::comphelper::HashType::MD5); + aDigest.update(reinterpret_cast<unsigned char const*>(&aGMT), sizeof(aGMT)); + aDigest.update(reinterpret_cast<unsigned char const*>(aInfoValuesOut.getStr()), aInfoValuesOut.getLength()); + //the binary form of the doc id is needed for encryption stuff + o_rIdentifier = aDigest.finalize(); +} + +/* i12626 methods */ +/* +check if the Unicode string must be encrypted or not, perform the requested task, +append the string as unicode hex, encrypted if needed + */ +inline void PDFWriterImpl::appendUnicodeTextStringEncrypt( const OUString& rInString, const sal_Int32 nInObjectNumber, OStringBuffer& rOutBuffer ) +{ + rOutBuffer.append( "<" ); + if( m_aContext.Encryption.Encrypt() ) + { + const sal_Unicode* pStr = rInString.getStr(); + sal_Int32 nLen = rInString.getLength(); + //prepare a unicode string, encrypt it + enableStringEncryption( nInObjectNumber ); + sal_uInt8 *pCopy = m_vEncryptionBuffer.data(); + sal_Int32 nChars = 2 + (nLen * 2); + m_vEncryptionBuffer.resize(nChars); + *pCopy++ = 0xFE; + *pCopy++ = 0xFF; + // we need to prepare a byte stream from the unicode string buffer + for( int i = 0; i < nLen; i++ ) + { + sal_Unicode aUnChar = pStr[i]; + *pCopy++ = static_cast<sal_uInt8>( aUnChar >> 8 ); + *pCopy++ = static_cast<sal_uInt8>( aUnChar & 255 ); + } + //encrypt in place + rtl_cipher_encodeARCFOUR( m_aCipher, m_vEncryptionBuffer.data(), nChars, m_vEncryptionBuffer.data(), nChars ); + //now append, hexadecimal (appendHex), the encrypted result + for(int i = 0; i < nChars; i++) + appendHex( m_vEncryptionBuffer[i], rOutBuffer ); + } + else + PDFWriter::AppendUnicodeTextString(rInString, rOutBuffer); + rOutBuffer.append( ">" ); +} + +inline void PDFWriterImpl::appendLiteralStringEncrypt( OStringBuffer const & rInString, const sal_Int32 nInObjectNumber, OStringBuffer& rOutBuffer ) +{ + rOutBuffer.append( "(" ); + sal_Int32 nChars = rInString.getLength(); + //check for encryption, if ok, encrypt the string, then convert with appndLiteralString + if( m_aContext.Encryption.Encrypt() ) + { + m_vEncryptionBuffer.resize(nChars); + //encrypt the string in a buffer, then append it + enableStringEncryption( nInObjectNumber ); + rtl_cipher_encodeARCFOUR( m_aCipher, rInString.getStr(), nChars, m_vEncryptionBuffer.data(), nChars ); + appendLiteralString( reinterpret_cast<char*>(m_vEncryptionBuffer.data()), nChars, rOutBuffer ); + } + else + appendLiteralString( rInString.getStr(), nChars , rOutBuffer ); + rOutBuffer.append( ")" ); +} + +inline void PDFWriterImpl::appendLiteralStringEncrypt( const OString& rInString, const sal_Int32 nInObjectNumber, OStringBuffer& rOutBuffer ) +{ + OStringBuffer aBufferString( rInString ); + appendLiteralStringEncrypt( aBufferString, nInObjectNumber, rOutBuffer); +} + +void PDFWriterImpl::appendLiteralStringEncrypt( const OUString& rInString, const sal_Int32 nInObjectNumber, OStringBuffer& rOutBuffer, rtl_TextEncoding nEnc ) +{ + OString aBufferString( OUStringToOString( rInString, nEnc ) ); + sal_Int32 nLen = aBufferString.getLength(); + OStringBuffer aBuf( nLen ); + const char* pT = aBufferString.getStr(); + + for( sal_Int32 i = 0; i < nLen; i++, pT++ ) + { + if( (*pT & 0x80) == 0 ) + aBuf.append( *pT ); + else + { + aBuf.append( '<' ); + appendHex( *pT, aBuf ); + aBuf.append( '>' ); + } + } + aBufferString = aBuf.makeStringAndClear(); + appendLiteralStringEncrypt( aBufferString, nInObjectNumber, rOutBuffer); +} + +/* end i12626 methods */ + +void PDFWriterImpl::emitComment( const char* pComment ) +{ + OString aLine = "% " + rtl::OStringView(pComment) + "\n"; + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +bool PDFWriterImpl::compressStream( SvMemoryStream* pStream ) +{ + if (!g_bDebugDisableCompression) + { + sal_uLong nEndPos = pStream->TellEnd(); + pStream->Seek( STREAM_SEEK_TO_BEGIN ); + ZCodec aCodec( 0x4000, 0x4000 ); + SvMemoryStream aStream; + aCodec.BeginCompression(); + aCodec.Write( aStream, static_cast<const sal_uInt8*>(pStream->GetData()), nEndPos ); + aCodec.EndCompression(); + nEndPos = aStream.Tell(); + pStream->Seek( STREAM_SEEK_TO_BEGIN ); + aStream.Seek( STREAM_SEEK_TO_BEGIN ); + pStream->SetStreamSize( nEndPos ); + pStream->WriteBytes( aStream.GetData(), nEndPos ); + return true; + } + else + return false; +} + +void PDFWriterImpl::beginCompression() +{ + if (!g_bDebugDisableCompression) + { + m_pCodec = std::make_unique<ZCodec>( 0x4000, 0x4000 ); + m_pMemStream = std::make_unique<SvMemoryStream>(); + m_pCodec->BeginCompression(); + } +} + +void PDFWriterImpl::endCompression() +{ + if (!g_bDebugDisableCompression && m_pCodec) + { + m_pCodec->EndCompression(); + m_pCodec.reset(); + sal_uInt64 nLen = m_pMemStream->Tell(); + m_pMemStream->Seek( 0 ); + writeBuffer( m_pMemStream->GetData(), nLen ); + m_pMemStream.reset(); + } +} + +bool PDFWriterImpl::writeBuffer( const void* pBuffer, sal_uInt64 nBytes ) +{ + if( ! m_bOpen ) // we are already down the drain + return false; + + if( ! nBytes ) // huh ? + return true; + + if( !m_aOutputStreams.empty() ) + { + m_aOutputStreams.front().m_pStream->Seek( STREAM_SEEK_TO_END ); + m_aOutputStreams.front().m_pStream->WriteBytes( + pBuffer, sal::static_int_cast<std::size_t>(nBytes)); + return true; + } + + sal_uInt64 nWritten; + if( m_pCodec ) + { + m_pCodec->Write( *m_pMemStream, static_cast<const sal_uInt8*>(pBuffer), static_cast<sal_uLong>(nBytes) ); + nWritten = nBytes; + } + else + { + bool buffOK = true; + if( m_bEncryptThisStream ) + { + /* implement the encryption part of the PDF spec encryption algorithm 3.1 */ + m_vEncryptionBuffer.resize(nBytes); + if( buffOK ) + rtl_cipher_encodeARCFOUR( m_aCipher, + pBuffer, static_cast<sal_Size>(nBytes), + m_vEncryptionBuffer.data(), static_cast<sal_Size>(nBytes) ); + } + + const void* pWriteBuffer = ( m_bEncryptThisStream && buffOK ) ? m_vEncryptionBuffer.data() : pBuffer; + m_DocDigest.update(static_cast<unsigned char const*>(pWriteBuffer), static_cast<sal_uInt32>(nBytes)); + + if (m_aFile.write(pWriteBuffer, nBytes, nWritten) != osl::File::E_None) + nWritten = 0; + + if( nWritten != nBytes ) + { + m_aFile.close(); + m_bOpen = false; + } + } + + return nWritten == nBytes; +} + +void PDFWriterImpl::newPage( double nPageWidth, double nPageHeight, PDFWriter::Orientation eOrientation ) +{ + endPage(); + m_nCurrentPage = m_aPages.size(); + m_aPages.emplace_back(this, nPageWidth, nPageHeight, eOrientation ); + + sal_Int32 nUserUnit = m_aPages.back().m_nUserUnit; + if (nUserUnit > 1) + { + m_aMapMode = MapMode(MapUnit::MapPoint, Point(), Fraction(nUserUnit, pointToPixel(1)), + Fraction(nUserUnit, pointToPixel(1))); + } + + m_aPages.back().beginStream(); + + // setup global graphics state + // linewidth is "1 pixel" by default + OStringBuffer aBuf( 16 ); + appendDouble( 72.0/double(GetDPIX()), aBuf ); + aBuf.append( " w\n" ); + writeBuffer( aBuf.getStr(), aBuf.getLength() ); +} + +void PDFWriterImpl::endPage() +{ + if( m_aPages.empty() ) + return; + + // close eventual MC sequence + endStructureElementMCSeq(); + + // sanity check + if( !m_aOutputStreams.empty() ) + { + OSL_FAIL( "redirection across pages !!!" ); + m_aOutputStreams.clear(); // leak ! + m_aMapMode.SetOrigin( Point() ); + } + + m_aGraphicsStack.clear(); + m_aGraphicsStack.emplace_back( ); + + // this should pop the PDF graphics stack if necessary + updateGraphicsState(); + + m_aPages.back().endStream(); + + // reset the default font + Font aFont; + aFont.SetFamilyName( "Times" ); + aFont.SetFontSize( Size( 0, 12 ) ); + + m_aCurrentPDFState = m_aGraphicsStack.front(); + m_aGraphicsStack.front().m_aFont = aFont; + + for (auto & bitmap : m_aBitmaps) + { + if( ! bitmap.m_aBitmap.IsEmpty() ) + { + writeBitmapObject(bitmap); + bitmap.m_aBitmap = BitmapEx(); + } + } + for (auto & jpeg : m_aJPGs) + { + if( jpeg.m_pStream ) + { + writeJPG( jpeg ); + jpeg.m_pStream.reset(); + jpeg.m_aMask = Bitmap(); + } + } + for (auto & item : m_aTransparentObjects) + { + if( item.m_pContentStream ) + { + writeTransparentObject(item); + item.m_pContentStream.reset(); + } + } + +} + +sal_Int32 PDFWriterImpl::createObject() +{ + m_aObjects.push_back( ~0U ); + return m_aObjects.size(); +} + +bool PDFWriterImpl::updateObject( sal_Int32 n ) +{ + if( ! m_bOpen ) + return false; + + sal_uInt64 nOffset = ~0U; + osl::File::RC aError = m_aFile.getPos(nOffset); + SAL_WARN_IF( aError != osl::File::E_None, "vcl.pdfwriter", "could not register object" ); + if (aError != osl::File::E_None) + { + m_aFile.close(); + m_bOpen = false; + } + m_aObjects[ n-1 ] = nOffset; + return aError == osl::File::E_None; +} + +#define CHECK_RETURN( x ) if( !(x) ) return 0 +#define CHECK_RETURN2( x ) if( !(x) ) return + +sal_Int32 PDFWriterImpl::emitStructParentTree( sal_Int32 nObject ) +{ + if( nObject > 0 ) + { + OStringBuffer aLine( 1024 ); + + aLine.append( nObject ); + aLine.append( " 0 obj\n" + "<</Nums[\n" ); + sal_Int32 nTreeItems = m_aStructParentTree.size(); + for( sal_Int32 n = 0; n < nTreeItems; n++ ) + { + aLine.append( n ); + aLine.append( ' ' ); + aLine.append( m_aStructParentTree[n] ); + aLine.append( "\n" ); + } + aLine.append( "]>>\nendobj\n\n" ); + CHECK_RETURN( updateObject( nObject ) ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + } + return nObject; +} + +const char* PDFWriterImpl::getAttributeTag( PDFWriter::StructAttribute eAttr ) +{ + static std::map< PDFWriter::StructAttribute, const char* > aAttributeStrings; + // fill maps once + if( aAttributeStrings.empty() ) + { + aAttributeStrings[ PDFWriter::Placement ] = "Placement"; + aAttributeStrings[ PDFWriter::WritingMode ] = "WritingMode"; + aAttributeStrings[ PDFWriter::SpaceBefore ] = "SpaceBefore"; + aAttributeStrings[ PDFWriter::SpaceAfter ] = "SpaceAfter"; + aAttributeStrings[ PDFWriter::StartIndent ] = "StartIndent"; + aAttributeStrings[ PDFWriter::EndIndent ] = "EndIndent"; + aAttributeStrings[ PDFWriter::TextIndent ] = "TextIndent"; + aAttributeStrings[ PDFWriter::TextAlign ] = "TextAlign"; + aAttributeStrings[ PDFWriter::Width ] = "Width"; + aAttributeStrings[ PDFWriter::Height ] = "Height"; + aAttributeStrings[ PDFWriter::BlockAlign ] = "BlockAlign"; + aAttributeStrings[ PDFWriter::InlineAlign ] = "InlineAlign"; + aAttributeStrings[ PDFWriter::LineHeight ] = "LineHeight"; + aAttributeStrings[ PDFWriter::BaselineShift ] = "BaselineShift"; + aAttributeStrings[ PDFWriter::TextDecorationType ] = "TextDecorationType"; + aAttributeStrings[ PDFWriter::ListNumbering ] = "ListNumbering"; + aAttributeStrings[ PDFWriter::RowSpan ] = "RowSpan"; + aAttributeStrings[ PDFWriter::ColSpan ] = "ColSpan"; + aAttributeStrings[ PDFWriter::LinkAnnotation ] = "LinkAnnotation"; + } + + std::map< PDFWriter::StructAttribute, const char* >::const_iterator it = + aAttributeStrings.find( eAttr ); + + if( it == aAttributeStrings.end() ) + SAL_INFO("vcl.pdfwriter", "invalid PDFWriter::StructAttribute " << eAttr); + + return it != aAttributeStrings.end() ? it->second : ""; +} + +const char* PDFWriterImpl::getAttributeValueTag( PDFWriter::StructAttributeValue eVal ) +{ + static std::map< PDFWriter::StructAttributeValue, const char* > aValueStrings; + + if( aValueStrings.empty() ) + { + aValueStrings[ PDFWriter::NONE ] = "None"; + aValueStrings[ PDFWriter::Block ] = "Block"; + aValueStrings[ PDFWriter::Inline ] = "Inline"; + aValueStrings[ PDFWriter::Before ] = "Before"; + aValueStrings[ PDFWriter::After ] = "After"; + aValueStrings[ PDFWriter::Start ] = "Start"; + aValueStrings[ PDFWriter::End ] = "End"; + aValueStrings[ PDFWriter::LrTb ] = "LrTb"; + aValueStrings[ PDFWriter::RlTb ] = "RlTb"; + aValueStrings[ PDFWriter::TbRl ] = "TbRl"; + aValueStrings[ PDFWriter::Center ] = "Center"; + aValueStrings[ PDFWriter::Justify ] = "Justify"; + aValueStrings[ PDFWriter::Auto ] = "Auto"; + aValueStrings[ PDFWriter::Middle ] = "Middle"; + aValueStrings[ PDFWriter::Normal ] = "Normal"; + aValueStrings[ PDFWriter::Underline ] = "Underline"; + aValueStrings[ PDFWriter::Overline ] = "Overline"; + aValueStrings[ PDFWriter::LineThrough ] = "LineThrough"; + aValueStrings[ PDFWriter::Disc ] = "Disc"; + aValueStrings[ PDFWriter::Circle ] = "Circle"; + aValueStrings[ PDFWriter::Square ] = "Square"; + aValueStrings[ PDFWriter::Decimal ] = "Decimal"; + aValueStrings[ PDFWriter::UpperRoman ] = "UpperRoman"; + aValueStrings[ PDFWriter::LowerRoman ] = "LowerRoman"; + aValueStrings[ PDFWriter::UpperAlpha ] = "UpperAlpha"; + aValueStrings[ PDFWriter::LowerAlpha ] = "LowerAlpha"; + } + + std::map< PDFWriter::StructAttributeValue, const char* >::const_iterator it = + aValueStrings.find( eVal ); + + if( it == aValueStrings.end() ) + SAL_INFO("vcl.pdfwriter", "invalid PDFWriter::StructAttributeValue " << eVal); + + return it != aValueStrings.end() ? it->second : ""; +} + +static void appendStructureAttributeLine( PDFWriter::StructAttribute i_eAttr, const PDFStructureAttribute& i_rVal, OStringBuffer& o_rLine, bool i_bIsFixedInt ) +{ + o_rLine.append( "/" ); + o_rLine.append( PDFWriterImpl::getAttributeTag( i_eAttr ) ); + + if( i_rVal.eValue != PDFWriter::Invalid ) + { + o_rLine.append( "/" ); + o_rLine.append( PDFWriterImpl::getAttributeValueTag( i_rVal.eValue ) ); + } + else + { + // numerical value + o_rLine.append( " " ); + if( i_bIsFixedInt ) + appendFixedInt( i_rVal.nValue, o_rLine ); + else + o_rLine.append( i_rVal.nValue ); + } + o_rLine.append( "\n" ); +} + +OString PDFWriterImpl::emitStructureAttributes( PDFStructureElement& i_rEle ) +{ + // create layout, list and table attribute sets + OStringBuffer aLayout(256), aList(64), aTable(64); + for (auto const& attribute : i_rEle.m_aAttributes) + { + if( attribute.first == PDFWriter::ListNumbering ) + appendStructureAttributeLine( attribute.first, attribute.second, aList, true ); + else if( attribute.first == PDFWriter::RowSpan || + attribute.first == PDFWriter::ColSpan ) + appendStructureAttributeLine( attribute.first, attribute.second, aTable, false ); + else if( attribute.first == PDFWriter::LinkAnnotation ) + { + sal_Int32 nLink = attribute.second.nValue; + std::map< sal_Int32, sal_Int32 >::const_iterator link_it = + m_aLinkPropertyMap.find( nLink ); + if( link_it != m_aLinkPropertyMap.end() ) + nLink = link_it->second; + if( nLink >= 0 && nLink < static_cast<sal_Int32>(m_aLinks.size()) ) + { + // update struct parent of link + OString aStructParentEntry = + OString::number( i_rEle.m_nObject ) + + " 0 R"; + m_aStructParentTree.push_back( aStructParentEntry ); + m_aLinks[ nLink ].m_nStructParent = m_aStructParentTree.size()-1; + + sal_Int32 nRefObject = createObject(); + if (updateObject(nRefObject)) + { + OString aRef = + OString::number( nRefObject ) + + " 0 obj\n" + "<</Type/OBJR/Obj " + + OString::number( m_aLinks[ nLink ].m_nObject ) + + " 0 R>>\n" + "endobj\n\n"; + writeBuffer( aRef.getStr(), aRef.getLength() ); + } + + i_rEle.m_aKids.emplace_back( nRefObject ); + } + else + { + OSL_FAIL( "unresolved link id for Link structure" ); + SAL_INFO("vcl.pdfwriter", "unresolved link id " << nLink << " for Link structure"); + if (g_bDebugDisableCompression) + { + OString aLine = "unresolved link id " + + OString::number( nLink ) + + " for Link structure"; + emitComment( aLine.getStr() ); + } + } + } + else + appendStructureAttributeLine( attribute.first, attribute.second, aLayout, true ); + } + if( ! i_rEle.m_aBBox.IsEmpty() ) + { + aLayout.append( "/BBox[" ); + appendFixedInt( i_rEle.m_aBBox.Left(), aLayout ); + aLayout.append( " " ); + appendFixedInt( i_rEle.m_aBBox.Top(), aLayout ); + aLayout.append( " " ); + appendFixedInt( i_rEle.m_aBBox.Right(), aLayout ); + aLayout.append( " " ); + appendFixedInt( i_rEle.m_aBBox.Bottom(), aLayout ); + aLayout.append( "]\n" ); + } + + std::vector< sal_Int32 > aAttribObjects; + if( !aLayout.isEmpty() ) + { + aAttribObjects.push_back( createObject() ); + if (updateObject( aAttribObjects.back() )) + { + OStringBuffer aObj( 64 ); + aObj.append( aAttribObjects.back() ); + aObj.append( " 0 obj\n" + "<</O/Layout\n" ); + aLayout.append( ">>\nendobj\n\n" ); + writeBuffer( aObj.getStr(), aObj.getLength() ); + writeBuffer( aLayout.getStr(), aLayout.getLength() ); + } + } + if( !aList.isEmpty() ) + { + aAttribObjects.push_back( createObject() ); + if (updateObject( aAttribObjects.back() )) + { + OStringBuffer aObj( 64 ); + aObj.append( aAttribObjects.back() ); + aObj.append( " 0 obj\n" + "<</O/List\n" ); + aList.append( ">>\nendobj\n\n" ); + writeBuffer( aObj.getStr(), aObj.getLength() ); + writeBuffer( aList.getStr(), aList.getLength() ); + } + } + if( !aTable.isEmpty() ) + { + aAttribObjects.push_back( createObject() ); + if (updateObject( aAttribObjects.back() )) + { + OStringBuffer aObj( 64 ); + aObj.append( aAttribObjects.back() ); + aObj.append( " 0 obj\n" + "<</O/Table\n" ); + aTable.append( ">>\nendobj\n\n" ); + writeBuffer( aObj.getStr(), aObj.getLength() ); + writeBuffer( aTable.getStr(), aTable.getLength() ); + } + } + + OStringBuffer aRet( 64 ); + if( aAttribObjects.size() > 1 ) + aRet.append( " [" ); + for (auto const& attrib : aAttribObjects) + { + aRet.append( " " ); + aRet.append( attrib ); + aRet.append( " 0 R" ); + } + if( aAttribObjects.size() > 1 ) + aRet.append( " ]" ); + return aRet.makeStringAndClear(); +} + +sal_Int32 PDFWriterImpl::emitStructure( PDFStructureElement& rEle ) +{ + if( + // do not emit NonStruct and its children + rEle.m_eType == PDFWriter::NonStructElement && + rEle.m_nOwnElement != rEle.m_nParentElement // but of course emit the struct tree root + ) + return 0; + + for (auto const& child : rEle.m_aChildren) + { + if( child > 0 && child < sal_Int32(m_aStructure.size()) ) + { + PDFStructureElement& rChild = m_aStructure[ child ]; + if( rChild.m_eType != PDFWriter::NonStructElement ) + { + if( rChild.m_nParentElement == rEle.m_nOwnElement ) + emitStructure( rChild ); + else + { + OSL_FAIL( "PDFWriterImpl::emitStructure: invalid child structure element" ); + SAL_INFO("vcl.pdfwriter", "PDFWriterImpl::emitStructure: invalid child structure element with id " << child); + } + } + } + else + { + OSL_FAIL( "PDFWriterImpl::emitStructure: invalid child structure id" ); + SAL_INFO("vcl.pdfwriter", "PDFWriterImpl::emitStructure: invalid child structure id " << child); + } + } + + OStringBuffer aLine( 512 ); + aLine.append( rEle.m_nObject ); + aLine.append( " 0 obj\n" + "<</Type" ); + sal_Int32 nParentTree = -1; + if( rEle.m_nOwnElement == rEle.m_nParentElement ) + { + nParentTree = createObject(); + CHECK_RETURN( nParentTree ); + aLine.append( "/StructTreeRoot\n" ); + aLine.append( "/ParentTree " ); + aLine.append( nParentTree ); + aLine.append( " 0 R\n" ); + if( ! m_aRoleMap.empty() ) + { + aLine.append( "/RoleMap<<" ); + for (auto const& role : m_aRoleMap) + { + aLine.append( '/' ); + aLine.append(role.first); + aLine.append( '/' ); + aLine.append( role.second ); + aLine.append( '\n' ); + } + aLine.append( ">>\n" ); + } + } + else + { + aLine.append( "/StructElem\n" + "/S/" ); + if( !rEle.m_aAlias.isEmpty() ) + aLine.append( rEle.m_aAlias ); + else + aLine.append( getStructureTag( rEle.m_eType ) ); + aLine.append( "\n" + "/P " ); + aLine.append( m_aStructure[ rEle.m_nParentElement ].m_nObject ); + aLine.append( " 0 R\n" + "/Pg " ); + aLine.append( rEle.m_nFirstPageObject ); + aLine.append( " 0 R\n" ); + if( !rEle.m_aActualText.isEmpty() ) + { + aLine.append( "/ActualText" ); + appendUnicodeTextStringEncrypt( rEle.m_aActualText, rEle.m_nObject, aLine ); + aLine.append( "\n" ); + } + if( !rEle.m_aAltText.isEmpty() ) + { + aLine.append( "/Alt" ); + appendUnicodeTextStringEncrypt( rEle.m_aAltText, rEle.m_nObject, aLine ); + aLine.append( "\n" ); + } + } + if( (! rEle.m_aBBox.IsEmpty()) || (! rEle.m_aAttributes.empty()) ) + { + OString aAttribs = emitStructureAttributes( rEle ); + if( !aAttribs.isEmpty() ) + { + aLine.append( "/A" ); + aLine.append( aAttribs ); + aLine.append( "\n" ); + } + } + if( !rEle.m_aLocale.Language.isEmpty() ) + { + /* PDF allows only RFC 3066, which is only partly BCP 47 and does not + * include script tags and others. + * http://pdf.editme.com/pdfua-naturalLanguageSpecification + * http://partners.adobe.com/public/developer/en/pdf/PDFReference16.pdf#page=886 + * https://www.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf#M13.9.19332.1Heading.97.Natural.Language.Specification + * */ + LanguageTag aLanguageTag( rEle.m_aLocale); + OUString aLanguage, aScript, aCountry; + aLanguageTag.getIsoLanguageScriptCountry( aLanguage, aScript, aCountry); + if (!aLanguage.isEmpty()) + { + OUStringBuffer aLocBuf( 16 ); + aLocBuf.append( aLanguage ); + if( !aCountry.isEmpty() ) + { + aLocBuf.append( '-' ); + aLocBuf.append( aCountry ); + } + aLine.append( "/Lang" ); + appendLiteralStringEncrypt( aLocBuf.makeStringAndClear(), rEle.m_nObject, aLine ); + aLine.append( "\n" ); + } + } + if( ! rEle.m_aKids.empty() ) + { + unsigned int i = 0; + aLine.append( "/K[" ); + for (auto const& kid : rEle.m_aKids) + { + if( kid.nMCID == -1 ) + { + aLine.append( kid.nObject ); + aLine.append( " 0 R" ); + aLine.append( ( (i & 15) == 15 ) ? "\n" : " " ); + } + else + { + if( kid.nObject == rEle.m_nFirstPageObject ) + { + aLine.append( kid.nMCID ); + aLine.append( " " ); + } + else + { + aLine.append( "<</Type/MCR/Pg " ); + aLine.append( kid.nObject ); + aLine.append( " 0 R /MCID " ); + aLine.append( kid.nMCID ); + aLine.append( ">>\n" ); + } + } + ++i; + } + aLine.append( "]\n" ); + } + aLine.append( ">>\nendobj\n\n" ); + + CHECK_RETURN( updateObject( rEle.m_nObject ) ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + CHECK_RETURN( emitStructParentTree( nParentTree ) ); + + return rEle.m_nObject; +} + +bool PDFWriterImpl::emitGradients() +{ + for (auto const& gradient : m_aGradients) + { + if ( !writeGradientFunction( gradient ) ) return false; + } + return true; +} + +bool PDFWriterImpl::emitTilings() +{ + OStringBuffer aTilingObj( 1024 ); + + for (auto & tiling : m_aTilings) + { + SAL_WARN_IF( !tiling.m_pTilingStream, "vcl.pdfwriter", "tiling without stream" ); + if( ! tiling.m_pTilingStream ) + continue; + + aTilingObj.setLength( 0 ); + + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::emitTilings" ); + } + + sal_Int32 nX = static_cast<sal_Int32>(tiling.m_aRectangle.Left()); + sal_Int32 nY = static_cast<sal_Int32>(tiling.m_aRectangle.Top()); + sal_Int32 nW = static_cast<sal_Int32>(tiling.m_aRectangle.GetWidth()); + sal_Int32 nH = static_cast<sal_Int32>(tiling.m_aRectangle.GetHeight()); + if( tiling.m_aCellSize.Width() == 0 ) + tiling.m_aCellSize.setWidth( nW ); + if( tiling.m_aCellSize.Height() == 0 ) + tiling.m_aCellSize.setHeight( nH ); + + bool bDeflate = compressStream( tiling.m_pTilingStream.get() ); + sal_uInt64 const nTilingStreamSize = tiling.m_pTilingStream->TellEnd(); + tiling.m_pTilingStream->Seek( STREAM_SEEK_TO_BEGIN ); + + // write pattern object + aTilingObj.append( tiling.m_nObject ); + aTilingObj.append( " 0 obj\n" ); + aTilingObj.append( "<</Type/Pattern/PatternType 1\n" + "/PaintType 1\n" + "/TilingType 2\n" + "/BBox[" ); + appendFixedInt( nX, aTilingObj ); + aTilingObj.append( ' ' ); + appendFixedInt( nY, aTilingObj ); + aTilingObj.append( ' ' ); + appendFixedInt( nX+nW, aTilingObj ); + aTilingObj.append( ' ' ); + appendFixedInt( nY+nH, aTilingObj ); + aTilingObj.append( "]\n" + "/XStep " ); + appendFixedInt( tiling.m_aCellSize.Width(), aTilingObj ); + aTilingObj.append( "\n" + "/YStep " ); + appendFixedInt( tiling.m_aCellSize.Height(), aTilingObj ); + aTilingObj.append( "\n" ); + if( tiling.m_aTransform.matrix[0] != 1.0 || + tiling.m_aTransform.matrix[1] != 0.0 || + tiling.m_aTransform.matrix[3] != 0.0 || + tiling.m_aTransform.matrix[4] != 1.0 || + tiling.m_aTransform.matrix[2] != 0.0 || + tiling.m_aTransform.matrix[5] != 0.0 ) + { + aTilingObj.append( "/Matrix [" ); + // TODO: scaling, mirroring on y, etc + appendDouble( tiling.m_aTransform.matrix[0], aTilingObj ); + aTilingObj.append( ' ' ); + appendDouble( tiling.m_aTransform.matrix[1], aTilingObj ); + aTilingObj.append( ' ' ); + appendDouble( tiling.m_aTransform.matrix[3], aTilingObj ); + aTilingObj.append( ' ' ); + appendDouble( tiling.m_aTransform.matrix[4], aTilingObj ); + aTilingObj.append( ' ' ); + appendDouble( tiling.m_aTransform.matrix[2], aTilingObj ); + aTilingObj.append( ' ' ); + appendDouble( tiling.m_aTransform.matrix[5], aTilingObj ); + aTilingObj.append( "]\n" ); + } + aTilingObj.append( "/Resources" ); + tiling.m_aResources.append( aTilingObj, getFontDictObject() ); + if( bDeflate ) + aTilingObj.append( "/Filter/FlateDecode" ); + aTilingObj.append( "/Length " ); + aTilingObj.append( static_cast<sal_Int32>(nTilingStreamSize) ); + aTilingObj.append( ">>\nstream\n" ); + if ( !updateObject( tiling.m_nObject ) ) return false; + if ( !writeBuffer( aTilingObj.getStr(), aTilingObj.getLength() ) ) return false; + checkAndEnableStreamEncryption( tiling.m_nObject ); + bool written = writeBuffer( tiling.m_pTilingStream->GetData(), nTilingStreamSize ); + tiling.m_pTilingStream.reset(); + if( !written ) + return false; + disableStreamEncryption(); + aTilingObj.setLength( 0 ); + aTilingObj.append( "\nendstream\nendobj\n\n" ); + if ( !writeBuffer( aTilingObj.getStr(), aTilingObj.getLength() ) ) return false; + } + return true; +} + +sal_Int32 PDFWriterImpl::emitBuildinFont(const pdf::BuildinFontFace* pFD, sal_Int32 nFontObject) +{ + if( !pFD ) + return 0; + const pdf::BuildinFont& rBuildinFont = pFD->GetBuildinFont(); + + OStringBuffer aLine( 1024 ); + + if( nFontObject <= 0 ) + nFontObject = createObject(); + CHECK_RETURN( updateObject( nFontObject ) ); + aLine.append( nFontObject ); + aLine.append( " 0 obj\n" + "<</Type/Font/Subtype/Type1/BaseFont/" ); + appendName( rBuildinFont.m_pPSName, aLine ); + aLine.append( "\n" ); + if( rBuildinFont.m_eCharSet == RTL_TEXTENCODING_MS_1252 ) + aLine.append( "/Encoding/WinAnsiEncoding\n" ); + aLine.append( ">>\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + return nFontObject; +} + +std::map< sal_Int32, sal_Int32 > PDFWriterImpl::emitSystemFont( const PhysicalFontFace* pFont, EmbedFont const & rEmbed ) +{ + std::map< sal_Int32, sal_Int32 > aRet; + + sal_Int32 nFontDescriptor = 0; + OString aSubType( "/Type1" ); + FontSubsetInfo aInfo; + // fill in dummy values + aInfo.m_nAscent = 1000; + aInfo.m_nDescent = 200; + aInfo.m_nCapHeight = 1000; + aInfo.m_aFontBBox = tools::Rectangle( Point( -200, -200 ), Size( 1700, 1700 ) ); + aInfo.m_aPSName = pFont->GetFamilyName(); + sal_Int32 pWidths[256] = {}; + + SalGraphics *pGraphics = GetGraphics(); + + assert(pGraphics); + + aSubType = OString( "/TrueType" ); + std::vector< sal_Int32 > aGlyphWidths; + Ucs2UIntMap aUnicodeMap; + pGraphics->GetGlyphWidths( pFont, false, aGlyphWidths, aUnicodeMap ); + + OUString aTmpName; + osl_createTempFile( nullptr, nullptr, &aTmpName.pData ); + sal_GlyphId aGlyphIds[ 256 ] = {}; + sal_uInt8 pEncoding[ 256 ] = {}; + sal_Int32 pDuWidths[ 256 ] = {}; + + for( sal_Ucs c = 32; c < 256; c++ ) + { + pEncoding[c] = c; + aGlyphIds[c] = 0; + if( aUnicodeMap.find( c ) != aUnicodeMap.end() ) + pWidths[ c ] = aGlyphWidths[ aUnicodeMap[ c ] ]; + } + //TODO: surely this is utterly broken because aGlyphIds is just all zeros, if we + //had the right glyphids here then I imagine we could replace pDuWidths with + //pWidths and remove pWidths assignment above. i.e. start with the glyph ids + //and map those to unicode rather than try and reverse map them ? + pGraphics->CreateFontSubset( aTmpName, pFont, aGlyphIds, pEncoding, pDuWidths, 256, aInfo ); + osl_removeFile( aTmpName.pData ); + + // write font descriptor + nFontDescriptor = emitFontDescriptor( pFont, aInfo, 0, 0 ); + if( nFontDescriptor ) + { + // write font object + sal_Int32 nObject = createObject(); + if( updateObject( nObject ) ) + { + OStringBuffer aLine( 1024 ); + aLine.append( nObject ); + aLine.append( " 0 obj\n" + "<</Type/Font/Subtype" ); + aLine.append( aSubType ); + aLine.append( "/BaseFont/" ); + appendName( aInfo.m_aPSName, aLine ); + aLine.append( "\n" ); + if( !pFont->IsSymbolFont() ) + aLine.append( "/Encoding/WinAnsiEncoding\n" ); + aLine.append( "/FirstChar 32 /LastChar 255\n" + "/Widths[" ); + for( int i = 32; i < 256; i++ ) + { + aLine.append( pWidths[i] ); + aLine.append( ((i&15) == 15) ? "\n" : " " ); + } + aLine.append( "]\n" + "/FontDescriptor " ); + aLine.append( nFontDescriptor ); + aLine.append( " 0 R>>\n" + "endobj\n\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + + aRet[ rEmbed.m_nNormalFontID ] = nObject; + } + } + + return aRet; +} + +typedef int ThreeInts[3]; +static bool getPfbSegmentLengths( const unsigned char* pFontBytes, int nByteLen, + ThreeInts& rSegmentLengths ) +{ + if( !pFontBytes || (nByteLen < 0) ) + return false; + const unsigned char* pPtr = pFontBytes; + const unsigned char* pEnd = pFontBytes + nByteLen; + + for(int & rSegmentLength : rSegmentLengths) { + // read segment1 header + if( pPtr+6 >= pEnd ) + return false; + if( (pPtr[0] != 0x80) || (pPtr[1] >= 0x03) ) + return false; + const int nLen = (pPtr[5]<<24) + (pPtr[4]<<16) + (pPtr[3]<<8) + pPtr[2]; + if( nLen <= 0) + return false; + rSegmentLength = nLen; + pPtr += nLen + 6; + } + + // read segment-end header + if( pPtr+2 >= pEnd ) + return false; + if( (pPtr[0] != 0x80) || (pPtr[1] != 0x03) ) + return false; + + return true; +} + +static void appendSubsetName( int nSubsetID, const OUString& rPSName, OStringBuffer& rBuffer ) +{ + if( nSubsetID ) + { + for( int i = 0; i < 6; i++ ) + { + int nOffset = nSubsetID % 26; + nSubsetID /= 26; + rBuffer.append( static_cast<char>('A'+nOffset) ); + } + rBuffer.append( '+' ); + } + appendName( rPSName, rBuffer ); +} + +sal_Int32 PDFWriterImpl::createToUnicodeCMap( sal_uInt8 const * pEncoding, + const sal_Ucs* pCodeUnits, + const sal_Int32* pCodeUnitsPerGlyph, + const sal_Int32* pEncToUnicodeIndex, + int nGlyphs ) +{ + int nMapped = 0; + for (int n = 0; n < nGlyphs; ++n) + if( pCodeUnits[pEncToUnicodeIndex[n]] && pCodeUnitsPerGlyph[n] ) + nMapped++; + + if( nMapped == 0 ) + return 0; + + sal_Int32 nStream = createObject(); + CHECK_RETURN( updateObject( nStream ) ); + + OStringBuffer aContents( 1024 ); + aContents.append( + "/CIDInit/ProcSet findresource begin\n" + "12 dict begin\n" + "begincmap\n" + "/CIDSystemInfo<<\n" + "/Registry (Adobe)\n" + "/Ordering (UCS)\n" + "/Supplement 0\n" + ">> def\n" + "/CMapName/Adobe-Identity-UCS def\n" + "/CMapType 2 def\n" + "1 begincodespacerange\n" + "<00> <FF>\n" + "endcodespacerange\n" + ); + int nCount = 0; + for (int n = 0; n < nGlyphs; ++n) + { + if( pCodeUnits[pEncToUnicodeIndex[n]] && pCodeUnitsPerGlyph[n] ) + { + if( (nCount % 100) == 0 ) + { + if( nCount ) + aContents.append( "endbfchar\n" ); + aContents.append( static_cast<sal_Int32>(std::min(nMapped-nCount, 100)) ); + aContents.append( " beginbfchar\n" ); + } + aContents.append( '<' ); + appendHex( static_cast<sal_Int8>(pEncoding[n]), aContents ); + aContents.append( "> <" ); + // TODO: handle code points>U+FFFF + sal_Int32 nIndex = pEncToUnicodeIndex[n]; + for( sal_Int32 j = 0; j < pCodeUnitsPerGlyph[n]; j++ ) + { + appendHex( static_cast<sal_Int8>(pCodeUnits[nIndex + j] / 256), aContents ); + appendHex( static_cast<sal_Int8>(pCodeUnits[nIndex + j] & 255), aContents ); + } + aContents.append( ">\n" ); + nCount++; + } + } + aContents.append( "endbfchar\n" + "endcmap\n" + "CMapName currentdict /CMap defineresource pop\n" + "end\n" + "end\n" ); + SvMemoryStream aStream; + if (!g_bDebugDisableCompression) + { + ZCodec aCodec( 0x4000, 0x4000 ); + aCodec.BeginCompression(); + aCodec.Write( aStream, reinterpret_cast<const sal_uInt8*>(aContents.getStr()), aContents.getLength() ); + aCodec.EndCompression(); + } + + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::createToUnicodeCMap" ); + } + OStringBuffer aLine( 40 ); + + aLine.append( nStream ); + aLine.append( " 0 obj\n<</Length " ); + sal_Int32 nLen = 0; + if (!g_bDebugDisableCompression) + { + nLen = static_cast<sal_Int32>(aStream.Tell()); + aStream.Seek( 0 ); + aLine.append( nLen ); + aLine.append( "/Filter/FlateDecode" ); + } + else + aLine.append( aContents.getLength() ); + aLine.append( ">>\nstream\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + checkAndEnableStreamEncryption( nStream ); + if (!g_bDebugDisableCompression) + { + CHECK_RETURN( writeBuffer( aStream.GetData(), nLen ) ); + } + else + { + CHECK_RETURN( writeBuffer( aContents.getStr(), aContents.getLength() ) ); + } + disableStreamEncryption(); + aLine.setLength( 0 ); + aLine.append( "\nendstream\n" + "endobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + return nStream; +} + +sal_Int32 PDFWriterImpl::emitFontDescriptor( const PhysicalFontFace* pFont, FontSubsetInfo const & rInfo, sal_Int32 nSubsetID, sal_Int32 nFontStream ) +{ + OStringBuffer aLine( 1024 ); + // get font flags, see PDF reference 1.4 p. 358 + // possibly characters outside Adobe standard encoding + // so set Symbolic flag + sal_Int32 nFontFlags = (1<<2); + if( pFont->GetItalic() == ITALIC_NORMAL || pFont->GetItalic() == ITALIC_OBLIQUE ) + nFontFlags |= (1 << 6); + if( pFont->GetPitch() == PITCH_FIXED ) + nFontFlags |= 1; + if( pFont->GetFamilyType() == FAMILY_SCRIPT ) + nFontFlags |= (1 << 3); + else if( pFont->GetFamilyType() == FAMILY_ROMAN ) + nFontFlags |= (1 << 1); + + sal_Int32 nFontDescriptor = createObject(); + CHECK_RETURN( updateObject( nFontDescriptor ) ); + aLine.setLength( 0 ); + aLine.append( nFontDescriptor ); + aLine.append( " 0 obj\n" + "<</Type/FontDescriptor/FontName/" ); + appendSubsetName( nSubsetID, rInfo.m_aPSName, aLine ); + aLine.append( "\n" + "/Flags " ); + aLine.append( nFontFlags ); + aLine.append( "\n" + "/FontBBox[" ); + // note: Top and Bottom are reversed in VCL and PDF rectangles + aLine.append( static_cast<sal_Int32>(rInfo.m_aFontBBox.TopLeft().X()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(rInfo.m_aFontBBox.TopLeft().Y()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(rInfo.m_aFontBBox.BottomRight().X()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(rInfo.m_aFontBBox.BottomRight().Y()+1) ); + aLine.append( "]/ItalicAngle " ); + if( pFont->GetItalic() == ITALIC_OBLIQUE || pFont->GetItalic() == ITALIC_NORMAL ) + aLine.append( "-30" ); + else + aLine.append( "0" ); + aLine.append( "\n" + "/Ascent " ); + aLine.append( static_cast<sal_Int32>(rInfo.m_nAscent) ); + aLine.append( "\n" + "/Descent " ); + aLine.append( static_cast<sal_Int32>(-rInfo.m_nDescent) ); + aLine.append( "\n" + "/CapHeight " ); + aLine.append( static_cast<sal_Int32>(rInfo.m_nCapHeight) ); + // According to PDF reference 1.4 StemV is required + // seems a tad strange to me, but well ... + aLine.append( "\n" + "/StemV 80\n" ); + if( nFontStream ) + { + aLine.append( "/FontFile" ); + switch( rInfo.m_nFontType ) + { + case FontType::SFNT_TTF: + aLine.append( '2' ); + break; + case FontType::TYPE1_PFA: + case FontType::TYPE1_PFB: + case FontType::ANY_TYPE1: + break; + default: + OSL_FAIL( "unknown fonttype in PDF font descriptor" ); + return 0; + } + aLine.append( ' ' ); + aLine.append( nFontStream ); + aLine.append( " 0 R\n" ); + } + aLine.append( ">>\n" + "endobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + return nFontDescriptor; +} + +void PDFWriterImpl::appendBuildinFontsToDict( OStringBuffer& rDict ) const +{ + for (auto const& item : m_aBuildinFontToObjectMap) + { + rDict.append( pdf::BuildinFontFace::Get(item.first).getNameObject() ); + rDict.append( ' ' ); + rDict.append( item.second ); + rDict.append( " 0 R" ); + } +} + +bool PDFWriterImpl::emitFonts() +{ + SalGraphics *pGraphics = GetGraphics(); + + if (!pGraphics) + return false; + + OStringBuffer aLine( 1024 ); + + std::map< sal_Int32, sal_Int32 > aFontIDToObject; + + OUString aTmpName; + osl_createTempFile( nullptr, nullptr, &aTmpName.pData ); + for (const auto & subset : m_aSubsets) + { + for (auto & s_subset :subset.second.m_aSubsets) + { + sal_GlyphId aGlyphIds[ 256 ] = {}; + sal_Int32 pWidths[ 256 ]; + sal_uInt8 pEncoding[ 256 ] = {}; + sal_Int32 pEncToUnicodeIndex[ 256 ] = {}; + sal_Int32 pCodeUnitsPerGlyph[ 256 ] = {}; + std::vector<sal_Ucs> aCodeUnits; + aCodeUnits.reserve( 256 ); + int nGlyphs = 1; + // fill arrays and prepare encoding index map + sal_Int32 nToUnicodeStream = 0; + + for (auto const& item : s_subset.m_aMapping) + { + sal_uInt8 nEnc = item.second.getGlyphId(); + + SAL_WARN_IF( aGlyphIds[nEnc] != 0 || pEncoding[nEnc] != 0, "vcl.pdfwriter", "duplicate glyph" ); + SAL_WARN_IF( nEnc > s_subset.m_aMapping.size(), "vcl.pdfwriter", "invalid glyph encoding" ); + + aGlyphIds[ nEnc ] = item.first; + pEncoding[ nEnc ] = nEnc; + pEncToUnicodeIndex[ nEnc ] = static_cast<sal_Int32>(aCodeUnits.size()); + pCodeUnitsPerGlyph[ nEnc ] = item.second.countCodes(); + for( sal_Int32 n = 0; n < pCodeUnitsPerGlyph[ nEnc ]; n++ ) + aCodeUnits.push_back( item.second.getCode( n ) ); + if( item.second.getCode(0) ) + nToUnicodeStream = 1; + if( nGlyphs < 256 ) + nGlyphs++; + else + { + OSL_FAIL( "too many glyphs for subset" ); + } + } + FontSubsetInfo aSubsetInfo; + if( pGraphics->CreateFontSubset( aTmpName, subset.first, aGlyphIds, pEncoding, pWidths, nGlyphs, aSubsetInfo ) ) + { + // create font stream + osl::File aFontFile(aTmpName); + if (osl::File::E_None != aFontFile.open(osl_File_OpenFlag_Read)) return false; + // get file size + sal_uInt64 nLength1; + if ( osl::File::E_None != aFontFile.setPos(osl_Pos_End, 0) ) return false; + if ( osl::File::E_None != aFontFile.getPos(nLength1) ) return false; + if ( osl::File::E_None != aFontFile.setPos(osl_Pos_Absolut, 0) ) return false; + + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::emitFonts" ); + } + sal_Int32 nFontStream = createObject(); + sal_Int32 nStreamLengthObject = createObject(); + if ( !updateObject( nFontStream ) ) return false; + aLine.setLength( 0 ); + aLine.append( nFontStream ); + aLine.append( " 0 obj\n" + "<</Length " ); + aLine.append( nStreamLengthObject ); + if (!g_bDebugDisableCompression) + aLine.append( " 0 R" + "/Filter/FlateDecode" + "/Length1 " ); + else + aLine.append( " 0 R" + "/Length1 " ); + + sal_uInt64 nStartPos = 0; + if( aSubsetInfo.m_nFontType == FontType::SFNT_TTF ) + { + aLine.append( static_cast<sal_Int32>(nLength1) ); + + aLine.append( ">>\n" + "stream\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return false; + if ( osl::File::E_None != m_aFile.getPos(nStartPos) ) return false; + + // copy font file + beginCompression(); + checkAndEnableStreamEncryption( nFontStream ); + sal_Bool bEOF = false; + do + { + char buf[8192]; + sal_uInt64 nRead; + if ( osl::File::E_None != aFontFile.read(buf, sizeof(buf), nRead) ) return false; + if ( !writeBuffer( buf, nRead ) ) return false; + if ( osl::File::E_None != aFontFile.isEndOfFile(&bEOF) ) return false; + } while( ! bEOF ); + } + else if( aSubsetInfo.m_nFontType & FontType::CFF_FONT) + { + // TODO: implement + OSL_FAIL( "PDFWriterImpl does not support CFF-font subsets yet!" ); + } + else if( aSubsetInfo.m_nFontType & FontType::TYPE1_PFB) // TODO: also support PFA? + { + std::unique_ptr<unsigned char[]> xBuffer(new unsigned char[nLength1]); + + sal_uInt64 nBytesRead = 0; + if ( osl::File::E_None != aFontFile.read(xBuffer.get(), nLength1, nBytesRead) ) return false; + SAL_WARN_IF( nBytesRead!=nLength1, "vcl.pdfwriter", "PDF-FontSubset read incomplete!" ); + if ( osl::File::E_None != aFontFile.setPos(osl_Pos_Absolut, 0) ) return false; + // get the PFB-segment lengths + ThreeInts aSegmentLengths = {0,0,0}; + getPfbSegmentLengths(xBuffer.get(), static_cast<int>(nBytesRead), aSegmentLengths); + // the lengths below are mandatory for PDF-exported Type1 fonts + // because the PFB segment headers get stripped! WhyOhWhy. + aLine.append( static_cast<sal_Int32>(aSegmentLengths[0]) ); + aLine.append( "/Length2 " ); + aLine.append( static_cast<sal_Int32>(aSegmentLengths[1]) ); + aLine.append( "/Length3 " ); + aLine.append( static_cast<sal_Int32>(aSegmentLengths[2]) ); + + aLine.append( ">>\n" + "stream\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return false; + if ( osl::File::E_None != m_aFile.getPos(nStartPos) ) return false; + + // emit PFB-sections without section headers + beginCompression(); + checkAndEnableStreamEncryption( nFontStream ); + if ( !writeBuffer( &xBuffer[6], aSegmentLengths[0] ) ) return false; + if ( !writeBuffer( &xBuffer[12] + aSegmentLengths[0], aSegmentLengths[1] ) ) return false; + if ( !writeBuffer( &xBuffer[18] + aSegmentLengths[0] + aSegmentLengths[1], aSegmentLengths[2] ) ) return false; + } + else + { + SAL_INFO("vcl.pdfwriter", "PDF: CreateFontSubset result in not yet supported format=" << static_cast<int>(aSubsetInfo.m_nFontType)); + aLine.append( "0 >>\nstream\n" ); + } + + endCompression(); + disableStreamEncryption(); + // close the file + aFontFile.close(); + + sal_uInt64 nEndPos = 0; + if ( osl::File::E_None != m_aFile.getPos(nEndPos) ) return false; + // end the stream + aLine.setLength( 0 ); + aLine.append( "\nendstream\nendobj\n\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return false; + + // emit stream length object + if ( !updateObject( nStreamLengthObject ) ) return false; + aLine.setLength( 0 ); + aLine.append( nStreamLengthObject ); + aLine.append( " 0 obj\n" ); + aLine.append( static_cast<sal_Int64>(nEndPos-nStartPos) ); + aLine.append( "\nendobj\n\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return false; + + // write font descriptor + sal_Int32 nFontDescriptor = emitFontDescriptor( subset.first, aSubsetInfo, s_subset.m_nFontID, nFontStream ); + + if( nToUnicodeStream ) + nToUnicodeStream = createToUnicodeCMap( pEncoding, aCodeUnits.data(), pCodeUnitsPerGlyph, pEncToUnicodeIndex, nGlyphs ); + + sal_Int32 nFontObject = createObject(); + if ( !updateObject( nFontObject ) ) return false; + aLine.setLength( 0 ); + aLine.append( nFontObject ); + + aLine.append( " 0 obj\n" ); + aLine.append( (aSubsetInfo.m_nFontType & FontType::ANY_TYPE1) ? + "<</Type/Font/Subtype/Type1/BaseFont/" : + "<</Type/Font/Subtype/TrueType/BaseFont/" ); + appendSubsetName( s_subset.m_nFontID, aSubsetInfo.m_aPSName, aLine ); + aLine.append( "\n" + "/FirstChar 0\n" + "/LastChar " ); + aLine.append( static_cast<sal_Int32>(nGlyphs-1) ); + aLine.append( "\n" + "/Widths[" ); + for( int i = 0; i < nGlyphs; i++ ) + { + aLine.append( pWidths[ i ] ); + aLine.append( ((i & 15) == 15) ? "\n" : " " ); + } + aLine.append( "]\n" + "/FontDescriptor " ); + aLine.append( nFontDescriptor ); + aLine.append( " 0 R\n" ); + if( nToUnicodeStream ) + { + aLine.append( "/ToUnicode " ); + aLine.append( nToUnicodeStream ); + aLine.append( " 0 R\n" ); + } + aLine.append( ">>\n" + "endobj\n\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return false; + + aFontIDToObject[ s_subset.m_nFontID ] = nFontObject; + } + else + { + const PhysicalFontFace* pFont = subset.first; + OStringBuffer aErrorComment( 256 ); + aErrorComment.append( "CreateFontSubset failed for font \"" ); + aErrorComment.append( OUStringToOString( pFont->GetFamilyName(), RTL_TEXTENCODING_UTF8 ) ); + aErrorComment.append( '\"' ); + if( pFont->GetItalic() == ITALIC_NORMAL ) + aErrorComment.append( " italic" ); + else if( pFont->GetItalic() == ITALIC_OBLIQUE ) + aErrorComment.append( " oblique" ); + aErrorComment.append( " weight=" ); + aErrorComment.append( sal_Int32(pFont->GetWeight()) ); + emitComment( aErrorComment.getStr() ); + } + } + } + osl_removeFile( aTmpName.pData ); + + // emit system fonts + for (auto const& systemFont : m_aSystemFonts) + { + std::map< sal_Int32, sal_Int32 > aObjects = emitSystemFont( systemFont.first, systemFont.second ); + for (auto const& item : aObjects) + { + if ( !item.second ) return false; + aFontIDToObject[ item.first ] = item.second; + } + } + + OStringBuffer aFontDict( 1024 ); + aFontDict.append( getFontDictObject() ); + aFontDict.append( " 0 obj\n" + "<<" ); + int ni = 0; + for (auto const& itemMap : aFontIDToObject) + { + aFontDict.append( "/F" ); + aFontDict.append( itemMap.first ); + aFontDict.append( ' ' ); + aFontDict.append( itemMap.second ); + aFontDict.append( " 0 R" ); + if( ((++ni) & 7) == 0 ) + aFontDict.append( '\n' ); + } + // emit builtin font for widget appearances / variable text + for (auto & item : m_aBuildinFontToObjectMap) + { + rtl::Reference<pdf::BuildinFontFace> aData(new pdf::BuildinFontFace(item.first)); + item.second = emitBuildinFont( aData.get(), item.second ); + } + + appendBuildinFontsToDict(aFontDict); + aFontDict.append( "\n>>\nendobj\n\n" ); + + if ( !updateObject( getFontDictObject() ) ) return false; + if ( !writeBuffer( aFontDict.getStr(), aFontDict.getLength() ) ) return false; + return true; +} + +sal_Int32 PDFWriterImpl::emitResources() +{ + // emit shadings + if( ! m_aGradients.empty() ) + CHECK_RETURN( emitGradients() ); + // emit tilings + if( ! m_aTilings.empty() ) + CHECK_RETURN( emitTilings() ); + + // emit font dict + CHECK_RETURN( emitFonts() ); + + // emit Resource dict + OStringBuffer aLine( 512 ); + sal_Int32 nResourceDict = getResourceDictObj(); + CHECK_RETURN( updateObject( nResourceDict ) ); + aLine.setLength( 0 ); + aLine.append( nResourceDict ); + aLine.append( " 0 obj\n" ); + m_aGlobalResourceDict.append( aLine, getFontDictObject() ); + aLine.append( "endobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + return nResourceDict; +} + +sal_Int32 PDFWriterImpl::updateOutlineItemCount( std::vector< sal_Int32 >& rCounts, + sal_Int32 nItemLevel, + sal_Int32 nCurrentItemId ) +{ + /* The /Count number of an item is + positive: the number of visible subitems + negative: the negative number of subitems that will become visible if + the item gets opened + see PDF ref 1.4 p 478 + */ + + sal_Int32 nCount = 0; + + if( m_aContext.OpenBookmarkLevels < 0 || // all levels are visible + m_aContext.OpenBookmarkLevels >= nItemLevel // this level is visible + ) + { + PDFOutlineEntry& rItem = m_aOutline[ nCurrentItemId ]; + sal_Int32 nChildren = rItem.m_aChildren.size(); + for( sal_Int32 i = 0; i < nChildren; i++ ) + nCount += updateOutlineItemCount( rCounts, nItemLevel+1, rItem.m_aChildren[i] ); + rCounts[nCurrentItemId] = nCount; + // return 1 (this item) + visible sub items + if( nCount < 0 ) + nCount = 0; + nCount++; + } + else + { + // this bookmark level is invisible + PDFOutlineEntry& rItem = m_aOutline[ nCurrentItemId ]; + sal_Int32 nChildren = rItem.m_aChildren.size(); + rCounts[ nCurrentItemId ] = -sal_Int32(rItem.m_aChildren.size()); + for( sal_Int32 i = 0; i < nChildren; i++ ) + updateOutlineItemCount( rCounts, nItemLevel+1, rItem.m_aChildren[i] ); + nCount = -1; + } + + return nCount; +} + +sal_Int32 PDFWriterImpl::emitOutline() +{ + int i, nItems = m_aOutline.size(); + + // do we have an outline at all ? + if( nItems < 2 ) + return 0; + + // reserve object numbers for all outline items + for( i = 0; i < nItems; ++i ) + m_aOutline[i].m_nObject = createObject(); + + // update all parent, next and prev object ids + for( i = 0; i < nItems; ++i ) + { + PDFOutlineEntry& rItem = m_aOutline[i]; + int nChildren = rItem.m_aChildren.size(); + + if( nChildren ) + { + for( int n = 0; n < nChildren; ++n ) + { + PDFOutlineEntry& rChild = m_aOutline[ rItem.m_aChildren[n] ]; + + rChild.m_nParentObject = rItem.m_nObject; + rChild.m_nPrevObject = (n > 0) ? m_aOutline[ rItem.m_aChildren[n-1] ].m_nObject : 0; + rChild.m_nNextObject = (n < nChildren-1) ? m_aOutline[ rItem.m_aChildren[n+1] ].m_nObject : 0; + } + + } + } + + // calculate Count entries for all items + std::vector< sal_Int32 > aCounts( nItems ); + updateOutlineItemCount( aCounts, 0, 0 ); + + // emit hierarchy + for( i = 0; i < nItems; ++i ) + { + PDFOutlineEntry& rItem = m_aOutline[i]; + OStringBuffer aLine( 1024 ); + + CHECK_RETURN( updateObject( rItem.m_nObject ) ); + aLine.append( rItem.m_nObject ); + aLine.append( " 0 obj\n" ); + aLine.append( "<<" ); + // number of visible children (all levels) + if( i > 0 || aCounts[0] > 0 ) + { + aLine.append( "/Count " ); + aLine.append( aCounts[i] ); + } + if( ! rItem.m_aChildren.empty() ) + { + // children list: First, Last + aLine.append( "/First " ); + aLine.append( m_aOutline[rItem.m_aChildren.front()].m_nObject ); + aLine.append( " 0 R/Last " ); + aLine.append( m_aOutline[rItem.m_aChildren.back()].m_nObject ); + aLine.append( " 0 R\n" ); + } + if( i > 0 ) + { + // Title, Dest, Parent, Prev, Next + aLine.append( "/Title" ); + appendUnicodeTextStringEncrypt( rItem.m_aTitle, rItem.m_nObject, aLine ); + aLine.append( "\n" ); + // Dest is not required + if( rItem.m_nDestID >= 0 && rItem.m_nDestID < static_cast<sal_Int32>(m_aDests.size()) ) + { + aLine.append( "/Dest" ); + appendDest( rItem.m_nDestID, aLine ); + } + aLine.append( "/Parent " ); + aLine.append( rItem.m_nParentObject ); + aLine.append( " 0 R" ); + if( rItem.m_nPrevObject ) + { + aLine.append( "/Prev " ); + aLine.append( rItem.m_nPrevObject ); + aLine.append( " 0 R" ); + } + if( rItem.m_nNextObject ) + { + aLine.append( "/Next " ); + aLine.append( rItem.m_nNextObject ); + aLine.append( " 0 R" ); + } + } + aLine.append( ">>\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + } + + return m_aOutline[0].m_nObject; +} + +#undef CHECK_RETURN +#define CHECK_RETURN( x ) if( !x ) return false + +bool PDFWriterImpl::appendDest( sal_Int32 nDestID, OStringBuffer& rBuffer ) +{ + if( nDestID < 0 || nDestID >= static_cast<sal_Int32>(m_aDests.size()) ) + { + SAL_INFO("vcl.pdfwriter", "ERROR: invalid dest " << static_cast<int>(nDestID) << " requested"); + return false; + } + + const PDFDest& rDest = m_aDests[ nDestID ]; + const PDFPage& rDestPage = m_aPages[ rDest.m_nPage ]; + + rBuffer.append( '[' ); + rBuffer.append( rDestPage.m_nPageObject ); + rBuffer.append( " 0 R" ); + + switch( rDest.m_eType ) + { + case PDFWriter::DestAreaType::XYZ: + default: + rBuffer.append( "/XYZ " ); + appendFixedInt( rDest.m_aRect.Left(), rBuffer ); + rBuffer.append( ' ' ); + appendFixedInt( rDest.m_aRect.Bottom(), rBuffer ); + rBuffer.append( " 0" ); + break; + case PDFWriter::DestAreaType::FitRectangle: + rBuffer.append( "/FitR " ); + appendFixedInt( rDest.m_aRect.Left(), rBuffer ); + rBuffer.append( ' ' ); + appendFixedInt( rDest.m_aRect.Top(), rBuffer ); + rBuffer.append( ' ' ); + appendFixedInt( rDest.m_aRect.Right(), rBuffer ); + rBuffer.append( ' ' ); + appendFixedInt( rDest.m_aRect.Bottom(), rBuffer ); + break; + } + rBuffer.append( ']' ); + + return true; +} + +bool PDFWriterImpl::emitScreenAnnotations() +{ + int nAnnots = m_aScreens.size(); + for (int i = 0; i < nAnnots; i++) + { + const PDFScreen& rScreen = m_aScreens[i]; + + OStringBuffer aLine; + bool bEmbed = false; + if (!rScreen.m_aTempFileURL.isEmpty()) + { + bEmbed = true; + if (!updateObject(rScreen.m_nTempFileObject)) + continue; + + SvFileStream aFileStream(rScreen.m_aTempFileURL, StreamMode::READ); + SvMemoryStream aMemoryStream; + aMemoryStream.WriteStream(aFileStream); + + aLine.append(rScreen.m_nTempFileObject); + aLine.append(" 0 obj\n"); + aLine.append("<< /Type /EmbeddedFile /Length "); + aLine.append(static_cast<sal_Int64>(aMemoryStream.GetSize())); + aLine.append(" >>\nstream\n"); + CHECK_RETURN(writeBuffer(aLine.getStr(), aLine.getLength())); + aLine.setLength(0); + + CHECK_RETURN(writeBuffer(aMemoryStream.GetData(), aMemoryStream.GetSize())); + + aLine.append("\nendstream\nendobj\n\n"); + CHECK_RETURN(writeBuffer(aLine.getStr(), aLine.getLength())); + aLine.setLength(0); + } + + if (!updateObject(rScreen.m_nObject)) + continue; + + // Annot dictionary. + aLine.append(rScreen.m_nObject); + aLine.append(" 0 obj\n"); + aLine.append("<</Type/Annot"); + aLine.append("/Subtype/Screen/Rect["); + appendFixedInt(rScreen.m_aRect.Left(), aLine); + aLine.append(' '); + appendFixedInt(rScreen.m_aRect.Top(), aLine); + aLine.append(' '); + appendFixedInt(rScreen.m_aRect.Right(), aLine); + aLine.append(' '); + appendFixedInt(rScreen.m_aRect.Bottom(), aLine); + aLine.append("]"); + + // Action dictionary. + aLine.append("/A<</Type/Action /S/Rendition /AN "); + aLine.append(rScreen.m_nObject); + aLine.append(" 0 R "); + + // Rendition dictionary. + aLine.append("/R<</Type/Rendition /S/MR "); + + // MediaClip dictionary. + aLine.append("/C<</Type/MediaClip /S/MCD "); + if (bEmbed) + { + aLine.append("/D << /Type /Filespec /F (<embedded file>) /EF << /F "); + aLine.append(rScreen.m_nTempFileObject); + aLine.append(" 0 R >> >>"); + } + else + { + // Linked. + aLine.append("/D << /Type /Filespec /FS /URL /F "); + appendLiteralStringEncrypt(rScreen.m_aURL, rScreen.m_nObject, aLine, osl_getThreadTextEncoding()); + aLine.append(" >>"); + } + // Allow playing the video via a tempfile. + aLine.append("/P <</TF (TEMPACCESS)>>"); + // Until the real MIME type (instead of application/vnd.sun.star.media) is available here. + aLine.append("/CT (video/mp4)"); + aLine.append(">>"); + + // End Rendition dictionary by requesting play/pause/stop controls. + aLine.append("/P<</BE<</C true >>>>"); + aLine.append(">>"); + + // End Action dictionary. + aLine.append("/OP 0 >>"); + + // End Annot dictionary. + aLine.append("/P "); + aLine.append(m_aPages[rScreen.m_nPage].m_nPageObject); + aLine.append(" 0 R\n>>\nendobj\n\n"); + CHECK_RETURN(writeBuffer(aLine.getStr(), aLine.getLength())); + } + + return true; +} + +bool PDFWriterImpl::emitLinkAnnotations() +{ + MARK("PDFWriterImpl::emitLinkAnnotations"); + int nAnnots = m_aLinks.size(); + for( int i = 0; i < nAnnots; i++ ) + { + const PDFLink& rLink = m_aLinks[i]; + if( ! updateObject( rLink.m_nObject ) ) + continue; + + OStringBuffer aLine( 1024 ); + aLine.append( rLink.m_nObject ); + aLine.append( " 0 obj\n" ); +// i59651: key /F set bits Print to 1 rest to 0. We don't set NoZoom NoRotate to 1, since it's a 'should' +// see PDF 8.4.2 and ISO 19005-1:2005 6.5.3 + aLine.append( "<</Type/Annot" ); + if( m_bIsPDF_A1 || m_bIsPDF_A2 || m_bIsPDF_A3) + aLine.append( "/F 4" ); + aLine.append( "/Subtype/Link/Border[0 0 0]/Rect[" ); + + appendFixedInt( rLink.m_aRect.Left()-7, aLine );//the +7 to have a better shape of the border rectangle + aLine.append( ' ' ); + appendFixedInt( rLink.m_aRect.Top(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rLink.m_aRect.Right()+7, aLine );//the +7 to have a better shape of the border rectangle + aLine.append( ' ' ); + appendFixedInt( rLink.m_aRect.Bottom(), aLine ); + aLine.append( "]" ); + if( rLink.m_nDest >= 0 ) + { + aLine.append( "/Dest" ); + appendDest( rLink.m_nDest, aLine ); + } + else + { +/* +destination is external to the document, so +we check in the following sequence: + + if target type is neither .pdf, nor .od[tpgs], then + check if relative or absolute and act accordingly (use URI or 'launch application' as requested) + end processing + else if target is .od[tpgs]: then + if conversion of type from od[tpgs] to pdf is requested, convert it and this becomes the new target file + processing continue + + if (new)target is .pdf : then + if GotToR is requested, then + convert the target in GoToR where the fragment of the URI is + considered the named destination in the target file, set relative or absolute as requested + else strip the fragment from URL and then set URI or 'launch application' as requested +*/ + +// FIXME: check if the decode mechanisms for URL processing throughout this implementation +// are the correct one!! + +// extract target file type + auto url(URIHelper::resolveIdnaHost(rLink.m_aURL)); + + INetURLObject aDocumentURL( m_aContext.BaseURL ); + INetURLObject aTargetURL( url ); + bool bSetGoToRMode = false; + bool bTargetHasPDFExtension = false; + INetProtocol eTargetProtocol = aTargetURL.GetProtocol(); + bool bIsUNCPath = false; + + // check if the protocol is a known one, or if there is no protocol at all (on target only) + // if there is no protocol, make the target relative to the current document directory + // getting the needed URL information from the current document path + if( eTargetProtocol == INetProtocol::NotValid ) + { + if( url.getLength() > 4 && url.startsWith("\\\\\\\\")) + { + bIsUNCPath = true; + } + else + { + INetURLObject aNewBase( aDocumentURL );//duplicate document URL + aNewBase.removeSegment(); //remove last segment from it, obtaining the base URL of the + //target document + aNewBase.insertName( url ); + aTargetURL = aNewBase;//reassign the new target URL + //recompute the target protocol, with the new URL + //normal URL processing resumes + eTargetProtocol = aTargetURL.GetProtocol(); + } + } + + OUString aFileExtension = aTargetURL.GetFileExtension(); + + // Check if the URL ends in '/': if yes it's a directory, + // it will be forced to a URI link. + // possibly a malformed URI, leave it as it is, force as URI + if( aTargetURL.hasFinalSlash() ) + m_aContext.DefaultLinkAction = PDFWriter::URIAction; + + if( !aFileExtension.isEmpty() ) + { + if( m_aContext.ConvertOOoTargetToPDFTarget ) + { + bool bChangeFileExtensionToPDF = false; + //examine the file type (.odm .odt. .odp, odg, ods) + if( aFileExtension.equalsIgnoreAsciiCase( "odm" ) ) + bChangeFileExtensionToPDF = true; + if( aFileExtension.equalsIgnoreAsciiCase( "odt" ) ) + bChangeFileExtensionToPDF = true; + else if( aFileExtension.equalsIgnoreAsciiCase( "odp" ) ) + bChangeFileExtensionToPDF = true; + else if( aFileExtension.equalsIgnoreAsciiCase( "odg" ) ) + bChangeFileExtensionToPDF = true; + else if( aFileExtension.equalsIgnoreAsciiCase( "ods" ) ) + bChangeFileExtensionToPDF = true; + if( bChangeFileExtensionToPDF ) + aTargetURL.setExtension("pdf" ); + } + //check if extension is pdf, see if GoToR should be forced + bTargetHasPDFExtension = aTargetURL.GetFileExtension().equalsIgnoreAsciiCase( "pdf" ); + if( m_aContext.ForcePDFAction && bTargetHasPDFExtension ) + bSetGoToRMode = true; + } + //prepare the URL, if relative or not + INetProtocol eBaseProtocol = aDocumentURL.GetProtocol(); + //queue the string common to all types of actions + aLine.append( "/A<</Type/Action/S"); + if( bIsUNCPath ) // handle Win UNC paths + { + aLine.append( "/Launch/Win<</F" ); + // INetURLObject is not good with UNC paths, use original path + appendLiteralStringEncrypt( url, rLink.m_nObject, aLine, osl_getThreadTextEncoding() ); + aLine.append( ">>" ); + } + else + { + bool bSetRelative = false; + bool bFileSpec = false; + //check if relative file link is requested and if the protocol is 'file://' + if( m_aContext.RelFsys && eBaseProtocol == eTargetProtocol && eTargetProtocol == INetProtocol::File ) + bSetRelative = true; + + OUString aFragment = aTargetURL.GetMark( INetURLObject::DecodeMechanism::NONE /*DecodeMechanism::WithCharset*/ ); //fragment as is, + if( !bSetGoToRMode ) + { + switch( m_aContext.DefaultLinkAction ) + { + default: + case PDFWriter::URIAction : + case PDFWriter::URIActionDestination : + aLine.append( "/URI/URI" ); + break; + case PDFWriter::LaunchAction: + // now: + // if a launch action is requested and the hyperlink target has a fragment + // and the target file does not have a pdf extension, or it's not a 'file:://' + // protocol then force the uri action on it + // This code will permit the correct opening of application on web pages, + // the one that normally have fragments (but I may be wrong...) + // and will force the use of URI when the protocol is not file: + if( (!aFragment.isEmpty() && !bTargetHasPDFExtension) || + eTargetProtocol != INetProtocol::File ) + { + aLine.append( "/URI/URI" ); + } + else + { + aLine.append( "/Launch/F" ); + bFileSpec = true; + } + break; + } + } + + //fragment are encoded in the same way as in the named destination processing + if( bSetGoToRMode ) + { + //add the fragment + OUString aURLNoMark = aTargetURL.GetURLNoMark( INetURLObject::DecodeMechanism::WithCharset ); + aLine.append("/GoToR"); + aLine.append("/F"); + appendLiteralStringEncrypt( bSetRelative ? INetURLObject::GetRelURL( m_aContext.BaseURL, aURLNoMark, + INetURLObject::EncodeMechanism::WasEncoded, + INetURLObject::DecodeMechanism::WithCharset ) : + aURLNoMark, rLink.m_nObject, aLine, osl_getThreadTextEncoding() ); + if( !aFragment.isEmpty() ) + { + aLine.append("/D/"); + appendDestinationName( aFragment , aLine ); + } + } + else + { + // change the fragment to accommodate the bookmark (only if the file extension + // is PDF and the requested action is of the correct type) + if(m_aContext.DefaultLinkAction == PDFWriter::URIActionDestination && + bTargetHasPDFExtension && !aFragment.isEmpty() ) + { + OStringBuffer aLineLoc( 1024 ); + appendDestinationName( aFragment , aLineLoc ); + //substitute the fragment + aTargetURL.SetMark( OStringToOUString(aLineLoc.makeStringAndClear(), RTL_TEXTENCODING_ASCII_US) ); + } + OUString aURL = aTargetURL.GetMainURL( bFileSpec ? INetURLObject::DecodeMechanism::WithCharset : INetURLObject::DecodeMechanism::NONE ); + appendLiteralStringEncrypt(bSetRelative ? INetURLObject::GetRelURL( m_aContext.BaseURL, aURL, + INetURLObject::EncodeMechanism::WasEncoded, + bFileSpec ? INetURLObject::DecodeMechanism::WithCharset : INetURLObject::DecodeMechanism::NONE + ) : + aURL , rLink.m_nObject, aLine, osl_getThreadTextEncoding() ); + } + } + aLine.append( ">>\n" ); + } + if( rLink.m_nStructParent > 0 ) + { + aLine.append( "/StructParent " ); + aLine.append( rLink.m_nStructParent ); + } + aLine.append( ">>\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + } + + return true; +} + +bool PDFWriterImpl::emitNoteAnnotations() +{ + // emit note annotations + int nAnnots = m_aNotes.size(); + for( int i = 0; i < nAnnots; i++ ) + { + const PDFNoteEntry& rNote = m_aNotes[i]; + if( ! updateObject( rNote.m_nObject ) ) + return false; + + OStringBuffer aLine( 1024 ); + aLine.append( rNote.m_nObject ); + aLine.append( " 0 obj\n" ); +// i59651: key /F set bits Print to 1 rest to 0. We don't set NoZoom NoRotate to 1, since it's a 'should' +// see PDF 8.4.2 and ISO 19005-1:2005 6.5.3 + aLine.append( "<</Type/Annot" ); + if( m_bIsPDF_A1 || m_bIsPDF_A2 || m_bIsPDF_A3 ) + aLine.append( "/F 4" ); + aLine.append( "/Subtype/Text/Rect[" ); + + appendFixedInt( rNote.m_aRect.Left(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rNote.m_aRect.Top(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rNote.m_aRect.Right(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rNote.m_aRect.Bottom(), aLine ); + aLine.append( "]" ); + + // contents of the note (type text string) + aLine.append( "/Contents\n" ); + appendUnicodeTextStringEncrypt( rNote.m_aContents.Contents, rNote.m_nObject, aLine ); + aLine.append( "\n" ); + + // optional title + if( !rNote.m_aContents.Title.isEmpty() ) + { + aLine.append( "/T" ); + appendUnicodeTextStringEncrypt( rNote.m_aContents.Title, rNote.m_nObject, aLine ); + aLine.append( "\n" ); + } + + aLine.append( ">>\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + } + return true; +} + +Font PDFWriterImpl::replaceFont( const vcl::Font& rControlFont, const vcl::Font& rAppSetFont ) +{ + bool bAdjustSize = false; + + Font aFont( rControlFont ); + if( aFont.GetFamilyName().isEmpty() ) + { + aFont = rAppSetFont; + if( rControlFont.GetFontHeight() ) + aFont.SetFontSize( Size( 0, rControlFont.GetFontHeight() ) ); + else + bAdjustSize = true; + if( rControlFont.GetItalic() != ITALIC_DONTKNOW ) + aFont.SetItalic( rControlFont.GetItalic() ); + if( rControlFont.GetWeight() != WEIGHT_DONTKNOW ) + aFont.SetWeight( rControlFont.GetWeight() ); + } + else if( ! aFont.GetFontHeight() ) + { + aFont.SetFontSize( rAppSetFont.GetFontSize() ); + bAdjustSize = true; + } + if( bAdjustSize ) + { + Size aFontSize = aFont.GetFontSize(); + OutputDevice* pDefDev = Application::GetDefaultDevice(); + aFontSize = OutputDevice::LogicToLogic( aFontSize, pDefDev->GetMapMode(), getMapMode() ); + aFont.SetFontSize( aFontSize ); + } + return aFont; +} + +sal_Int32 PDFWriterImpl::getBestBuildinFont( const vcl::Font& rFont ) +{ + sal_Int32 nBest = 4; // default to Helvetica + OUString aFontName( rFont.GetFamilyName() ); + aFontName = aFontName.toAsciiLowerCase(); + + if( aFontName.indexOf( "times" ) != -1 ) + nBest = 8; + else if( aFontName.indexOf( "courier" ) != -1 ) + nBest = 0; + else if( aFontName.indexOf( "dingbats" ) != -1 ) + nBest = 13; + else if( aFontName.indexOf( "symbol" ) != -1 ) + nBest = 12; + if( nBest < 12 ) + { + if( rFont.GetItalic() == ITALIC_OBLIQUE || rFont.GetItalic() == ITALIC_NORMAL ) + nBest += 1; + if( rFont.GetWeight() > WEIGHT_MEDIUM ) + nBest += 2; + } + + if( m_aBuildinFontToObjectMap.find( nBest ) == m_aBuildinFontToObjectMap.end() ) + m_aBuildinFontToObjectMap[ nBest ] = createObject(); + + return nBest; +} + +static const Color& replaceColor( const Color& rCol1, const Color& rCol2 ) +{ + return (rCol1 == COL_TRANSPARENT) ? rCol2 : rCol1; +} + +void PDFWriterImpl::createDefaultPushButtonAppearance( PDFWidget& rButton, const PDFWriter::PushButtonWidget& rWidget ) +{ + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + + // save graphics state + push( PushFlags::ALL ); + + // transform relative to control's coordinates since an + // appearance stream is a form XObject + // this relies on the m_aRect member of rButton NOT already being transformed + // to default user space + if( rWidget.Background || rWidget.Border ) + { + setLineColor( rWidget.Border ? replaceColor( rWidget.BorderColor, rSettings.GetLightColor() ) : COL_TRANSPARENT ); + setFillColor( rWidget.Background ? replaceColor( rWidget.BackgroundColor, rSettings.GetDialogColor() ) : COL_TRANSPARENT ); + drawRectangle( rWidget.Location ); + } + // prepare font to use + Font aFont = replaceFont( rWidget.TextFont, rSettings.GetPushButtonFont() ); + setFont( aFont ); + setTextColor( replaceColor( rWidget.TextColor, rSettings.GetButtonTextColor() ) ); + + drawText( rButton.m_aRect, rButton.m_aText, rButton.m_nTextStyle ); + + // create DA string while local mapmode is still in place + // (that is before endRedirect()) + OStringBuffer aDA( 256 ); + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetButtonTextColor() ), aDA ); + Font aDummyFont( "Helvetica", aFont.GetFontSize() ); + sal_Int32 nDummyBuildin = getBestBuildinFont( aDummyFont ); + aDA.append( ' ' ); + aDA.append(pdf::BuildinFontFace::Get(nDummyBuildin).getNameObject()); + aDA.append( ' ' ); + m_aPages[m_nCurrentPage].appendMappedLength( sal_Int32( aFont.GetFontHeight() ), aDA ); + aDA.append( " Tf" ); + rButton.m_aDAString = aDA.makeStringAndClear(); + + pop(); + + rButton.m_aAppearances[ "N" ][ "Standard" ] = new SvMemoryStream(); + + /* seems like a bad hack but at least works in both AR5 and 6: + we draw the button ourselves and tell AR + the button would be totally transparent with no text + + One would expect that simply setting a normal appearance + should suffice, but no, as soon as the user actually presses + the button and an action is tied to it (gasp! a button that + does something) the appearance gets replaced by some crap that AR + creates on the fly even if no DA or MK is given. On AR6 at least + the DA and MK work as expected, but on AR5 this creates a region + filled with the background color but nor text. Urgh. + */ + rButton.m_aMKDict = "/BC [] /BG [] /CA"; + rButton.m_aMKDictCAString = ""; +} + +Font PDFWriterImpl::drawFieldBorder( PDFWidget& rIntern, + const PDFWriter::AnyWidget& rWidget, + const StyleSettings& rSettings ) +{ + Font aFont = replaceFont( rWidget.TextFont, rSettings.GetFieldFont() ); + + if( rWidget.Background || rWidget.Border ) + { + if( rWidget.Border && rWidget.BorderColor == COL_TRANSPARENT ) + { + sal_Int32 nDelta = GetDPIX() / 500; + if( nDelta < 1 ) + nDelta = 1; + setLineColor( COL_TRANSPARENT ); + tools::Rectangle aRect = rIntern.m_aRect; + setFillColor( rSettings.GetLightBorderColor() ); + drawRectangle( aRect ); + aRect.AdjustLeft(nDelta ); aRect.AdjustTop(nDelta ); + aRect.AdjustRight( -nDelta ); aRect.AdjustBottom( -nDelta ); + setFillColor( rSettings.GetFieldColor() ); + drawRectangle( aRect ); + setFillColor( rSettings.GetLightColor() ); + drawRectangle( tools::Rectangle( Point( aRect.Left(), aRect.Bottom()-nDelta ), aRect.BottomRight() ) ); + drawRectangle( tools::Rectangle( Point( aRect.Right()-nDelta, aRect.Top() ), aRect.BottomRight() ) ); + setFillColor( rSettings.GetDarkShadowColor() ); + drawRectangle( tools::Rectangle( aRect.TopLeft(), Point( aRect.Left()+nDelta, aRect.Bottom() ) ) ); + drawRectangle( tools::Rectangle( aRect.TopLeft(), Point( aRect.Right(), aRect.Top()+nDelta ) ) ); + } + else + { + setLineColor( rWidget.Border ? replaceColor( rWidget.BorderColor, rSettings.GetShadowColor() ) : COL_TRANSPARENT ); + setFillColor( rWidget.Background ? replaceColor( rWidget.BackgroundColor, rSettings.GetFieldColor() ) : COL_TRANSPARENT ); + drawRectangle( rIntern.m_aRect ); + } + + if( rWidget.Border ) + { + // adjust edit area accounting for border + sal_Int32 nDelta = aFont.GetFontHeight()/4; + if( nDelta < 1 ) + nDelta = 1; + rIntern.m_aRect.AdjustLeft(nDelta ); + rIntern.m_aRect.AdjustTop(nDelta ); + rIntern.m_aRect.AdjustRight( -nDelta ); + rIntern.m_aRect.AdjustBottom( -nDelta ); + } + } + return aFont; +} + +void PDFWriterImpl::createDefaultEditAppearance( PDFWidget& rEdit, const PDFWriter::EditWidget& rWidget ) +{ + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + SvMemoryStream* pEditStream = new SvMemoryStream( 1024, 1024 ); + + push( PushFlags::ALL ); + + // prepare font to use, draw field border + Font aFont = drawFieldBorder( rEdit, rWidget, rSettings ); + sal_Int32 nBest = getSystemFont( aFont ); + + // prepare DA string + OStringBuffer aDA( 32 ); + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetFieldTextColor() ), aDA ); + aDA.append( ' ' ); + aDA.append( "/F" ); + aDA.append( nBest ); + + OStringBuffer aDR( 32 ); + aDR.append( "/Font " ); + aDR.append( getFontDictObject() ); + aDR.append( " 0 R" ); + rEdit.m_aDRDict = aDR.makeStringAndClear(); + aDA.append( ' ' ); + m_aPages[ m_nCurrentPage ].appendMappedLength( sal_Int32( aFont.GetFontHeight() ), aDA ); + aDA.append( " Tf" ); + + /* create an empty appearance stream, let the viewer create + the appearance at runtime. This is because AR5 seems to + paint the widget appearance always, and a dynamically created + appearance on top of it. AR6 is well behaved in that regard, so + that behaviour seems to be a bug. Anyway this empty appearance + relies on /NeedAppearances in the AcroForm dictionary set to "true" + */ + beginRedirect( pEditStream, rEdit.m_aRect ); + OString aAppearance = "/Tx BMC\nEMC\n"; + writeBuffer( aAppearance.getStr(), aAppearance.getLength() ); + + endRedirect(); + pop(); + + rEdit.m_aAppearances[ "N" ][ "Standard" ] = pEditStream; + + rEdit.m_aDAString = aDA.makeStringAndClear(); +} + +void PDFWriterImpl::createDefaultListBoxAppearance( PDFWidget& rBox, const PDFWriter::ListBoxWidget& rWidget ) +{ + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + SvMemoryStream* pListBoxStream = new SvMemoryStream( 1024, 1024 ); + + push( PushFlags::ALL ); + + // prepare font to use, draw field border + Font aFont = drawFieldBorder( rBox, rWidget, rSettings ); + sal_Int32 nBest = getSystemFont( aFont ); + + beginRedirect( pListBoxStream, rBox.m_aRect ); + + setLineColor( COL_TRANSPARENT ); + setFillColor( replaceColor( rWidget.BackgroundColor, rSettings.GetFieldColor() ) ); + drawRectangle( rBox.m_aRect ); + + // empty appearance, see createDefaultEditAppearance for reference + OString aAppearance = "/Tx BMC\nEMC\n"; + writeBuffer( aAppearance.getStr(), aAppearance.getLength() ); + + endRedirect(); + pop(); + + rBox.m_aAppearances[ "N" ][ "Standard" ] = pListBoxStream; + + // prepare DA string + OStringBuffer aDA( 256 ); + // prepare DA string + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetFieldTextColor() ), aDA ); + aDA.append( ' ' ); + aDA.append( "/F" ); + aDA.append( nBest ); + + OStringBuffer aDR( 32 ); + aDR.append( "/Font " ); + aDR.append( getFontDictObject() ); + aDR.append( " 0 R" ); + rBox.m_aDRDict = aDR.makeStringAndClear(); + aDA.append( ' ' ); + m_aPages[ m_nCurrentPage ].appendMappedLength( sal_Int32( aFont.GetFontHeight() ), aDA ); + aDA.append( " Tf" ); + rBox.m_aDAString = aDA.makeStringAndClear(); +} + +void PDFWriterImpl::createDefaultCheckBoxAppearance( PDFWidget& rBox, const PDFWriter::CheckBoxWidget& rWidget ) +{ + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + + // save graphics state + push( PushFlags::ALL ); + + if( rWidget.Background || rWidget.Border ) + { + setLineColor( rWidget.Border ? replaceColor( rWidget.BorderColor, rSettings.GetCheckedColor() ) : COL_TRANSPARENT ); + setFillColor( rWidget.Background ? replaceColor( rWidget.BackgroundColor, rSettings.GetFieldColor() ) : COL_TRANSPARENT ); + drawRectangle( rBox.m_aRect ); + } + + Font aFont = replaceFont( rWidget.TextFont, rSettings.GetRadioCheckFont() ); + setFont( aFont ); + Size aFontSize = aFont.GetFontSize(); + if( aFontSize.Height() > rBox.m_aRect.GetHeight() ) + aFontSize.setHeight( rBox.m_aRect.GetHeight() ); + sal_Int32 nDelta = aFontSize.Height()/10; + if( nDelta < 1 ) + nDelta = 1; + + tools::Rectangle aCheckRect, aTextRect; + { + aCheckRect.SetLeft( rBox.m_aRect.Left() + nDelta ); + aCheckRect.SetTop( rBox.m_aRect.Top() + (rBox.m_aRect.GetHeight()-aFontSize.Height())/2 ); + aCheckRect.SetRight( aCheckRect.Left() + aFontSize.Height() ); + aCheckRect.SetBottom( aCheckRect.Top() + aFontSize.Height() ); + + // #i74206# handle small controls without text area + while( aCheckRect.GetWidth() > rBox.m_aRect.GetWidth() && aCheckRect.GetWidth() > nDelta ) + { + aCheckRect.AdjustRight( -nDelta ); + aCheckRect.AdjustTop(nDelta/2 ); + aCheckRect.AdjustBottom( -(nDelta - (nDelta/2)) ); + } + + aTextRect.SetLeft( rBox.m_aRect.Left() + aCheckRect.GetWidth()+5*nDelta ); + aTextRect.SetTop( rBox.m_aRect.Top() ); + aTextRect.SetRight( aTextRect.Left() + rBox.m_aRect.GetWidth() - aCheckRect.GetWidth()-6*nDelta ); + aTextRect.SetBottom( rBox.m_aRect.Bottom() ); + } + setLineColor( COL_BLACK ); + setFillColor( COL_TRANSPARENT ); + OStringBuffer aLW( 32 ); + aLW.append( "q " ); + m_aPages[m_nCurrentPage].appendMappedLength( nDelta, aLW ); + aLW.append( " w " ); + writeBuffer( aLW.getStr(), aLW.getLength() ); + drawRectangle( aCheckRect ); + writeBuffer( " Q\n", 3 ); + setTextColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ) ); + drawText( aTextRect, rBox.m_aText, rBox.m_nTextStyle ); + + pop(); + + OStringBuffer aDA( 256 ); + + // tdf#93853 don't rely on Zapf (or any other 'standard' font) + // being present, but our own OpenSymbol - N.B. PDF/A for good + // reasons require even the standard PS fonts to be embedded! + Push(); + SetFont( Font( OUString( "OpenSymbol" ), aFont.GetFontSize() ) ); + FontCharMapRef pMap; + GetFontCharMap(pMap); + const LogicalFontInstance* pFontInstance = GetFontInstance(); + const PhysicalFontFace* pDevFont = pFontInstance->GetFontFace(); + Pop(); + + // make sure OpenSymbol is embedded, and includes our checkmark + const sal_Unicode cMark=0x2713; + const GlyphItem aItem(0, 0, pMap->GetGlyphIndex(cMark), + Point(), GlyphItemFlags::NONE, 0, 0, + const_cast<LogicalFontInstance*>(pFontInstance)); + const std::vector<sal_Ucs> aCodeUnits={ cMark }; + sal_uInt8 nMappedGlyph; + sal_Int32 nMappedFontObject; + registerGlyph(&aItem, pDevFont, aCodeUnits, nMappedGlyph, nMappedFontObject); + + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ), aDA ); + aDA.append( ' ' ); + aDA.append( "/F" ); + aDA.append( nMappedFontObject ); + aDA.append( " 0 Tf" ); + + OStringBuffer aDR( 32 ); + aDR.append( "/Font " ); + aDR.append( getFontDictObject() ); + aDR.append( " 0 R" ); + rBox.m_aDRDict = aDR.makeStringAndClear(); + rBox.m_aDAString = aDA.makeStringAndClear(); + rBox.m_aMKDict = "/CA"; + rBox.m_aMKDictCAString = "8"; + rBox.m_aRect = aCheckRect; + + // create appearance streams + sal_Int32 nCharXOffset = 1000 - 787; // metrics from OpenSymbol + nCharXOffset *= aCheckRect.GetHeight(); + nCharXOffset /= 2000; + sal_Int32 nCharYOffset = 1000 - (820-143); // metrics from Zapf + nCharYOffset *= aCheckRect.GetHeight(); + nCharYOffset /= 2000; + + // write 'checked' appearance stream + SvMemoryStream* pCheckStream = new SvMemoryStream( 256, 256 ); + beginRedirect( pCheckStream, aCheckRect ); + aDA.append( "/Tx BMC\nq BT\n" ); + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ), aDA ); + aDA.append( ' ' ); + aDA.append( "/F" ); + aDA.append( nMappedFontObject ); + aDA.append( ' ' ); + m_aPages[ m_nCurrentPage ].appendMappedLength( sal_Int32( aCheckRect.GetHeight() ), aDA ); + aDA.append( " Tf\n" ); + m_aPages[ m_nCurrentPage ].appendMappedLength( nCharXOffset, aDA ); + aDA.append( " " ); + m_aPages[ m_nCurrentPage ].appendMappedLength( nCharYOffset, aDA ); + aDA.append( " Td <" ); + appendHex( nMappedGlyph, aDA ); + aDA.append( "> Tj\nET\nQ\nEMC\n" ); + writeBuffer( aDA.getStr(), aDA.getLength() ); + endRedirect(); + rBox.m_aAppearances[ "N" ][ "Yes" ] = pCheckStream; + + // write 'unchecked' appearance stream + SvMemoryStream* pUncheckStream = new SvMemoryStream( 256, 256 ); + beginRedirect( pUncheckStream, aCheckRect ); + writeBuffer( "/Tx BMC\nEMC\n", 12 ); + endRedirect(); + rBox.m_aAppearances[ "N" ][ "Off" ] = pUncheckStream; +} + +void PDFWriterImpl::createDefaultRadioButtonAppearance( PDFWidget& rBox, const PDFWriter::RadioButtonWidget& rWidget ) +{ + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + + // save graphics state + push( PushFlags::ALL ); + + if( rWidget.Background || rWidget.Border ) + { + setLineColor( rWidget.Border ? replaceColor( rWidget.BorderColor, rSettings.GetCheckedColor() ) : COL_TRANSPARENT ); + setFillColor( rWidget.Background ? replaceColor( rWidget.BackgroundColor, rSettings.GetFieldColor() ) : COL_TRANSPARENT ); + drawRectangle( rBox.m_aRect ); + } + + Font aFont = replaceFont( rWidget.TextFont, rSettings.GetRadioCheckFont() ); + setFont( aFont ); + Size aFontSize = aFont.GetFontSize(); + if( aFontSize.Height() > rBox.m_aRect.GetHeight() ) + aFontSize.setHeight( rBox.m_aRect.GetHeight() ); + sal_Int32 nDelta = aFontSize.Height()/10; + if( nDelta < 1 ) + nDelta = 1; + + tools::Rectangle aCheckRect, aTextRect; + { + aCheckRect.SetLeft( rBox.m_aRect.Left() + nDelta ); + aCheckRect.SetTop( rBox.m_aRect.Top() + (rBox.m_aRect.GetHeight()-aFontSize.Height())/2 ); + aCheckRect.SetRight( aCheckRect.Left() + aFontSize.Height() ); + aCheckRect.SetBottom( aCheckRect.Top() + aFontSize.Height() ); + + // #i74206# handle small controls without text area + while( aCheckRect.GetWidth() > rBox.m_aRect.GetWidth() && aCheckRect.GetWidth() > nDelta ) + { + aCheckRect.AdjustRight( -nDelta ); + aCheckRect.AdjustTop(nDelta/2 ); + aCheckRect.AdjustBottom( -(nDelta - (nDelta/2)) ); + } + + aTextRect.SetLeft( rBox.m_aRect.Left() + aCheckRect.GetWidth()+5*nDelta ); + aTextRect.SetTop( rBox.m_aRect.Top() ); + aTextRect.SetRight( aTextRect.Left() + rBox.m_aRect.GetWidth() - aCheckRect.GetWidth()-6*nDelta ); + aTextRect.SetBottom( rBox.m_aRect.Bottom() ); + } + setLineColor( COL_BLACK ); + setFillColor( COL_TRANSPARENT ); + OStringBuffer aLW( 32 ); + aLW.append( "q " ); + m_aPages[ m_nCurrentPage ].appendMappedLength( nDelta, aLW ); + aLW.append( " w " ); + writeBuffer( aLW.getStr(), aLW.getLength() ); + drawEllipse( aCheckRect ); + writeBuffer( " Q\n", 3 ); + setTextColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ) ); + drawText( aTextRect, rBox.m_aText, rBox.m_nTextStyle ); + + pop(); + + OStringBuffer aDA( 256 ); + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ), aDA ); + rBox.m_aDAString = aDA.makeStringAndClear(); + //to encrypt this (el) + rBox.m_aMKDict = "/CA"; + //after this assignment, to m_aMKDic cannot be added anything + rBox.m_aMKDictCAString = "l"; + + rBox.m_aRect = aCheckRect; + + // create appearance streams + push( PushFlags::ALL); + SvMemoryStream* pCheckStream = new SvMemoryStream( 256, 256 ); + + beginRedirect( pCheckStream, aCheckRect ); + aDA.append( "/Tx BMC\nq BT\n" ); + appendNonStrokingColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ), aDA ); + aDA.append( ' ' ); + m_aPages[m_nCurrentPage].appendMappedLength( sal_Int32( aCheckRect.GetHeight() ), aDA ); + aDA.append( " 0 0 Td\nET\nQ\n" ); + writeBuffer( aDA.getStr(), aDA.getLength() ); + setFillColor( replaceColor( rWidget.TextColor, rSettings.GetRadioCheckTextColor() ) ); + setLineColor( COL_TRANSPARENT ); + aCheckRect.AdjustLeft(3*nDelta ); + aCheckRect.AdjustTop(3*nDelta ); + aCheckRect.AdjustBottom( -(3*nDelta) ); + aCheckRect.AdjustRight( -(3*nDelta) ); + drawEllipse( aCheckRect ); + writeBuffer( "\nEMC\n", 5 ); + endRedirect(); + + pop(); + rBox.m_aAppearances[ "N" ][ "Yes" ] = pCheckStream; + + SvMemoryStream* pUncheckStream = new SvMemoryStream( 256, 256 ); + beginRedirect( pUncheckStream, aCheckRect ); + writeBuffer( "/Tx BMC\nEMC\n", 12 ); + endRedirect(); + rBox.m_aAppearances[ "N" ][ "Off" ] = pUncheckStream; +} + +bool PDFWriterImpl::emitAppearances( PDFWidget& rWidget, OStringBuffer& rAnnotDict ) +{ + // TODO: check and insert default streams + OString aStandardAppearance; + switch( rWidget.m_eType ) + { + case PDFWriter::CheckBox: + aStandardAppearance = OUStringToOString( rWidget.m_aValue, RTL_TEXTENCODING_ASCII_US ); + break; + default: + break; + } + + if( !rWidget.m_aAppearances.empty() ) + { + rAnnotDict.append( "/AP<<\n" ); + for (auto & dict_item : rWidget.m_aAppearances) + { + rAnnotDict.append( "/" ); + rAnnotDict.append( dict_item.first ); + bool bUseSubDict = (dict_item.second.size() > 1); + + // PDF/A requires sub-dicts for /FT/Btn objects (clause + // 6.3.3) + if( m_bIsPDF_A1 || m_bIsPDF_A2 || m_bIsPDF_A3) + { + if( rWidget.m_eType == PDFWriter::RadioButton || + rWidget.m_eType == PDFWriter::CheckBox || + rWidget.m_eType == PDFWriter::PushButton ) + { + bUseSubDict = true; + } + } + + rAnnotDict.append( bUseSubDict ? "<<" : " " ); + + for (auto const& stream_item : dict_item.second) + { + SvMemoryStream* pApppearanceStream = stream_item.second; + dict_item.second[ stream_item.first ] = nullptr; + + bool bDeflate = compressStream( pApppearanceStream ); + + sal_Int64 nStreamLen = pApppearanceStream->TellEnd(); + pApppearanceStream->Seek( STREAM_SEEK_TO_BEGIN ); + sal_Int32 nObject = createObject(); + CHECK_RETURN( updateObject( nObject ) ); + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::emitAppearances" ); + } + OStringBuffer aLine; + aLine.append( nObject ); + + aLine.append( " 0 obj\n" + "<</Type/XObject\n" + "/Subtype/Form\n" + "/BBox[0 0 " ); + appendFixedInt( rWidget.m_aRect.GetWidth()-1, aLine ); + aLine.append( " " ); + appendFixedInt( rWidget.m_aRect.GetHeight()-1, aLine ); + aLine.append( "]\n" + "/Resources " ); + aLine.append( getResourceDictObj() ); + aLine.append( " 0 R\n" + "/Length " ); + aLine.append( nStreamLen ); + aLine.append( "\n" ); + if( bDeflate ) + aLine.append( "/Filter/FlateDecode\n" ); + aLine.append( ">>\nstream\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + checkAndEnableStreamEncryption( nObject ); + CHECK_RETURN( writeBuffer( pApppearanceStream->GetData(), nStreamLen ) ); + disableStreamEncryption(); + CHECK_RETURN( writeBuffer( "\nendstream\nendobj\n\n", 19 ) ); + + if( bUseSubDict ) + { + rAnnotDict.append( " /" ); + rAnnotDict.append( stream_item.first ); + rAnnotDict.append( " " ); + } + rAnnotDict.append( nObject ); + rAnnotDict.append( " 0 R" ); + + delete pApppearanceStream; + } + + rAnnotDict.append( bUseSubDict ? ">>\n" : "\n" ); + } + rAnnotDict.append( ">>\n" ); + if( !aStandardAppearance.isEmpty() ) + { + rAnnotDict.append( "/AS /" ); + rAnnotDict.append( aStandardAppearance ); + rAnnotDict.append( "\n" ); + } + } + + return true; +} + +bool PDFWriterImpl::emitWidgetAnnotations() +{ + ensureUniqueRadioOnValues(); + + int nAnnots = m_aWidgets.size(); + for( int a = 0; a < nAnnots; a++ ) + { + PDFWidget& rWidget = m_aWidgets[a]; + + OStringBuffer aLine( 1024 ); + OStringBuffer aValue( 256 ); + aLine.append( rWidget.m_nObject ); + aLine.append( " 0 obj\n" + "<<" ); + if( rWidget.m_eType != PDFWriter::Hierarchy ) + { + // emit widget annotation only for terminal fields + if( rWidget.m_aKids.empty() ) + { + int iRectMargin; + + aLine.append( "/Type/Annot/Subtype/Widget/F " ); + + if (rWidget.m_eType == PDFWriter::Signature) + { + aLine.append( "132\n" ); // Print & Locked + iRectMargin = 0; + } + else + { + aLine.append( "4\n" ); + iRectMargin = 1; + } + + aLine.append("/Rect[" ); + appendFixedInt( rWidget.m_aRect.Left()-iRectMargin, aLine ); + aLine.append( ' ' ); + appendFixedInt( rWidget.m_aRect.Top()+iRectMargin, aLine ); + aLine.append( ' ' ); + appendFixedInt( rWidget.m_aRect.Right()+iRectMargin, aLine ); + aLine.append( ' ' ); + appendFixedInt( rWidget.m_aRect.Bottom()-iRectMargin, aLine ); + aLine.append( "]\n" ); + } + aLine.append( "/FT/" ); + switch( rWidget.m_eType ) + { + case PDFWriter::RadioButton: + case PDFWriter::CheckBox: + // for radio buttons only the RadioButton field, not the + // CheckBox children should have a value, else acrobat reader + // does not always check the right button + // of course real check boxes (not belonging to a radio group) + // need their values, too + if( rWidget.m_eType == PDFWriter::RadioButton || rWidget.m_nRadioGroup < 0 ) + { + aValue.append( "/" ); + // check for radio group with all buttons unpressed + if( rWidget.m_aValue.isEmpty() ) + aValue.append( "Off" ); + else + appendName( rWidget.m_aValue, aValue ); + } + [[fallthrough]]; + case PDFWriter::PushButton: + aLine.append( "Btn" ); + break; + case PDFWriter::ListBox: + if( rWidget.m_nFlags & 0x200000 ) // multiselect + { + aValue.append( "[" ); + for( size_t i = 0; i < rWidget.m_aSelectedEntries.size(); i++ ) + { + sal_Int32 nEntry = rWidget.m_aSelectedEntries[i]; + if( nEntry >= 0 && nEntry < sal_Int32(rWidget.m_aListEntries.size()) ) + appendUnicodeTextStringEncrypt( rWidget.m_aListEntries[ nEntry ], rWidget.m_nObject, aValue ); + } + aValue.append( "]" ); + } + else if( !rWidget.m_aSelectedEntries.empty() && + rWidget.m_aSelectedEntries[0] >= 0 && + rWidget.m_aSelectedEntries[0] < sal_Int32(rWidget.m_aListEntries.size()) ) + { + appendUnicodeTextStringEncrypt( rWidget.m_aListEntries[ rWidget.m_aSelectedEntries[0] ], rWidget.m_nObject, aValue ); + } + else + appendUnicodeTextStringEncrypt( OUString(), rWidget.m_nObject, aValue ); + aLine.append( "Ch" ); + break; + case PDFWriter::ComboBox: + appendUnicodeTextStringEncrypt( rWidget.m_aValue, rWidget.m_nObject, aValue ); + aLine.append( "Ch" ); + break; + case PDFWriter::Edit: + aLine.append( "Tx" ); + appendUnicodeTextStringEncrypt( rWidget.m_aValue, rWidget.m_nObject, aValue ); + break; + case PDFWriter::Signature: + aLine.append( "Sig" ); + aValue.append(OUStringToOString(rWidget.m_aValue, RTL_TEXTENCODING_ASCII_US)); + break; + case PDFWriter::Hierarchy: // make the compiler happy + break; + } + aLine.append( "\n" ); + aLine.append( "/P " ); + aLine.append( m_aPages[ rWidget.m_nPage ].m_nPageObject ); + aLine.append( " 0 R\n" ); + } + if( rWidget.m_nParent ) + { + aLine.append( "/Parent " ); + aLine.append( rWidget.m_nParent ); + aLine.append( " 0 R\n" ); + } + if( !rWidget.m_aKids.empty() ) + { + aLine.append( "/Kids[" ); + for( size_t i = 0; i < rWidget.m_aKids.size(); i++ ) + { + aLine.append( rWidget.m_aKids[i] ); + aLine.append( " 0 R" ); + aLine.append( ( (i&15) == 15 ) ? "\n" : " " ); + } + aLine.append( "]\n" ); + } + if( !rWidget.m_aName.isEmpty() ) + { + aLine.append( "/T" ); + appendLiteralStringEncrypt( rWidget.m_aName, rWidget.m_nObject, aLine ); + aLine.append( "\n" ); + } + if( m_aContext.Version > PDFWriter::PDFVersion::PDF_1_2 && !rWidget.m_aDescription.isEmpty() ) + { + // the alternate field name should be unicode able since it is + // supposed to be used in UI + aLine.append( "/TU" ); + appendUnicodeTextStringEncrypt( rWidget.m_aDescription, rWidget.m_nObject, aLine ); + aLine.append( "\n" ); + } + + if( rWidget.m_nFlags ) + { + aLine.append( "/Ff " ); + aLine.append( rWidget.m_nFlags ); + aLine.append( "\n" ); + } + if( !aValue.isEmpty() ) + { + OString aVal = aValue.makeStringAndClear(); + aLine.append( "/V " ); + aLine.append( aVal ); + aLine.append( "\n" + "/DV " ); + aLine.append( aVal ); + aLine.append( "\n" ); + } + if( rWidget.m_eType == PDFWriter::ListBox || rWidget.m_eType == PDFWriter::ComboBox ) + { + sal_Int32 nTI = -1; + aLine.append( "/Opt[\n" ); + sal_Int32 i = 0; + for (auto const& entry : rWidget.m_aListEntries) + { + appendUnicodeTextStringEncrypt( entry, rWidget.m_nObject, aLine ); + aLine.append( "\n" ); + if( entry == rWidget.m_aValue ) + nTI = i; + ++i; + } + aLine.append( "]\n" ); + if( nTI > 0 ) + { + aLine.append( "/TI " ); + aLine.append( nTI ); + aLine.append( "\n" ); + if( rWidget.m_nFlags & 0x200000 ) // Multiselect + { + aLine.append( "/I [" ); + aLine.append( nTI ); + aLine.append( "]\n" ); + } + } + } + if( rWidget.m_eType == PDFWriter::Edit && rWidget.m_nMaxLen > 0 ) + { + aLine.append( "/MaxLen " ); + aLine.append( rWidget.m_nMaxLen ); + aLine.append( "\n" ); + } + if( rWidget.m_eType == PDFWriter::PushButton ) + { + if(!m_bIsPDF_A1) + { + OStringBuffer aDest; + if( rWidget.m_nDest != -1 && appendDest( m_aDestinationIdTranslation[ rWidget.m_nDest ], aDest ) ) + { + aLine.append( "/AA<</D<</Type/Action/S/GoTo/D " ); + aLine.append( aDest.makeStringAndClear() ); + aLine.append( ">>>>\n" ); + } + else if( rWidget.m_aListEntries.empty() ) + { + if( !m_bIsPDF_A2 && !m_bIsPDF_A3 ) + { + // create a reset form action + aLine.append( "/AA<</D<</Type/Action/S/ResetForm>>>>\n" ); + } + } + else if( rWidget.m_bSubmit ) + { + // create a submit form action + aLine.append( "/AA<</D<</Type/Action/S/SubmitForm/F" ); + appendLiteralStringEncrypt( rWidget.m_aListEntries.front(), rWidget.m_nObject, aLine, osl_getThreadTextEncoding() ); + aLine.append( "/Flags " ); + + sal_Int32 nFlags = 0; + switch( m_aContext.SubmitFormat ) + { + case PDFWriter::HTML: + nFlags |= 4; + break; + case PDFWriter::XML: + if( m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 ) + nFlags |= 32; + break; + case PDFWriter::PDF: + if( m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 ) + nFlags |= 256; + break; + case PDFWriter::FDF: + default: + break; + } + if( rWidget.m_bSubmitGet ) + nFlags |= 8; + aLine.append( nFlags ); + aLine.append( ">>>>\n" ); + } + else + { + // create a URI action + aLine.append( "/AA<</D<</Type/Action/S/URI/URI(" ); + aLine.append( OUStringToOString( rWidget.m_aListEntries.front(), RTL_TEXTENCODING_ASCII_US ) ); + aLine.append( ")>>>>\n" ); + } + } + else + m_aErrors.insert( PDFWriter::Warning_FormAction_Omitted_PDFA ); + } + if( !rWidget.m_aDAString.isEmpty() ) + { + if( !rWidget.m_aDRDict.isEmpty() ) + { + aLine.append( "/DR<<" ); + aLine.append( rWidget.m_aDRDict ); + aLine.append( ">>\n" ); + } + else + { + aLine.append( "/DR<</Font<<" ); + appendBuildinFontsToDict( aLine ); + aLine.append( ">>>>\n" ); + } + aLine.append( "/DA" ); + appendLiteralStringEncrypt( rWidget.m_aDAString, rWidget.m_nObject, aLine ); + aLine.append( "\n" ); + if( rWidget.m_nTextStyle & DrawTextFlags::Center ) + aLine.append( "/Q 1\n" ); + else if( rWidget.m_nTextStyle & DrawTextFlags::Right ) + aLine.append( "/Q 2\n" ); + } + // appearance characteristics for terminal fields + // which are supposed to have an appearance constructed + // by the viewer application + if( !rWidget.m_aMKDict.isEmpty() ) + { + aLine.append( "/MK<<" ); + aLine.append( rWidget.m_aMKDict ); + //add the CA string, encrypting it + appendLiteralStringEncrypt(rWidget.m_aMKDictCAString, rWidget.m_nObject, aLine); + aLine.append( ">>\n" ); + } + + CHECK_RETURN( emitAppearances( rWidget, aLine ) ); + + aLine.append( ">>\n" + "endobj\n\n" ); + CHECK_RETURN( updateObject( rWidget.m_nObject ) ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + } + return true; +} + +bool PDFWriterImpl::emitAnnotations() +{ + if( m_aPages.empty() ) + return false; + + CHECK_RETURN( emitLinkAnnotations() ); + CHECK_RETURN(emitScreenAnnotations()); + CHECK_RETURN( emitNoteAnnotations() ); + CHECK_RETURN( emitWidgetAnnotations() ); + + return true; +} + +bool PDFWriterImpl::emitEmbeddedFiles() +{ + for (auto& rEmbeddedFile : m_aEmbeddedFiles) + { + if (!updateObject(rEmbeddedFile.m_nObject)) + continue; + + OStringBuffer aLine; + aLine.append(rEmbeddedFile.m_nObject); + aLine.append(" 0 obj\n"); + aLine.append("<< /Type /EmbeddedFile /Length "); + aLine.append(static_cast<sal_Int64>(rEmbeddedFile.m_pData->size())); + aLine.append(" >>\nstream\n"); + CHECK_RETURN(writeBuffer(aLine.getStr(), aLine.getLength())); + aLine.setLength(0); + + CHECK_RETURN(writeBuffer(rEmbeddedFile.m_pData->data(), rEmbeddedFile.m_pData->size())); + + aLine.append("\nendstream\nendobj\n\n"); + CHECK_RETURN(writeBuffer(aLine.getStr(), aLine.getLength())); + } + return true; +} + +#undef CHECK_RETURN +#define CHECK_RETURN( x ) if( !x ) return false + +bool PDFWriterImpl::emitCatalog() +{ + // build page tree + // currently there is only one node that contains all leaves + + // first create a page tree node id + sal_Int32 nTreeNode = createObject(); + + // emit global resource dictionary (page emit needs it) + CHECK_RETURN( emitResources() ); + + // emit all pages + for (auto & page : m_aPages) + if( ! page.emit( nTreeNode ) ) + return false; + + sal_Int32 nNamedDestinationsDictionary = emitNamedDestinations(); + + sal_Int32 nOutlineDict = emitOutline(); + + // emit Output intent + sal_Int32 nOutputIntentObject = emitOutputIntent(); + + // emit metadata + sal_Int32 nMetadataObject = emitDocumentMetadata(); + + sal_Int32 nStructureDict = 0; + if(m_aStructure.size() > 1) + { + // check if dummy structure containers are needed + addInternalStructureContainer(m_aStructure[0]); + nStructureDict = m_aStructure[0].m_nObject = createObject(); + emitStructure( m_aStructure[ 0 ] ); + } + + // adjust tree node file offset + if( ! updateObject( nTreeNode ) ) + return false; + + // emit tree node + OStringBuffer aLine( 2048 ); + aLine.append( nTreeNode ); + aLine.append( " 0 obj\n" ); + aLine.append( "<</Type/Pages\n" ); + aLine.append( "/Resources " ); + aLine.append( getResourceDictObj() ); + aLine.append( " 0 R\n" ); + + sal_Int32 nMediaBoxWidth = 0; + sal_Int32 nMediaBoxHeight = 0; + sal_Int32 nUserUnit = 1; + if( m_aPages.empty() ) // sanity check, this should not happen + { + nMediaBoxWidth = g_nInheritedPageWidth; + nMediaBoxHeight = g_nInheritedPageHeight; + } + else + { + for (auto const& page : m_aPages) + { + if( page.m_nPageWidth > nMediaBoxWidth ) + { + nMediaBoxWidth = page.m_nPageWidth; + nUserUnit = page.m_nUserUnit; + } + if( page.m_nPageHeight > nMediaBoxHeight ) + { + nMediaBoxHeight = page.m_nPageHeight; + nUserUnit = page.m_nUserUnit; + } + } + } + aLine.append( "/MediaBox[ 0 0 " ); + aLine.append(nMediaBoxWidth / nUserUnit); + aLine.append( ' ' ); + aLine.append(nMediaBoxHeight / nUserUnit); + aLine.append(" ]\n"); + if (nUserUnit > 1) + { + aLine.append("/UserUnit "); + aLine.append(nUserUnit); + aLine.append("\n"); + } + aLine.append("/Kids[ "); + unsigned int i = 0; + for (const auto & page : m_aPages) + { + aLine.append( page.m_nPageObject ); + aLine.append( " 0 R" ); + aLine.append( ( (i&15) == 15 ) ? "\n" : " " ); + ++i; + } + aLine.append( "]\n" + "/Count " ); + aLine.append( static_cast<sal_Int32>(m_aPages.size()) ); + aLine.append( ">>\n" + "endobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + // emit annotation objects + CHECK_RETURN( emitAnnotations() ); + CHECK_RETURN( emitEmbeddedFiles() ); + + // emit Catalog + m_nCatalogObject = createObject(); + if( ! updateObject( m_nCatalogObject ) ) + return false; + aLine.setLength( 0 ); + aLine.append( m_nCatalogObject ); + aLine.append( " 0 obj\n" + "<</Type/Catalog/Pages " ); + aLine.append( nTreeNode ); + aLine.append( " 0 R\n" ); + + // check if there are named destinations to emit (root must be inside the catalog) + if( nNamedDestinationsDictionary ) + { + aLine.append("/Dests "); + aLine.append( nNamedDestinationsDictionary ); + aLine.append( " 0 R\n" ); + } + + if( m_aContext.PageLayout != PDFWriter::DefaultLayout ) + switch( m_aContext.PageLayout ) + { + default : + case PDFWriter::SinglePage : + aLine.append( "/PageLayout/SinglePage\n" ); + break; + case PDFWriter::Continuous : + aLine.append( "/PageLayout/OneColumn\n" ); + break; + case PDFWriter::ContinuousFacing : + // the flag m_aContext.FirstPageLeft below is used to set the page on the left side + aLine.append( "/PageLayout/TwoColumnRight\n" );//odd page on the right side + break; + } + if( m_aContext.PDFDocumentMode != PDFWriter::ModeDefault && !m_aContext.OpenInFullScreenMode ) + switch( m_aContext.PDFDocumentMode ) + { + default : + aLine.append( "/PageMode/UseNone\n" ); + break; + case PDFWriter::UseOutlines : + aLine.append( "/PageMode/UseOutlines\n" ); //document is opened with outline pane open + break; + case PDFWriter::UseThumbs : + aLine.append( "/PageMode/UseThumbs\n" ); //document is opened with thumbnails pane open + break; + } + else if( m_aContext.OpenInFullScreenMode ) + aLine.append( "/PageMode/FullScreen\n" ); //document is opened full screen + + OStringBuffer aInitPageRef; + if( m_aContext.InitialPage >= 0 && m_aContext.InitialPage < static_cast<sal_Int32>(m_aPages.size()) ) + { + aInitPageRef.append( m_aPages[m_aContext.InitialPage].m_nPageObject ); + aInitPageRef.append( " 0 R" ); + } + else + aInitPageRef.append( "0" ); + + switch( m_aContext.PDFDocumentAction ) + { + case PDFWriter::ActionDefault : //do nothing, this is the Acrobat default + default: + if( aInitPageRef.getLength() > 1 ) + { + aLine.append( "/OpenAction[" ); + aLine.append( aInitPageRef.makeStringAndClear() ); + aLine.append( " /XYZ null null 0]\n" ); + } + break; + case PDFWriter::FitInWindow : + aLine.append( "/OpenAction[" ); + aLine.append( aInitPageRef.makeStringAndClear() ); + aLine.append( " /Fit]\n" ); //Open fit page + break; + case PDFWriter::FitWidth : + aLine.append( "/OpenAction[" ); + aLine.append( aInitPageRef.makeStringAndClear() ); + aLine.append( " /FitH " ); + aLine.append( g_nInheritedPageHeight );//Open fit width + aLine.append( "]\n" ); + break; + case PDFWriter::FitVisible : + aLine.append( "/OpenAction[" ); + aLine.append( aInitPageRef.makeStringAndClear() ); + aLine.append( " /FitBH " ); + aLine.append( g_nInheritedPageHeight );//Open fit visible + aLine.append( "]\n" ); + break; + case PDFWriter::ActionZoom : + aLine.append( "/OpenAction[" ); + aLine.append( aInitPageRef.makeStringAndClear() ); + aLine.append( " /XYZ null null " ); + if( m_aContext.Zoom >= 50 && m_aContext.Zoom <= 1600 ) + aLine.append( static_cast<double>(m_aContext.Zoom)/100.0 ); + else + aLine.append( "0" ); + aLine.append( "]\n" ); + break; + } + + // viewer preferences, if we had some, then emit + if( m_aContext.HideViewerToolbar || + ( m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 && !m_aContext.DocumentInfo.Title.isEmpty() && m_aContext.DisplayPDFDocumentTitle ) || + m_aContext.HideViewerMenubar || + m_aContext.HideViewerWindowControls || m_aContext.FitWindow || + m_aContext.CenterWindow || (m_aContext.FirstPageLeft && m_aContext.PageLayout == PDFWriter::ContinuousFacing ) || + m_aContext.OpenInFullScreenMode ) + { + aLine.append( "/ViewerPreferences<<" ); + if( m_aContext.HideViewerToolbar ) + aLine.append( "/HideToolbar true\n" ); + if( m_aContext.HideViewerMenubar ) + aLine.append( "/HideMenubar true\n" ); + if( m_aContext.HideViewerWindowControls ) + aLine.append( "/HideWindowUI true\n" ); + if( m_aContext.FitWindow ) + aLine.append( "/FitWindow true\n" ); + if( m_aContext.CenterWindow ) + aLine.append( "/CenterWindow true\n" ); + if( m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 && !m_aContext.DocumentInfo.Title.isEmpty() && m_aContext.DisplayPDFDocumentTitle ) + aLine.append( "/DisplayDocTitle true\n" ); + if( m_aContext.FirstPageLeft && m_aContext.PageLayout == PDFWriter::ContinuousFacing ) + aLine.append( "/Direction/R2L\n" ); + if( m_aContext.OpenInFullScreenMode ) + switch( m_aContext.PDFDocumentMode ) + { + default : + case PDFWriter::ModeDefault : + aLine.append( "/NonFullScreenPageMode/UseNone\n" ); + break; + case PDFWriter::UseOutlines : + aLine.append( "/NonFullScreenPageMode/UseOutlines\n" ); + break; + case PDFWriter::UseThumbs : + aLine.append( "/NonFullScreenPageMode/UseThumbs\n" ); + break; + } + aLine.append( ">>\n" ); + } + + if( nOutlineDict ) + { + aLine.append( "/Outlines " ); + aLine.append( nOutlineDict ); + aLine.append( " 0 R\n" ); + } + if( nStructureDict ) + { + aLine.append( "/StructTreeRoot " ); + aLine.append( nStructureDict ); + aLine.append( " 0 R\n" ); + } + if( !m_aContext.DocumentLocale.Language.isEmpty() ) + { + /* PDF allows only RFC 3066, see above in emitStructure(). */ + LanguageTag aLanguageTag( m_aContext.DocumentLocale); + OUString aLanguage, aScript, aCountry; + aLanguageTag.getIsoLanguageScriptCountry( aLanguage, aScript, aCountry); + if (!aLanguage.isEmpty()) + { + OUStringBuffer aLocBuf( 16 ); + aLocBuf.append( aLanguage ); + if( !aCountry.isEmpty() ) + { + aLocBuf.append( '-' ); + aLocBuf.append( aCountry ); + } + aLine.append( "/Lang" ); + appendLiteralStringEncrypt( aLocBuf.makeStringAndClear(), m_nCatalogObject, aLine ); + aLine.append( "\n" ); + } + } + if( m_aContext.Tagged && m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 ) + { + aLine.append( "/MarkInfo<</Marked true>>\n" ); + } + if( !m_aWidgets.empty() ) + { + aLine.append( "/AcroForm<</Fields[\n" ); + int nWidgets = m_aWidgets.size(); + int nOut = 0; + for( int j = 0; j < nWidgets; j++ ) + { + // output only root fields + if( m_aWidgets[j].m_nParent < 1 ) + { + aLine.append( m_aWidgets[j].m_nObject ); + aLine.append( (nOut++ % 5)==4 ? " 0 R\n" : " 0 R " ); + } + } + aLine.append( "\n]" ); + +#if HAVE_FEATURE_NSS + if (m_nSignatureObject != -1) + aLine.append( "/SigFlags 3"); +#endif + + aLine.append( "/DR " ); + aLine.append( getResourceDictObj() ); + aLine.append( " 0 R" ); + // NeedAppearances must not be used if PDF is signed + if( m_bIsPDF_A1 || m_bIsPDF_A2 || m_bIsPDF_A3 +#if HAVE_FEATURE_NSS + || ( m_nSignatureObject != -1 ) +#endif + ) + aLine.append( ">>\n" ); + else + aLine.append( "/NeedAppearances true>>\n" ); + } + + //check if there is a Metadata object + if( nOutputIntentObject ) + { + aLine.append("/OutputIntents["); + aLine.append( nOutputIntentObject ); + aLine.append( " 0 R]" ); + } + + if( nMetadataObject ) + { + aLine.append("/Metadata "); + aLine.append( nMetadataObject ); + aLine.append( " 0 R" ); + } + + aLine.append( ">>\n" + "endobj\n\n" ); + return writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +#if HAVE_FEATURE_NSS + +bool PDFWriterImpl::emitSignature() +{ + if( !updateObject( m_nSignatureObject ) ) + return false; + + OStringBuffer aLine( 0x5000 ); + aLine.append( m_nSignatureObject ); + aLine.append( " 0 obj\n" ); + aLine.append("<</Contents <" ); + + sal_uInt64 nOffset = ~0U; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nOffset) ) ); + + m_nSignatureContentOffset = nOffset + aLine.getLength(); + + // reserve some space for the PKCS#7 object + OStringBuffer aContentFiller( MAX_SIGNATURE_CONTENT_LENGTH ); + comphelper::string::padToLength(aContentFiller, MAX_SIGNATURE_CONTENT_LENGTH, '0'); + aLine.append( aContentFiller.makeStringAndClear() ); + aLine.append( ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached"); + + if( !m_aContext.DocumentInfo.Author.isEmpty() ) + { + aLine.append( "/Name" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Author, m_nSignatureObject, aLine ); + } + + aLine.append( " /M "); + appendLiteralStringEncrypt( m_aCreationDateString, m_nSignatureObject, aLine ); + + aLine.append( " /ByteRange [ 0 "); + aLine.append( m_nSignatureContentOffset - 1 ); + aLine.append( " " ); + aLine.append( m_nSignatureContentOffset + MAX_SIGNATURE_CONTENT_LENGTH + 1 ); + aLine.append( " " ); + + m_nSignatureLastByteRangeNoOffset = nOffset + aLine.getLength(); + + // mark the last ByteRange no and add some space. Now, we don't know + // how many bytes we need for this ByteRange value + // The real value will be overwritten in the finalizeSignature method + OStringBuffer aByteRangeFiller( 100 ); + comphelper::string::padToLength(aByteRangeFiller, 100, ' '); + aLine.append( aByteRangeFiller.makeStringAndClear() ); + aLine.append(" /Filter/Adobe.PPKMS"); + + //emit reason, location and contactinfo + if ( !m_aContext.SignReason.isEmpty() ) + { + aLine.append("/Reason"); + appendUnicodeTextStringEncrypt( m_aContext.SignReason, m_nSignatureObject, aLine ); + } + + if ( !m_aContext.SignLocation.isEmpty() ) + { + aLine.append("/Location"); + appendUnicodeTextStringEncrypt( m_aContext.SignLocation, m_nSignatureObject, aLine ); + } + + if ( !m_aContext.SignContact.isEmpty() ) + { + aLine.append("/ContactInfo"); + appendUnicodeTextStringEncrypt( m_aContext.SignContact, m_nSignatureObject, aLine ); + } + + aLine.append(" >>\nendobj\n\n" ); + + return writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +bool PDFWriterImpl::finalizeSignature() +{ + if (!m_aContext.SignCertificate.is()) + return false; + + // 1- calculate last ByteRange value + sal_uInt64 nOffset = ~0U; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nOffset) ) ); + + sal_Int64 nLastByteRangeNo = nOffset - (m_nSignatureContentOffset + MAX_SIGNATURE_CONTENT_LENGTH + 1); + + // 2- overwrite the value to the m_nSignatureLastByteRangeNoOffset position + sal_uInt64 nWritten = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.setPos(osl_Pos_Absolut, m_nSignatureLastByteRangeNoOffset) ) ); + OString aByteRangeNo = OString::number( nLastByteRangeNo ) + " ]"; + + if (m_aFile.write(aByteRangeNo.getStr(), aByteRangeNo.getLength(), nWritten) != osl::File::E_None) + { + CHECK_RETURN( (osl::File::E_None == m_aFile.setPos(osl_Pos_Absolut, nOffset)) ); + return false; + } + + // 3- create the PKCS#7 object using NSS + + // Prepare buffer and calculate PDF file digest + CHECK_RETURN( (osl::File::E_None == m_aFile.setPos(osl_Pos_Absolut, 0)) ); + + std::unique_ptr<char[]> buffer1(new char[m_nSignatureContentOffset + 1]); + sal_uInt64 bytesRead1; + + //FIXME: Check if hash is calculated from the correct byterange + if (osl::File::E_None != m_aFile.read(buffer1.get(), m_nSignatureContentOffset - 1 , bytesRead1) || + bytesRead1 != static_cast<sal_uInt64>(m_nSignatureContentOffset) - 1) + { + SAL_WARN("vcl.pdfwriter", "First buffer read failed"); + return false; + } + + std::unique_ptr<char[]> buffer2(new char[nLastByteRangeNo + 1]); + sal_uInt64 bytesRead2; + + if (osl::File::E_None != m_aFile.setPos(osl_Pos_Absolut, m_nSignatureContentOffset + MAX_SIGNATURE_CONTENT_LENGTH + 1) || + osl::File::E_None != m_aFile.read(buffer2.get(), nLastByteRangeNo, bytesRead2) || + bytesRead2 != static_cast<sal_uInt64>(nLastByteRangeNo)) + { + SAL_WARN("vcl.pdfwriter", "Second buffer read failed"); + return false; + } + + OStringBuffer aCMSHexBuffer; + svl::crypto::Signing aSigning(m_aContext.SignCertificate); + aSigning.AddDataRange(buffer1.get(), bytesRead1); + aSigning.AddDataRange(buffer2.get(), bytesRead2); + aSigning.SetSignTSA(m_aContext.SignTSA); + aSigning.SetSignPassword(m_aContext.SignPassword); + if (!aSigning.Sign(aCMSHexBuffer)) + { + SAL_WARN("vcl.pdfwriter", "PDFWriter::Sign() failed"); + return false; + } + + assert(aCMSHexBuffer.getLength() <= MAX_SIGNATURE_CONTENT_LENGTH); + + // Set file pointer to the m_nSignatureContentOffset, we're ready to overwrite PKCS7 object + nWritten = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.setPos(osl_Pos_Absolut, m_nSignatureContentOffset)) ); + m_aFile.write(aCMSHexBuffer.getStr(), aCMSHexBuffer.getLength(), nWritten); + + return osl::File::E_None == m_aFile.setPos(osl_Pos_Absolut, nOffset); +} + +#endif //HAVE_FEATURE_NSS + +sal_Int32 PDFWriterImpl::emitInfoDict( ) +{ + sal_Int32 nObject = createObject(); + + if( updateObject( nObject ) ) + { + OStringBuffer aLine( 1024 ); + aLine.append( nObject ); + aLine.append( " 0 obj\n" + "<<" ); + if( !m_aContext.DocumentInfo.Title.isEmpty() ) + { + aLine.append( "/Title" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Title, nObject, aLine ); + aLine.append( "\n" ); + } + if( !m_aContext.DocumentInfo.Author.isEmpty() ) + { + aLine.append( "/Author" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Author, nObject, aLine ); + aLine.append( "\n" ); + } + if( !m_aContext.DocumentInfo.Subject.isEmpty() ) + { + aLine.append( "/Subject" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Subject, nObject, aLine ); + aLine.append( "\n" ); + } + if( !m_aContext.DocumentInfo.Keywords.isEmpty() ) + { + aLine.append( "/Keywords" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Keywords, nObject, aLine ); + aLine.append( "\n" ); + } + if( !m_aContext.DocumentInfo.Creator.isEmpty() ) + { + aLine.append( "/Creator" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Creator, nObject, aLine ); + aLine.append( "\n" ); + } + if( !m_aContext.DocumentInfo.Producer.isEmpty() ) + { + aLine.append( "/Producer" ); + appendUnicodeTextStringEncrypt( m_aContext.DocumentInfo.Producer, nObject, aLine ); + aLine.append( "\n" ); + } + + aLine.append( "/CreationDate" ); + appendLiteralStringEncrypt( m_aCreationDateString, nObject, aLine ); + aLine.append( ">>\nendobj\n\n" ); + if( ! writeBuffer( aLine.getStr(), aLine.getLength() ) ) + nObject = 0; + } + else + nObject = 0; + + return nObject; +} + +// Part of this function may be shared with method appendDest. +sal_Int32 PDFWriterImpl::emitNamedDestinations() +{ + sal_Int32 nCount = m_aNamedDests.size(); + if( nCount <= 0 ) + return 0;//define internal error + + //get the object number for all the destinations + sal_Int32 nObject = createObject(); + + if( updateObject( nObject ) ) + { + //emit the dictionary + OStringBuffer aLine( 1024 ); + aLine.append( nObject ); + aLine.append( " 0 obj\n" + "<<" ); + + sal_Int32 nDestID; + for( nDestID = 0; nDestID < nCount; nDestID++ ) + { + const PDFNamedDest& rDest = m_aNamedDests[ nDestID ]; + // In order to correctly function both under an Internet browser and + // directly with a reader (provided the reader has the feature) we + // need to set the name of the destination the same way it will be encoded + // in an Internet link + INetURLObject aLocalURL( "http://ahost.ax" ); //dummy location, won't be used + aLocalURL.SetMark( rDest.m_aDestName ); + + const OUString aName = aLocalURL.GetMark( INetURLObject::DecodeMechanism::NONE ); //same coding as + // in link creation ( see PDFWriterImpl::emitLinkAnnotations ) + const PDFPage& rDestPage = m_aPages[ rDest.m_nPage ]; + + aLine.append( '/' ); + appendDestinationName( aName, aLine ); // this conversion must be done when forming the link to target ( see in emitCatalog ) + aLine.append( '[' ); // the '[' can be emitted immediately, because the appendDestinationName function + //maps the preceding character properly + aLine.append( rDestPage.m_nPageObject ); + aLine.append( " 0 R" ); + + switch( rDest.m_eType ) + { + case PDFWriter::DestAreaType::XYZ: + default: + aLine.append( "/XYZ " ); + appendFixedInt( rDest.m_aRect.Left(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rDest.m_aRect.Bottom(), aLine ); + aLine.append( " 0" ); + break; + case PDFWriter::DestAreaType::FitRectangle: + aLine.append( "/FitR " ); + appendFixedInt( rDest.m_aRect.Left(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rDest.m_aRect.Top(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rDest.m_aRect.Right(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rDest.m_aRect.Bottom(), aLine ); + break; + } + aLine.append( "]\n" ); + } + + //close + aLine.append( ">>\nendobj\n\n" ); + if( ! writeBuffer( aLine.getStr(), aLine.getLength() ) ) + nObject = 0; + } + else + nObject = 0; + + return nObject; +} + +// emits the output intent dictionary +sal_Int32 PDFWriterImpl::emitOutputIntent() +{ + if( !m_bIsPDF_A1 && !m_bIsPDF_A2 && !m_bIsPDF_A3 ) + return 0; + + //emit the sRGB standard profile, in ICC format, in a stream, per IEC61966-2.1 + + OStringBuffer aLine( 1024 ); + sal_Int32 nICCObject = createObject(); + sal_Int32 nStreamLengthObject = createObject(); + + aLine.append( nICCObject ); +// sRGB has 3 colors, hence /N 3 below (PDF 1.4 table 4.16) + aLine.append( " 0 obj\n<</N 3/Length " ); + aLine.append( nStreamLengthObject ); + aLine.append( " 0 R" ); + if (!g_bDebugDisableCompression) + aLine.append( "/Filter/FlateDecode" ); + aLine.append( ">>\nstream\n" ); + if ( !updateObject( nICCObject ) ) return 0; + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return 0; + //get file position + sal_uInt64 nBeginStreamPos = 0; + if (osl::File::E_None != m_aFile.getPos(nBeginStreamPos)) + return 0; + beginCompression(); + checkAndEnableStreamEncryption( nICCObject ); + cmsHPROFILE hProfile = cmsCreate_sRGBProfile(); + //force ICC profile version 2.1 + cmsSetProfileVersion(hProfile, 2.1); + cmsUInt32Number nBytesNeeded = 0; + cmsSaveProfileToMem(hProfile, nullptr, &nBytesNeeded); + if (!nBytesNeeded) + return 0; + std::vector<unsigned char> aBuffer(nBytesNeeded); + cmsSaveProfileToMem(hProfile, aBuffer.data(), &nBytesNeeded); + cmsCloseProfile(hProfile); + bool written = writeBuffer( aBuffer.data(), static_cast<sal_Int32>(aBuffer.size()) ); + disableStreamEncryption(); + endCompression(); + + sal_uInt64 nEndStreamPos = 0; + if (m_aFile.getPos(nEndStreamPos) != osl::File::E_None) + return 0; + + if( !written ) + return 0; + if( ! writeBuffer( "\nendstream\nendobj\n\n", 19 ) ) + return 0 ; + aLine.setLength( 0 ); + + //emit the stream length object + if ( !updateObject( nStreamLengthObject ) ) return 0; + aLine.setLength( 0 ); + aLine.append( nStreamLengthObject ); + aLine.append( " 0 obj\n" ); + aLine.append( static_cast<sal_Int64>(nEndStreamPos-nBeginStreamPos) ); + aLine.append( "\nendobj\n\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return 0; + aLine.setLength( 0 ); + + //emit the OutputIntent dictionary + sal_Int32 nOIObject = createObject(); + if ( !updateObject( nOIObject ) ) return 0; + aLine.append( nOIObject ); + aLine.append( " 0 obj\n" + "<</Type/OutputIntent/S/GTS_PDFA1/OutputConditionIdentifier"); + + OUString const aComment( "sRGB IEC61966-2.1" ); + appendLiteralStringEncrypt( aComment ,nOIObject, aLine ); + aLine.append("/DestOutputProfile "); + aLine.append( nICCObject ); + aLine.append( " 0 R>>\nendobj\n\n" ); + if ( !writeBuffer( aLine.getStr(), aLine.getLength() ) ) return 0; + + return nOIObject; +} + +// formats the string for the XML stream +static void escapeStringXML( const OUString& rStr, OUString &rValue) +{ + const sal_Unicode* pUni = rStr.getStr(); + int nLen = rStr.getLength(); + for( ; nLen; nLen--, pUni++ ) + { + switch( *pUni ) + { + case u'&': + rValue += "&"; + break; + case u'<': + rValue += "<"; + break; + case u'>': + rValue += ">"; + break; + case u'\'': + rValue += "'"; + break; + case u'"': + rValue += """; + break; + default: + rValue += OUStringChar( *pUni ); + break; + } + } +} + +static void lcl_assignMeta(const OUString& aValue, OString& aMeta) +{ + if (!aValue.isEmpty()) + { + OUString aTempString; + escapeStringXML(aValue, aTempString); + aMeta = OUStringToOString(aTempString, RTL_TEXTENCODING_UTF8); + } +} + +// emits the document metadata +sal_Int32 PDFWriterImpl::emitDocumentMetadata() +{ + if( !m_bIsPDF_A1 && !m_bIsPDF_A2 && !m_bIsPDF_A3 ) + return 0; + + //get the object number for all the destinations + sal_Int32 nObject = createObject(); + + if( updateObject( nObject ) ) + { + pdf::XmpMetadata aMetadata; + + if (m_bIsPDF_A1) + aMetadata.mnPDF_A = 1; + else if (m_bIsPDF_A2) + aMetadata.mnPDF_A = 2; + else if (m_bIsPDF_A3) + aMetadata.mnPDF_A = 3; + + aMetadata.mbPDF_UA = m_bIsPDF_UA; + + lcl_assignMeta(m_aContext.DocumentInfo.Title, aMetadata.msTitle); + lcl_assignMeta(m_aContext.DocumentInfo.Author, aMetadata.msAuthor); + lcl_assignMeta(m_aContext.DocumentInfo.Subject, aMetadata.msSubject); + lcl_assignMeta(m_aContext.DocumentInfo.Producer, aMetadata.msProducer); + lcl_assignMeta(m_aContext.DocumentInfo.Keywords, aMetadata.msKeywords); + lcl_assignMeta(m_aContext.DocumentInfo.Creator, aMetadata.m_sCreatorTool); + aMetadata.m_sCreateDate = m_aCreationMetaDateString; + + OStringBuffer aMetadataObj( 1024 ); + + aMetadataObj.append( nObject ); + aMetadataObj.append( " 0 obj\n" ); + + aMetadataObj.append( "<</Type/Metadata/Subtype/XML/Length " ); + + aMetadataObj.append( sal_Int32(aMetadata.getSize()) ); + aMetadataObj.append( ">>\nstream\n" ); + if ( !writeBuffer( aMetadataObj.getStr(), aMetadataObj.getLength() ) ) + return 0; + //emit the stream + if ( !writeBuffer( aMetadata.getData(), aMetadata.getSize() ) ) + return 0; + + aMetadataObj.setLength( 0 ); + aMetadataObj.append( "\nendstream\nendobj\n\n" ); + if( ! writeBuffer( aMetadataObj.getStr(), aMetadataObj.getLength() ) ) + nObject = 0; + } + else + nObject = 0; + + return nObject; +} + +bool PDFWriterImpl::emitTrailer() +{ + // emit doc info + sal_Int32 nDocInfoObject = emitInfoDict( ); + + sal_Int32 nSecObject = 0; + + if( m_aContext.Encryption.Encrypt() ) + { + //emit the security information + //must be emitted as indirect dictionary object, since + //Acrobat Reader 5 works only with this kind of implementation + nSecObject = createObject(); + + if( updateObject( nSecObject ) ) + { + OStringBuffer aLineS( 1024 ); + aLineS.append( nSecObject ); + aLineS.append( " 0 obj\n" + "<</Filter/Standard/V " ); + // check the version + aLineS.append( "2/Length 128/R 3" ); + + // emit the owner password, must not be encrypted + aLineS.append( "/O(" ); + appendLiteralString( reinterpret_cast<char*>(m_aContext.Encryption.OValue.data()), sal_Int32(m_aContext.Encryption.OValue.size()), aLineS ); + aLineS.append( ")/U(" ); + appendLiteralString( reinterpret_cast<char*>(m_aContext.Encryption.UValue.data()), sal_Int32(m_aContext.Encryption.UValue.size()), aLineS ); + aLineS.append( ")/P " );// the permission set + aLineS.append( m_nAccessPermissions ); + aLineS.append( ">>\nendobj\n\n" ); + if( !writeBuffer( aLineS.getStr(), aLineS.getLength() ) ) + nSecObject = 0; + } + else + nSecObject = 0; + } + // emit xref table + // remember start + sal_uInt64 nXRefOffset = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nXRefOffset )) ); + CHECK_RETURN( writeBuffer( "xref\n", 5 ) ); + + sal_Int32 nObjects = m_aObjects.size(); + OStringBuffer aLine; + aLine.append( "0 " ); + aLine.append( static_cast<sal_Int32>(nObjects+1) ); + aLine.append( "\n" ); + aLine.append( "0000000000 65535 f \n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + for( sal_Int32 i = 0; i < nObjects; i++ ) + { + aLine.setLength( 0 ); + OString aOffset = OString::number( m_aObjects[i] ); + for( sal_Int32 j = 0; j < (10-aOffset.getLength()); j++ ) + aLine.append( '0' ); + aLine.append( aOffset ); + aLine.append( " 00000 n \n" ); + SAL_WARN_IF( aLine.getLength() != 20, "vcl.pdfwriter", "invalid xref entry" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + } + + // prepare document checksum + OStringBuffer aDocChecksum( 2*RTL_DIGEST_LENGTH_MD5+1 ); + ::std::vector<unsigned char> const nMD5Sum(m_DocDigest.finalize()); + for (sal_uInt8 i : nMD5Sum) + appendHex( i, aDocChecksum ); + // document id set in setDocInfo method + // emit trailer + aLine.setLength( 0 ); + aLine.append( "trailer\n" + "<</Size " ); + aLine.append( static_cast<sal_Int32>(nObjects+1) ); + aLine.append( "/Root " ); + aLine.append( m_nCatalogObject ); + aLine.append( " 0 R\n" ); + if( nSecObject ) + { + aLine.append( "/Encrypt "); + aLine.append( nSecObject ); + aLine.append( " 0 R\n" ); + } + if( nDocInfoObject ) + { + aLine.append( "/Info " ); + aLine.append( nDocInfoObject ); + aLine.append( " 0 R\n" ); + } + if( ! m_aContext.Encryption.DocumentIdentifier.empty() ) + { + aLine.append( "/ID [ <" ); + for (auto const& item : m_aContext.Encryption.DocumentIdentifier) + { + appendHex( sal_Int8(item), aLine ); + } + aLine.append( ">\n" + "<" ); + for (auto const& item : m_aContext.Encryption.DocumentIdentifier) + { + appendHex( sal_Int8(item), aLine ); + } + aLine.append( "> ]\n" ); + } + if( !aDocChecksum.isEmpty() ) + { + aLine.append( "/DocChecksum /" ); + aLine.append( aDocChecksum.makeStringAndClear() ); + aLine.append( "\n" ); + } + if( !m_aAdditionalStreams.empty() ) + { + aLine.append( "/AdditionalStreams [" ); + for(const PDFAddStream & rAdditionalStream : m_aAdditionalStreams) + { + aLine.append( "/" ); + appendName( rAdditionalStream.m_aMimeType, aLine ); + aLine.append( " " ); + aLine.append( rAdditionalStream.m_nStreamObject ); + aLine.append( " 0 R\n" ); + } + aLine.append( "]\n" ); + } + aLine.append( ">>\n" + "startxref\n" ); + aLine.append( static_cast<sal_Int64>(nXRefOffset) ); + aLine.append( "\n" + "%%EOF\n" ); + return writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +namespace { + +struct AnnotationSortEntry +{ + sal_Int32 nTabOrder; + sal_Int32 nObject; + sal_Int32 nWidgetIndex; + + AnnotationSortEntry( sal_Int32 nTab, sal_Int32 nObj, sal_Int32 nI ) : + nTabOrder( nTab ), + nObject( nObj ), + nWidgetIndex( nI ) + {} +}; + +struct AnnotSortContainer +{ + std::set< sal_Int32 > aObjects; + std::vector< AnnotationSortEntry > aSortedAnnots; +}; + +struct AnnotSorterLess +{ + std::vector<PDFWidget>& m_rWidgets; + + explicit AnnotSorterLess( std::vector<PDFWidget>& rWidgets ) : m_rWidgets( rWidgets ) {} + + bool operator()( const AnnotationSortEntry& rLeft, const AnnotationSortEntry& rRight ) + { + if( rLeft.nTabOrder < rRight.nTabOrder ) + return true; + if( rRight.nTabOrder < rLeft.nTabOrder ) + return false; + if( rLeft.nWidgetIndex < 0 && rRight.nWidgetIndex < 0 ) + return false; + if( rRight.nWidgetIndex < 0 ) + return true; + if( rLeft.nWidgetIndex < 0 ) + return false; + // remember: widget rects are in PDF coordinates, so they are ordered down up + if( m_rWidgets[ rLeft.nWidgetIndex ].m_aRect.Top() > + m_rWidgets[ rRight.nWidgetIndex ].m_aRect.Top() ) + return true; + if( m_rWidgets[ rRight.nWidgetIndex ].m_aRect.Top() > + m_rWidgets[ rLeft.nWidgetIndex ].m_aRect.Top() ) + return false; + if( m_rWidgets[ rLeft.nWidgetIndex ].m_aRect.Left() < + m_rWidgets[ rRight.nWidgetIndex ].m_aRect.Left() ) + return true; + return false; + } +}; + +} + +void PDFWriterImpl::sortWidgets() +{ + // sort widget annotations on each page as per their + // TabOrder attribute + std::unordered_map< sal_Int32, AnnotSortContainer > sorted; + int nWidgets = m_aWidgets.size(); + for( int nW = 0; nW < nWidgets; nW++ ) + { + const PDFWidget& rWidget = m_aWidgets[nW]; + if( rWidget.m_nPage >= 0 ) + { + AnnotSortContainer& rCont = sorted[ rWidget.m_nPage ]; + // optimize vector allocation + if( rCont.aSortedAnnots.empty() ) + rCont.aSortedAnnots.reserve( m_aPages[ rWidget.m_nPage ].m_aAnnotations.size() ); + // insert widget to tab sorter + // RadioButtons are not page annotations, only their individual check boxes are + if( rWidget.m_eType != PDFWriter::RadioButton ) + { + rCont.aObjects.insert( rWidget.m_nObject ); + rCont.aSortedAnnots.emplace_back( rWidget.m_nTabOrder, rWidget.m_nObject, nW ); + } + } + } + for (auto & item : sorted) + { + // append entries for non widget annotations + PDFPage& rPage = m_aPages[ item.first ]; + unsigned int nAnnots = rPage.m_aAnnotations.size(); + for( unsigned int nA = 0; nA < nAnnots; nA++ ) + if( item.second.aObjects.find( rPage.m_aAnnotations[nA] ) == item.second.aObjects.end()) + item.second.aSortedAnnots.emplace_back( 10000, rPage.m_aAnnotations[nA], -1 ); + + AnnotSorterLess aLess( m_aWidgets ); + std::stable_sort( item.second.aSortedAnnots.begin(), item.second.aSortedAnnots.end(), aLess ); + // sanity check + if( item.second.aSortedAnnots.size() == nAnnots) + { + for( unsigned int nA = 0; nA < nAnnots; nA++ ) + rPage.m_aAnnotations[nA] = item.second.aSortedAnnots[nA].nObject; + } + else + { + SAL_WARN( "vcl.pdfwriter", "wrong number of sorted annotations" ); + SAL_INFO("vcl.pdfwriter", "PDFWriterImpl::sortWidgets(): wrong number of sorted assertions " + "on page nr " << item.first << ", " << + static_cast<long int>(item.second.aSortedAnnots.size()) << " sorted and " << + static_cast<long int>(nAnnots) << " unsorted"); + } + } + + // FIXME: implement tab order in structure tree for PDF 1.5 +} + +namespace vcl { +class PDFStreamIf : + public cppu::WeakImplHelper< css::io::XOutputStream > +{ + VclPtr<PDFWriterImpl> m_pWriter; + bool m_bWrite; + public: + explicit PDFStreamIf( PDFWriterImpl* pWriter ) : m_pWriter( pWriter ), m_bWrite( true ) {} + + virtual void SAL_CALL writeBytes( const css::uno::Sequence< sal_Int8 >& aData ) override; + virtual void SAL_CALL flush() override; + virtual void SAL_CALL closeOutput() override; +}; +} + +void SAL_CALL PDFStreamIf::writeBytes( const css::uno::Sequence< sal_Int8 >& aData ) +{ + if( m_bWrite && aData.hasElements() ) + { + sal_Int32 nBytes = aData.getLength(); + m_pWriter->writeBuffer( aData.getConstArray(), nBytes ); + } +} + +void SAL_CALL PDFStreamIf::flush() +{ +} + +void SAL_CALL PDFStreamIf::closeOutput() +{ + m_bWrite = false; +} + +bool PDFWriterImpl::emitAdditionalStreams() +{ + unsigned int nStreams = m_aAdditionalStreams.size(); + for( unsigned int i = 0; i < nStreams; i++ ) + { + PDFAddStream& rStream = m_aAdditionalStreams[i]; + rStream.m_nStreamObject = createObject(); + sal_Int32 nSizeObject = createObject(); + + if( ! updateObject( rStream.m_nStreamObject ) ) + return false; + + OStringBuffer aLine; + aLine.append( rStream.m_nStreamObject ); + aLine.append( " 0 obj\n<</Length " ); + aLine.append( nSizeObject ); + aLine.append( " 0 R" ); + if( rStream.m_bCompress ) + aLine.append( "/Filter/FlateDecode" ); + aLine.append( ">>\nstream\n" ); + if( ! writeBuffer( aLine.getStr(), aLine.getLength() ) ) + return false; + sal_uInt64 nBeginStreamPos = 0, nEndStreamPos = 0; + if( osl::File::E_None != m_aFile.getPos(nBeginStreamPos) ) + { + m_aFile.close(); + m_bOpen = false; + } + if( rStream.m_bCompress ) + beginCompression(); + + checkAndEnableStreamEncryption( rStream.m_nStreamObject ); + css::uno::Reference< css::io::XOutputStream > xStream( new PDFStreamIf( this ) ); + assert(rStream.m_pStream); + if (!rStream.m_pStream) + return false; + rStream.m_pStream->write( xStream ); + xStream.clear(); + delete rStream.m_pStream; + rStream.m_pStream = nullptr; + disableStreamEncryption(); + + if( rStream.m_bCompress ) + endCompression(); + + if (osl::File::E_None != m_aFile.getPos(nEndStreamPos)) + { + m_aFile.close(); + m_bOpen = false; + return false; + } + if( ! writeBuffer( "\nendstream\nendobj\n\n", 19 ) ) + return false ; + // emit stream length object + if( ! updateObject( nSizeObject ) ) + return false; + aLine.setLength( 0 ); + aLine.append( nSizeObject ); + aLine.append( " 0 obj\n" ); + aLine.append( static_cast<sal_Int64>(nEndStreamPos-nBeginStreamPos) ); + aLine.append( "\nendobj\n\n" ); + if( ! writeBuffer( aLine.getStr(), aLine.getLength() ) ) + return false; + } + return true; +} + +bool PDFWriterImpl::emit() +{ + endPage(); + + // resort structure tree and annotations if necessary + // needed for widget tab order + sortWidgets(); + +#if HAVE_FEATURE_NSS + if( m_aContext.SignPDF ) + { + // sign the document + PDFWriter::SignatureWidget aSignature; + aSignature.Name = "Signature1"; + createControl( aSignature, 0 ); + } +#endif + + // emit additional streams + CHECK_RETURN( emitAdditionalStreams() ); + + // emit catalog + CHECK_RETURN( emitCatalog() ); + +#if HAVE_FEATURE_NSS + if (m_nSignatureObject != -1) // if document is signed, emit sigdict + { + if( !emitSignature() ) + { + m_aErrors.insert( PDFWriter::Error_Signature_Failed ); + return false; + } + } +#endif + + // emit trailer + CHECK_RETURN( emitTrailer() ); + +#if HAVE_FEATURE_NSS + if (m_nSignatureObject != -1) // finalize the signature + { + if( !finalizeSignature() ) + { + m_aErrors.insert( PDFWriter::Error_Signature_Failed ); + return false; + } + } +#endif + + m_aFile.close(); + m_bOpen = false; + + return true; +} + + +sal_Int32 PDFWriterImpl::getSystemFont( const vcl::Font& i_rFont ) +{ + Push(); + + SetFont( i_rFont ); + + const PhysicalFontFace* pDevFont = GetFontInstance()->GetFontFace(); + sal_Int32 nFontID = 0; + auto it = m_aSystemFonts.find( pDevFont ); + if( it != m_aSystemFonts.end() ) + nFontID = it->second.m_nNormalFontID; + else + { + nFontID = m_nNextFID++; + m_aSystemFonts[ pDevFont ] = EmbedFont(); + m_aSystemFonts[ pDevFont ].m_nNormalFontID = nFontID; + } + + Pop(); + return nFontID; +} + +void PDFWriterImpl::registerGlyph(const GlyphItem* pGlyph, + const PhysicalFontFace* pFont, + const std::vector<sal_Ucs>& rCodeUnits, + sal_uInt8& nMappedGlyph, + sal_Int32& nMappedFontObject) +{ + const int nFontGlyphId = pGlyph->glyphId(); + FontSubset& rSubset = m_aSubsets[ pFont ]; + // search for font specific glyphID + auto it = rSubset.m_aMapping.find( nFontGlyphId ); + if( it != rSubset.m_aMapping.end() ) + { + nMappedFontObject = it->second.m_nFontID; + nMappedGlyph = it->second.m_nSubsetGlyphID; + } + else + { + // create new subset if necessary + if( rSubset.m_aSubsets.empty() + || (rSubset.m_aSubsets.back().m_aMapping.size() > 254) ) + { + rSubset.m_aSubsets.emplace_back( m_nNextFID++ ); + } + + // copy font id + nMappedFontObject = rSubset.m_aSubsets.back().m_nFontID; + // create new glyph in subset + sal_uInt8 nNewId = sal::static_int_cast<sal_uInt8>(rSubset.m_aSubsets.back().m_aMapping.size()+1); + nMappedGlyph = nNewId; + + // add new glyph to emitted font subset + GlyphEmit& rNewGlyphEmit = rSubset.m_aSubsets.back().m_aMapping[ nFontGlyphId ]; + rNewGlyphEmit.setGlyphId( nNewId ); + for (const auto nCode : rCodeUnits) + rNewGlyphEmit.addCode(nCode); + + // add new glyph to font mapping + Glyph& rNewGlyph = rSubset.m_aMapping[ nFontGlyphId ]; + rNewGlyph.m_nFontID = nMappedFontObject; + rNewGlyph.m_nSubsetGlyphID = nNewId; + } +} + +void PDFWriterImpl::drawRelief( SalLayout& rLayout, const OUString& rText, bool bTextLines ) +{ + push( PushFlags::ALL ); + + FontRelief eRelief = m_aCurrentPDFState.m_aFont.GetRelief(); + + Color aTextColor = m_aCurrentPDFState.m_aFont.GetColor(); + Color aTextLineColor = m_aCurrentPDFState.m_aTextLineColor; + Color aOverlineColor = m_aCurrentPDFState.m_aOverlineColor; + Color aReliefColor( COL_LIGHTGRAY ); + if( aTextColor == COL_BLACK ) + aTextColor = COL_WHITE; + if( aTextLineColor == COL_BLACK ) + aTextLineColor = COL_WHITE; + if( aOverlineColor == COL_BLACK ) + aOverlineColor = COL_WHITE; + // coverity[copy_paste_error : FALSE] - aReliefColor depending on aTextColor is correct + if( aTextColor == COL_WHITE ) + aReliefColor = COL_BLACK; + + Font aSetFont = m_aCurrentPDFState.m_aFont; + aSetFont.SetRelief( FontRelief::NONE ); + aSetFont.SetShadow( false ); + + aSetFont.SetColor( aReliefColor ); + setTextLineColor( aReliefColor ); + setOverlineColor( aReliefColor ); + setFont( aSetFont ); + long nOff = 1 + GetDPIX()/300; + if( eRelief == FontRelief::Engraved ) + nOff = -nOff; + + rLayout.DrawOffset() += Point( nOff, nOff ); + updateGraphicsState(); + drawLayout( rLayout, rText, bTextLines ); + + rLayout.DrawOffset() -= Point( nOff, nOff ); + setTextLineColor( aTextLineColor ); + setOverlineColor( aOverlineColor ); + aSetFont.SetColor( aTextColor ); + setFont( aSetFont ); + updateGraphicsState(); + drawLayout( rLayout, rText, bTextLines ); + + // clean up the mess + pop(); +} + +void PDFWriterImpl::drawShadow( SalLayout& rLayout, const OUString& rText, bool bTextLines ) +{ + Font aSaveFont = m_aCurrentPDFState.m_aFont; + Color aSaveTextLineColor = m_aCurrentPDFState.m_aTextLineColor; + Color aSaveOverlineColor = m_aCurrentPDFState.m_aOverlineColor; + + Font& rFont = m_aCurrentPDFState.m_aFont; + if( rFont.GetColor() == COL_BLACK || rFont.GetColor().GetLuminance() < 8 ) + rFont.SetColor( COL_LIGHTGRAY ); + else + rFont.SetColor( COL_BLACK ); + rFont.SetShadow( false ); + rFont.SetOutline( false ); + setFont( rFont ); + setTextLineColor( rFont.GetColor() ); + setOverlineColor( rFont.GetColor() ); + updateGraphicsState(); + + long nOff = 1 + ((GetFontInstance()->mnLineHeight-24)/24); + if( rFont.IsOutline() ) + nOff++; + rLayout.DrawBase() += Point( nOff, nOff ); + drawLayout( rLayout, rText, bTextLines ); + rLayout.DrawBase() -= Point( nOff, nOff ); + + setFont( aSaveFont ); + setTextLineColor( aSaveTextLineColor ); + setOverlineColor( aSaveOverlineColor ); + updateGraphicsState(); +} + +void PDFWriterImpl::drawVerticalGlyphs( + const std::vector<PDFGlyph>& rGlyphs, + OStringBuffer& rLine, + const Point& rAlignOffset, + const Matrix3& rRotScale, + double fAngle, + double fXScale, + double fSkew, + sal_Int32 nFontHeight ) +{ + long nXOffset = 0; + Point aCurPos( rGlyphs[0].m_aPos ); + aCurPos = PixelToLogic( aCurPos ); + aCurPos += rAlignOffset; + for( size_t i = 0; i < rGlyphs.size(); i++ ) + { + // have to emit each glyph on its own + double fDeltaAngle = 0.0; + double fYScale = 1.0; + double fTempXScale = fXScale; + double fSkewB = fSkew; + double fSkewA = 0.0; + + Point aDeltaPos; + if (rGlyphs[i].m_pGlyph->IsVertical()) + { + fDeltaAngle = M_PI/2.0; + aDeltaPos.setX( GetFontMetric().GetAscent() ); + aDeltaPos.setY( static_cast<int>(static_cast<double>(GetFontMetric().GetDescent()) * fXScale) ); + fYScale = fXScale; + fTempXScale = 1.0; + fSkewA = -fSkewB; + fSkewB = 0.0; + } + aDeltaPos += PixelToLogic( Point( static_cast<int>(static_cast<double>(nXOffset)/fXScale), 0 ) ) - PixelToLogic( Point() ); + if( i < rGlyphs.size()-1 ) + // #i120627# the text on the Y axis is reversed when export ppt file to PDF format + { + long nOffsetX = rGlyphs[i+1].m_aPos.X() - rGlyphs[i].m_aPos.X(); + long nOffsetY = rGlyphs[i+1].m_aPos.Y() - rGlyphs[i].m_aPos.Y(); + nXOffset += static_cast<int>(sqrt(double(nOffsetX*nOffsetX + nOffsetY*nOffsetY))); + } + if (!rGlyphs[i].m_pGlyph->glyphId()) + continue; + + aDeltaPos = rRotScale.transform( aDeltaPos ); + + Matrix3 aMat; + if( fSkewB != 0.0 || fSkewA != 0.0 ) + aMat.skew( fSkewA, fSkewB ); + aMat.scale( fTempXScale, fYScale ); + aMat.rotate( fAngle+fDeltaAngle ); + aMat.translate( aCurPos.X()+aDeltaPos.X(), aCurPos.Y()+aDeltaPos.Y() ); + m_aPages.back().appendMatrix3(aMat, rLine); + rLine.append( " Tm" ); + if( i == 0 || rGlyphs[i-1].m_nMappedFontId != rGlyphs[i].m_nMappedFontId ) + { + rLine.append( " /F" ); + rLine.append( rGlyphs[i].m_nMappedFontId ); + rLine.append( ' ' ); + m_aPages.back().appendMappedLength( nFontHeight, rLine ); + rLine.append( " Tf" ); + } + rLine.append( "<" ); + appendHex( rGlyphs[i].m_nMappedGlyphId, rLine ); + rLine.append( ">Tj\n" ); + } +} + +void PDFWriterImpl::drawHorizontalGlyphs( + const std::vector<PDFGlyph>& rGlyphs, + OStringBuffer& rLine, + const Point& rAlignOffset, + bool bFirst, + double fAngle, + double fXScale, + double fSkew, + sal_Int32 nFontHeight, + sal_Int32 nPixelFontHeight + ) +{ + // horizontal (= normal) case + + // fill in run end indices + // end is marked by index of the first glyph of the next run + // a run is marked by same mapped font id and same Y position + std::vector< sal_uInt32 > aRunEnds; + aRunEnds.reserve( rGlyphs.size() ); + for( size_t i = 1; i < rGlyphs.size(); i++ ) + { + if( rGlyphs[i].m_nMappedFontId != rGlyphs[i-1].m_nMappedFontId || + rGlyphs[i].m_aPos.Y() != rGlyphs[i-1].m_aPos.Y() ) + { + aRunEnds.push_back(i); + } + } + // last run ends at last glyph + aRunEnds.push_back( rGlyphs.size() ); + + // loop over runs of the same font + sal_uInt32 nBeginRun = 0; + for( size_t nRun = 0; nRun < aRunEnds.size(); nRun++ ) + { + // setup text matrix + Point aCurPos = rGlyphs[nBeginRun].m_aPos; + // back transformation to current coordinate system + aCurPos = PixelToLogic( aCurPos ); + aCurPos += rAlignOffset; + // the first run can be set with "Td" operator + // subsequent use of that operator would move + // the textline matrix relative to what was set before + // making use of that would drive us into rounding issues + Matrix3 aMat; + if( bFirst && nRun == 0 && fAngle == 0.0 && fXScale == 1.0 && fSkew == 0.0 ) + { + m_aPages.back().appendPoint( aCurPos, rLine ); + rLine.append( " Td " ); + } + else + { + if( fSkew != 0.0 ) + aMat.skew( 0.0, fSkew ); + aMat.scale( fXScale, 1.0 ); + aMat.rotate( fAngle ); + aMat.translate( aCurPos.X(), aCurPos.Y() ); + m_aPages.back().appendMatrix3(aMat, rLine); + rLine.append( " Tm\n" ); + } + // set up correct font + rLine.append( "/F" ); + rLine.append( rGlyphs[nBeginRun].m_nMappedFontId ); + rLine.append( ' ' ); + m_aPages.back().appendMappedLength( nFontHeight, rLine ); + rLine.append( " Tf" ); + + // output glyphs using Tj or TJ + OStringBuffer aKernedLine( 256 ), aUnkernedLine( 256 ); + aKernedLine.append( "[<" ); + aUnkernedLine.append( '<' ); + appendHex( rGlyphs[nBeginRun].m_nMappedGlyphId, aKernedLine ); + appendHex( rGlyphs[nBeginRun].m_nMappedGlyphId, aUnkernedLine ); + + aMat.invert(); + bool bNeedKern = false; + for( sal_uInt32 nPos = nBeginRun+1; nPos < aRunEnds[nRun]; nPos++ ) + { + appendHex( rGlyphs[nPos].m_nMappedGlyphId, aUnkernedLine ); + // check if default glyph positioning is sufficient + const Point aThisPos = aMat.transform( rGlyphs[nPos].m_aPos ); + const Point aPrevPos = aMat.transform( rGlyphs[nPos-1].m_aPos ); + double fAdvance = aThisPos.X() - aPrevPos.X(); + fAdvance *= 1000.0 / nPixelFontHeight; + const double fAdjustment = rGlyphs[nPos-1].m_nNativeWidth - fAdvance + 0.5; + SAL_WARN_IF( + fAdjustment < SAL_MIN_INT32 || fAdjustment > SAL_MAX_INT32, "vcl.pdfwriter", + "adjustment " << fAdjustment << " outside 32-bit int"); + const sal_Int32 nAdjustment = static_cast<sal_Int32>( + std::clamp(fAdjustment, double(SAL_MIN_INT32), double(SAL_MAX_INT32))); + if( nAdjustment != 0 ) + { + // apply individual glyph positioning + bNeedKern = true; + aKernedLine.append( ">" ); + aKernedLine.append( nAdjustment ); + aKernedLine.append( "<" ); + } + appendHex( rGlyphs[nPos].m_nMappedGlyphId, aKernedLine ); + } + aKernedLine.append( ">]TJ\n" ); + aUnkernedLine.append( ">Tj\n" ); + rLine.append( + (bNeedKern ? aKernedLine : aUnkernedLine).makeStringAndClear() ); + + // set beginning of next run + nBeginRun = aRunEnds[nRun]; + } +} + +void PDFWriterImpl::drawLayout( SalLayout& rLayout, const OUString& rText, bool bTextLines ) +{ + // relief takes precedence over shadow (see outdev3.cxx) + if( m_aCurrentPDFState.m_aFont.GetRelief() != FontRelief::NONE ) + { + drawRelief( rLayout, rText, bTextLines ); + return; + } + else if( m_aCurrentPDFState.m_aFont.IsShadow() ) + drawShadow( rLayout, rText, bTextLines ); + + OStringBuffer aLine( 512 ); + + const int nMaxGlyphs = 256; + + std::vector<sal_Ucs> aCodeUnits; + bool bVertical = m_aCurrentPDFState.m_aFont.IsVertical(); + int nIndex = 0; + double fXScale = 1.0; + double fSkew = 0.0; + sal_Int32 nPixelFontHeight = GetFontInstance()->GetFontSelectPattern().mnHeight; + TextAlign eAlign = m_aCurrentPDFState.m_aFont.GetAlignment(); + + // transform font height back to current units + // note: the layout calculates in outdevs device pixel !! + sal_Int32 nFontHeight = ImplDevicePixelToLogicHeight( nPixelFontHeight ); + if( m_aCurrentPDFState.m_aFont.GetAverageFontWidth() ) + { + Font aFont( m_aCurrentPDFState.m_aFont ); + aFont.SetAverageFontWidth( 0 ); + FontMetric aMetric = GetFontMetric( aFont ); + if( aMetric.GetAverageFontWidth() != m_aCurrentPDFState.m_aFont.GetAverageFontWidth() ) + { + fXScale = + static_cast<double>(m_aCurrentPDFState.m_aFont.GetAverageFontWidth()) / + static_cast<double>(aMetric.GetAverageFontWidth()); + } + } + + // perform artificial italics if necessary + if( ( m_aCurrentPDFState.m_aFont.GetItalic() == ITALIC_NORMAL || + m_aCurrentPDFState.m_aFont.GetItalic() == ITALIC_OBLIQUE ) && + !( GetFontInstance()->GetFontFace()->GetItalic() == ITALIC_NORMAL || + GetFontInstance()->GetFontFace()->GetItalic() == ITALIC_OBLIQUE ) + ) + { + fSkew = M_PI/12.0; + } + + // if the mapmode is distorted we need to adjust for that also + if( m_aCurrentPDFState.m_aMapMode.GetScaleX() != m_aCurrentPDFState.m_aMapMode.GetScaleY() ) + { + fXScale *= double(m_aCurrentPDFState.m_aMapMode.GetScaleX()) / double(m_aCurrentPDFState.m_aMapMode.GetScaleY()); + } + + int nAngle = m_aCurrentPDFState.m_aFont.GetOrientation(); + // normalize angles + while( nAngle < 0 ) + nAngle += 3600; + nAngle = nAngle % 3600; + double fAngle = static_cast<double>(nAngle) * M_PI / 1800.0; + + Matrix3 aRotScale; + aRotScale.scale( fXScale, 1.0 ); + if( fAngle != 0.0 ) + aRotScale.rotate( -fAngle ); + + bool bPop = false; + bool bABold = false; + // artificial bold necessary ? + if( GetFontInstance()->GetFontFace()->GetWeight() <= WEIGHT_MEDIUM && + GetFontInstance()->GetFontSelectPattern().GetWeight() > WEIGHT_MEDIUM ) + { + aLine.append("q "); + bPop = true; + bABold = true; + } + // setup text colors (if necessary) + Color aStrokeColor( COL_TRANSPARENT ); + Color aNonStrokeColor( COL_TRANSPARENT ); + + if( m_aCurrentPDFState.m_aFont.IsOutline() ) + { + aStrokeColor = m_aCurrentPDFState.m_aFont.GetColor(); + aNonStrokeColor = COL_WHITE; + } + else + aNonStrokeColor = m_aCurrentPDFState.m_aFont.GetColor(); + if( bABold ) + aStrokeColor = m_aCurrentPDFState.m_aFont.GetColor(); + + if( aStrokeColor != COL_TRANSPARENT && aStrokeColor != m_aCurrentPDFState.m_aLineColor ) + { + if( ! bPop ) + aLine.append( "q " ); + bPop = true; + appendStrokingColor( aStrokeColor, aLine ); + aLine.append( "\n" ); + } + if( aNonStrokeColor != COL_TRANSPARENT && aNonStrokeColor != m_aCurrentPDFState.m_aFillColor ) + { + if( ! bPop ) + aLine.append( "q " ); + bPop = true; + appendNonStrokingColor( aNonStrokeColor, aLine ); + aLine.append( "\n" ); + } + + // begin text object + aLine.append( "BT\n" ); + // outline attribute ? + if( m_aCurrentPDFState.m_aFont.IsOutline() || bABold ) + { + // set correct text mode, set stroke width + aLine.append( "2 Tr " ); // fill, then stroke + + if( m_aCurrentPDFState.m_aFont.IsOutline() ) + { + // unclear what to do in case of outline and artificial bold + // for the time being outline wins + aLine.append( "0.25 w \n" ); + } + else + { + double fW = static_cast<double>(m_aCurrentPDFState.m_aFont.GetFontHeight()) / 30.0; + m_aPages.back().appendMappedLength( fW, aLine ); + aLine.append ( " w\n" ); + } + } + + FontMetric aRefDevFontMetric = GetFontMetric(); + const PhysicalFontFace* pDevFont = GetFontInstance()->GetFontFace(); + const GlyphItem* pGlyph = nullptr; + const PhysicalFontFace* pFallbackFont = nullptr; + + // collect the glyphs into a single array + std::vector< PDFGlyph > aGlyphs; + aGlyphs.reserve( nMaxGlyphs ); + // first get all the glyphs and register them; coordinates still in Pixel + Point aPos; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nIndex, &pFallbackFont)) + { + const auto* pFont = pFallbackFont ? pFallbackFont : pDevFont; + + aCodeUnits.clear(); + + // tdf#66597, tdf#115117 + // + // Here is how we embed textual content in PDF files, to allow for + // better text extraction for complex and typography-rich text. + // + // * If there is many to one or many to many mapping, use an + // ActualText span embedding the original string, since ToUnicode + // can’t handle these. + // * If the one glyph is used for several Unicode code points, also + // use ActualText since ToUnicode can map each glyph in the font + // only once. + // * Limit ActualText to single cluster at a time, since using it + // for whole words or sentences breaks text selection and + // highlighting in PDF viewers (there will be no way to tell + // which glyphs belong to which characters). + // * Keep generating (now) redundant ToUnicode entries for + // compatibility with old tools not supporting ActualText. + + assert(pGlyph->charCount() >= 0); + for (int n = 0; n < pGlyph->charCount(); n++) + aCodeUnits.push_back(rText[pGlyph->charPos() + n]); + + bool bUseActualText = false; + + // If this is a start of complex cluster, use ActualText. + if (pGlyph->IsClusterStart()) + bUseActualText = true; + + // Or part of a complex cluster, will be handled by the ActualText + // of its cluster start. + if (pGlyph->IsInCluster()) + assert(aCodeUnits.empty()); + + // A glyph can’t have more than one ToUnicode entry, use ActualText + // instead. + if (!aCodeUnits.empty() && !bUseActualText) + { + for (const auto& rSubset : m_aSubsets[pFont].m_aSubsets) + { + const auto& it = rSubset.m_aMapping.find(pGlyph->glyphId()); + if (it != rSubset.m_aMapping.cend() && it->second.codes() != aCodeUnits) + { + bUseActualText = true; + aCodeUnits.clear(); + } + } + } + + assert(!aCodeUnits.empty() || bUseActualText || pGlyph->IsInCluster()); + + sal_uInt8 nMappedGlyph; + sal_Int32 nMappedFontObject; + registerGlyph(pGlyph, pFont, aCodeUnits, nMappedGlyph, nMappedFontObject); + + sal_Int32 nGlyphWidth = 0; + SalGraphics *pGraphics = GetGraphics(); + if (pGraphics) + nGlyphWidth = m_aFontCache.getGlyphWidth(pFont, + pGlyph->glyphId(), + pGlyph->IsVertical(), + pGraphics); + + int nCharPos = -1; + if (bUseActualText || pGlyph->IsInCluster()) + nCharPos = pGlyph->charPos(); + + aGlyphs.emplace_back(aPos, + pGlyph, + nGlyphWidth, + nMappedFontObject, + nMappedGlyph, + nCharPos); + } + + // Avoid fill color when map mode is in pixels, the below code assumes + // logic map mode. + bool bPixel = m_aCurrentPDFState.m_aMapMode.GetMapUnit() == MapUnit::MapPixel; + if (m_aCurrentPDFState.m_aFont.GetFillColor() != COL_TRANSPARENT && !bPixel) + { + // PDF doesn't have a text fill color, so draw a rectangle before + // drawing the actual text. + push(PushFlags::FILLCOLOR | PushFlags::LINECOLOR); + setFillColor(m_aCurrentPDFState.m_aFont.GetFillColor()); + // Avoid border around the rectangle for Writer shape text. + setLineColor(COL_TRANSPARENT); + + // The rectangle is the bounding box of the text, but also includes + // ascent / descent to match the on-screen rendering. + tools::Rectangle aRectangle; + // This is the top left of the text without ascent / descent. + aRectangle.SetPos(PixelToLogic(rLayout.GetDrawPosition())); + aRectangle.setY(aRectangle.getY() - aRefDevFontMetric.GetAscent()); + aRectangle.SetSize(PixelToLogic(Size(rLayout.GetTextWidth(), 0))); + // This includes ascent / descent. + aRectangle.setHeight(aRefDevFontMetric.GetLineHeight()); + + const LogicalFontInstance* pFontInstance = GetFontInstance(); + if (pFontInstance->mnOrientation) + { + // Adapt rectangle for rotated text. + tools::Polygon aPolygon(aRectangle); + aPolygon.Rotate(PixelToLogic(rLayout.GetDrawPosition()), pFontInstance->mnOrientation); + drawPolygon(aPolygon); + } + else + drawRectangle(aRectangle); + + pop(); + } + + Point aAlignOffset; + if ( eAlign == ALIGN_BOTTOM ) + aAlignOffset.AdjustY( -(aRefDevFontMetric.GetDescent()) ); + else if ( eAlign == ALIGN_TOP ) + aAlignOffset.AdjustY(aRefDevFontMetric.GetAscent() ); + if( aAlignOffset.X() || aAlignOffset.Y() ) + aAlignOffset = aRotScale.transform( aAlignOffset ); + + /* #159153# do not emit an empty glyph vector; this can happen if e.g. the original + string contained only one of the UTF16 BOMs + */ + if( ! aGlyphs.empty() ) + { + size_t nStart = 0; + size_t nEnd = 0; + while (nStart < aGlyphs.size()) + { + while (nEnd < aGlyphs.size() && aGlyphs[nEnd].m_nCharPos == aGlyphs[nStart].m_nCharPos) + nEnd++; + + std::vector<PDFGlyph> aRun(aGlyphs.begin() + nStart, aGlyphs.begin() + nEnd); + + int nCharPos, nCharCount; + if (!aRun.front().m_pGlyph->IsRTLGlyph()) + { + nCharPos = aRun.front().m_nCharPos; + nCharCount = aRun.front().m_pGlyph->charCount(); + } + else + { + nCharPos = aRun.back().m_nCharPos; + nCharCount = aRun.back().m_pGlyph->charCount(); + } + + if (nCharPos >= 0 && nCharCount) + { + aLine.append("/Span<</ActualText<FEFF"); + for (int i = 0; i < nCharCount; i++) + { + sal_Unicode aChar = rText[nCharPos + i]; + appendHex(static_cast<sal_Int8>(aChar >> 8), aLine); + appendHex(static_cast<sal_Int8>(aChar & 255), aLine); + } + aLine.append( ">>>\nBDC\n" ); + } + + if (bVertical) + drawVerticalGlyphs(aRun, aLine, aAlignOffset, aRotScale, fAngle, fXScale, fSkew, nFontHeight); + else + drawHorizontalGlyphs(aRun, aLine, aAlignOffset, nStart == 0, fAngle, fXScale, fSkew, nFontHeight, nPixelFontHeight); + + if (nCharPos >= 0 && nCharCount) + aLine.append( "EMC\n" ); + + nStart = nEnd; + } + } + + // end textobject + aLine.append( "ET\n" ); + if( bPop ) + aLine.append( "Q\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); + + // draw eventual textlines + FontStrikeout eStrikeout = m_aCurrentPDFState.m_aFont.GetStrikeout(); + FontLineStyle eUnderline = m_aCurrentPDFState.m_aFont.GetUnderline(); + FontLineStyle eOverline = m_aCurrentPDFState.m_aFont.GetOverline(); + if( bTextLines && + ( + ( eUnderline != LINESTYLE_NONE && eUnderline != LINESTYLE_DONTKNOW ) || + ( eOverline != LINESTYLE_NONE && eOverline != LINESTYLE_DONTKNOW ) || + ( eStrikeout != STRIKEOUT_NONE && eStrikeout != STRIKEOUT_DONTKNOW ) + ) + ) + { + bool bUnderlineAbove = m_aCurrentPDFState.m_aFont.IsUnderlineAbove(); + if( m_aCurrentPDFState.m_aFont.IsWordLineMode() ) + { + Point aStartPt; + sal_Int32 nWidth = 0; + nIndex = 0; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nIndex)) + { + if (!pGlyph->IsSpacing()) + { + if( !nWidth ) + aStartPt = aPos; + + nWidth += pGlyph->m_nNewWidth; + } + else if( nWidth > 0 ) + { + drawTextLine( PixelToLogic( aStartPt ), + ImplDevicePixelToLogicWidth( nWidth ), + eStrikeout, eUnderline, eOverline, bUnderlineAbove ); + nWidth = 0; + } + } + + if( nWidth > 0 ) + { + drawTextLine( PixelToLogic( aStartPt ), + ImplDevicePixelToLogicWidth( nWidth ), + eStrikeout, eUnderline, eOverline, bUnderlineAbove ); + } + } + else + { + Point aStartPt = rLayout.GetDrawPosition(); + int nWidth = rLayout.GetTextWidth() / rLayout.GetUnitsPerPixel(); + drawTextLine( PixelToLogic( aStartPt ), + ImplDevicePixelToLogicWidth( nWidth ), + eStrikeout, eUnderline, eOverline, bUnderlineAbove ); + } + } + + // write eventual emphasis marks + if( !(m_aCurrentPDFState.m_aFont.GetEmphasisMark() & FontEmphasisMark::Style) ) + return; + + tools::PolyPolygon aEmphPoly; + tools::Rectangle aEmphRect1; + tools::Rectangle aEmphRect2; + long nEmphYOff; + long nEmphWidth; + long nEmphHeight; + bool bEmphPolyLine; + FontEmphasisMark nEmphMark; + + push( PushFlags::ALL ); + + aLine.setLength( 0 ); + aLine.append( "q\n" ); + + nEmphMark = OutputDevice::ImplGetEmphasisMarkStyle( m_aCurrentPDFState.m_aFont ); + if ( nEmphMark & FontEmphasisMark::PosBelow ) + nEmphHeight = GetEmphasisDescent(); + else + nEmphHeight = GetEmphasisAscent(); + ImplGetEmphasisMark( aEmphPoly, + bEmphPolyLine, + aEmphRect1, + aEmphRect2, + nEmphYOff, + nEmphWidth, + nEmphMark, + ImplDevicePixelToLogicWidth(nEmphHeight) ); + if ( bEmphPolyLine ) + { + setLineColor( m_aCurrentPDFState.m_aFont.GetColor() ); + setFillColor( COL_TRANSPARENT ); + } + else + { + setFillColor( m_aCurrentPDFState.m_aFont.GetColor() ); + setLineColor( COL_TRANSPARENT ); + } + writeBuffer( aLine.getStr(), aLine.getLength() ); + + Point aOffset(0,0); + + if ( nEmphMark & FontEmphasisMark::PosBelow ) + aOffset.AdjustY(GetFontInstance()->mxFontMetric->GetDescent() + nEmphYOff ); + else + aOffset.AdjustY( -(GetFontInstance()->mxFontMetric->GetAscent() + nEmphYOff) ); + + long nEmphWidth2 = nEmphWidth / 2; + long nEmphHeight2 = nEmphHeight / 2; + aOffset += Point( nEmphWidth2, nEmphHeight2 ); + + if ( eAlign == ALIGN_BOTTOM ) + aOffset.AdjustY( -(GetFontInstance()->mxFontMetric->GetDescent()) ); + else if ( eAlign == ALIGN_TOP ) + aOffset.AdjustY(GetFontInstance()->mxFontMetric->GetAscent() ); + + nIndex = 0; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nIndex)) + { + if (pGlyph->IsSpacing()) + { + Point aAdjOffset = aOffset; + aAdjOffset.AdjustX((pGlyph->m_nNewWidth - nEmphWidth) / 2 ); + aAdjOffset = aRotScale.transform( aAdjOffset ); + + aAdjOffset -= Point( nEmphWidth2, nEmphHeight2 ); + + aPos += aAdjOffset; + aPos = PixelToLogic( aPos ); + drawEmphasisMark( aPos.X(), aPos.Y(), + aEmphPoly, bEmphPolyLine, + aEmphRect1, aEmphRect2 ); + } + } + + writeBuffer( "Q\n", 2 ); + pop(); + +} + +void PDFWriterImpl::drawEmphasisMark( long nX, long nY, + const tools::PolyPolygon& rPolyPoly, bool bPolyLine, + const tools::Rectangle& rRect1, const tools::Rectangle& rRect2 ) +{ + // TODO: pass nWidth as width of this mark + // long nWidth = 0; + + if ( rPolyPoly.Count() ) + { + if ( bPolyLine ) + { + tools::Polygon aPoly = rPolyPoly.GetObject( 0 ); + aPoly.Move( nX, nY ); + drawPolyLine( aPoly ); + } + else + { + tools::PolyPolygon aPolyPoly = rPolyPoly; + aPolyPoly.Move( nX, nY ); + drawPolyPolygon( aPolyPoly ); + } + } + + if ( !rRect1.IsEmpty() ) + { + tools::Rectangle aRect( Point( nX+rRect1.Left(), + nY+rRect1.Top() ), rRect1.GetSize() ); + drawRectangle( aRect ); + } + + if ( !rRect2.IsEmpty() ) + { + tools::Rectangle aRect( Point( nX+rRect2.Left(), + nY+rRect2.Top() ), rRect2.GetSize() ); + + drawRectangle( aRect ); + } +} + +void PDFWriterImpl::drawText( const Point& rPos, const OUString& rText, sal_Int32 nIndex, sal_Int32 nLen, bool bTextLines ) +{ + MARK( "drawText" ); + + updateGraphicsState(); + + // get a layout from the OutputDevice's SalGraphics + // this also enforces font substitution and sets the font on SalGraphics + std::unique_ptr<SalLayout> pLayout = ImplLayout( rText, nIndex, nLen, rPos ); + if( pLayout ) + { + drawLayout( *pLayout, rText, bTextLines ); + } +} + +void PDFWriterImpl::drawTextArray( const Point& rPos, const OUString& rText, const long* pDXArray, sal_Int32 nIndex, sal_Int32 nLen ) +{ + MARK( "drawText with array" ); + + updateGraphicsState(); + + // get a layout from the OutputDevice's SalGraphics + // this also enforces font substitution and sets the font on SalGraphics + std::unique_ptr<SalLayout> pLayout = ImplLayout( rText, nIndex, nLen, rPos, 0, pDXArray ); + if( pLayout ) + { + drawLayout( *pLayout, rText, true ); + } +} + +void PDFWriterImpl::drawStretchText( const Point& rPos, sal_uLong nWidth, const OUString& rText, sal_Int32 nIndex, sal_Int32 nLen ) +{ + MARK( "drawStretchText" ); + + updateGraphicsState(); + + // get a layout from the OutputDevice's SalGraphics + // this also enforces font substitution and sets the font on SalGraphics + std::unique_ptr<SalLayout> pLayout = ImplLayout( rText, nIndex, nLen, rPos, nWidth ); + if( pLayout ) + { + drawLayout( *pLayout, rText, true ); + } +} + +void PDFWriterImpl::drawText( const tools::Rectangle& rRect, const OUString& rOrigStr, DrawTextFlags nStyle ) +{ + long nWidth = rRect.GetWidth(); + long nHeight = rRect.GetHeight(); + + if ( nWidth <= 0 || nHeight <= 0 ) + return; + + MARK( "drawText with rectangle" ); + + updateGraphicsState(); + + // clip with rectangle + OStringBuffer aLine; + aLine.append( "q " ); + m_aPages.back().appendRect( rRect, aLine ); + aLine.append( " W* n\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + + // if disabled text is needed, put in here + + Point aPos = rRect.TopLeft(); + + long nTextHeight = GetTextHeight(); + sal_Int32 nMnemonicPos = -1; + + OUString aStr = rOrigStr; + if ( nStyle & DrawTextFlags::Mnemonic ) + aStr = OutputDevice::GetNonMnemonicString( aStr, nMnemonicPos ); + + // multiline text + if ( nStyle & DrawTextFlags::MultiLine ) + { + OUString aLastLine; + ImplMultiTextLineInfo aMultiLineInfo; + ImplTextLineInfo* pLineInfo; + sal_Int32 i; + sal_Int32 nLines; + sal_Int32 nFormatLines; + + if ( nTextHeight ) + { + vcl::DefaultTextLayout aLayout( *this ); + OutputDevice::ImplGetTextLines( aMultiLineInfo, nWidth, aStr, nStyle, aLayout ); + nLines = nHeight/nTextHeight; + nFormatLines = aMultiLineInfo.Count(); + if ( !nLines ) + nLines = 1; + if ( nFormatLines > nLines ) + { + if ( nStyle & DrawTextFlags::EndEllipsis ) + { + // handle last line + nFormatLines = nLines-1; + + pLineInfo = aMultiLineInfo.GetLine( nFormatLines ); + aLastLine = convertLineEnd(aStr.copy(pLineInfo->GetIndex()), LINEEND_LF); + // replace line feed by space + aLastLine = aLastLine.replace('\n', ' '); + aLastLine = GetEllipsisString( aLastLine, nWidth, nStyle ); + nStyle &= ~DrawTextFlags(DrawTextFlags::VCenter | DrawTextFlags::Bottom); + nStyle |= DrawTextFlags::Top; + } + } + + // vertical alignment + if ( nStyle & DrawTextFlags::Bottom ) + aPos.AdjustY(nHeight-(nFormatLines*nTextHeight) ); + else if ( nStyle & DrawTextFlags::VCenter ) + aPos.AdjustY((nHeight-(nFormatLines*nTextHeight))/2 ); + + // draw all lines excluding the last + for ( i = 0; i < nFormatLines; i++ ) + { + pLineInfo = aMultiLineInfo.GetLine( i ); + if ( nStyle & DrawTextFlags::Right ) + aPos.AdjustX(nWidth-pLineInfo->GetWidth() ); + else if ( nStyle & DrawTextFlags::Center ) + aPos.AdjustX((nWidth-pLineInfo->GetWidth())/2 ); + sal_Int32 nIndex = pLineInfo->GetIndex(); + sal_Int32 nLineLen = pLineInfo->GetLen(); + drawText( aPos, aStr, nIndex, nLineLen ); + // mnemonics should not appear in documents, + // if the need arises, put them in here + aPos.AdjustY(nTextHeight ); + aPos.setX( rRect.Left() ); + } + + // output last line left adjusted since it was shortened + if (!aLastLine.isEmpty()) + drawText( aPos, aLastLine, 0, aLastLine.getLength() ); + } + } + else + { + long nTextWidth = GetTextWidth( aStr ); + + // Evt. Text kuerzen + if ( nTextWidth > nWidth ) + { + if ( nStyle & (DrawTextFlags::EndEllipsis | DrawTextFlags::PathEllipsis | DrawTextFlags::NewsEllipsis) ) + { + aStr = GetEllipsisString( aStr, nWidth, nStyle ); + nStyle &= ~DrawTextFlags(DrawTextFlags::Center | DrawTextFlags::Right); + nStyle |= DrawTextFlags::Left; + nTextWidth = GetTextWidth( aStr ); + } + } + + // vertical alignment + if ( nStyle & DrawTextFlags::Right ) + aPos.AdjustX(nWidth-nTextWidth ); + else if ( nStyle & DrawTextFlags::Center ) + aPos.AdjustX((nWidth-nTextWidth)/2 ); + + if ( nStyle & DrawTextFlags::Bottom ) + aPos.AdjustY(nHeight-nTextHeight ); + else if ( nStyle & DrawTextFlags::VCenter ) + aPos.AdjustY((nHeight-nTextHeight)/2 ); + + // mnemonics should be inserted here if the need arises + + // draw the actual text + drawText( aPos, aStr, 0, aStr.getLength() ); + } + + // reset clip region to original value + aLine.setLength( 0 ); + aLine.append( "Q\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawLine( const Point& rStart, const Point& rStop ) +{ + MARK( "drawLine" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT ) + return; + + OStringBuffer aLine; + m_aPages.back().appendPoint( rStart, aLine ); + aLine.append( " m " ); + m_aPages.back().appendPoint( rStop, aLine ); + aLine.append( " l S\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawLine( const Point& rStart, const Point& rStop, const LineInfo& rInfo ) +{ + MARK( "drawLine with LineInfo" ); + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT ) + return; + + if( rInfo.GetStyle() == LineStyle::Solid && rInfo.GetWidth() < 2 ) + { + drawLine( rStart, rStop ); + return; + } + + OStringBuffer aLine; + + aLine.append( "q " ); + if( m_aPages.back().appendLineInfo( rInfo, aLine ) ) + { + m_aPages.back().appendPoint( rStart, aLine ); + aLine.append( " m " ); + m_aPages.back().appendPoint( rStop, aLine ); + aLine.append( " l S Q\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); + } + else + { + PDFWriter::ExtLineInfo aInfo; + convertLineInfoToExtLineInfo( rInfo, aInfo ); + Point aPolyPoints[2] = { rStart, rStop }; + tools::Polygon aPoly( 2, aPolyPoints ); + drawPolyLine( aPoly, aInfo ); + } +} + +#define HCONV( x ) ImplDevicePixelToLogicHeight( x ) + +void PDFWriterImpl::drawWaveTextLine( OStringBuffer& aLine, long nWidth, FontLineStyle eTextLine, Color aColor, bool bIsAbove ) +{ + // note: units in pFontInstance are ref device pixel + const LogicalFontInstance* pFontInstance = GetFontInstance(); + long nLineHeight = 0; + long nLinePos = 0; + + appendStrokingColor( aColor, aLine ); + aLine.append( "\n" ); + + if ( bIsAbove ) + { + if ( !pFontInstance->mxFontMetric->GetAboveWavelineUnderlineSize() ) + ImplInitAboveTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetAboveWavelineUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetAboveWavelineUnderlineOffset() ); + } + else + { + if ( !pFontInstance->mxFontMetric->GetWavelineUnderlineSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetWavelineUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetWavelineUnderlineOffset() ); + } + if ( (eTextLine == LINESTYLE_SMALLWAVE) && (nLineHeight > 3) ) + nLineHeight = 3; + + long nLineWidth = GetDPIX()/450; + if ( ! nLineWidth ) + nLineWidth = 1; + + if ( eTextLine == LINESTYLE_BOLDWAVE ) + nLineWidth = 3*nLineWidth; + + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineWidth), aLine ); + aLine.append( " w " ); + + if ( eTextLine == LINESTYLE_DOUBLEWAVE ) + { + long nOrgLineHeight = nLineHeight; + nLineHeight /= 3; + if ( nLineHeight < 2 ) + { + if ( nOrgLineHeight > 1 ) + nLineHeight = 2; + else + nLineHeight = 1; + } + long nLineDY = nOrgLineHeight-(nLineHeight*2); + if ( nLineDY < nLineWidth ) + nLineDY = nLineWidth; + long nLineDY2 = nLineDY/2; + if ( !nLineDY2 ) + nLineDY2 = 1; + + nLinePos -= nLineWidth-nLineDY2; + + m_aPages.back().appendWaveLine( nWidth, -nLinePos, 2*nLineHeight, aLine ); + + nLinePos += nLineWidth+nLineDY; + m_aPages.back().appendWaveLine( nWidth, -nLinePos, 2*nLineHeight, aLine ); + } + else + { + if ( eTextLine != LINESTYLE_BOLDWAVE ) + nLinePos -= nLineWidth/2; + m_aPages.back().appendWaveLine( nWidth, -nLinePos, nLineHeight, aLine ); + } +} + +void PDFWriterImpl::drawStraightTextLine( OStringBuffer& aLine, long nWidth, FontLineStyle eTextLine, Color aColor, bool bIsAbove ) +{ + // note: units in pFontInstance are ref device pixel + const LogicalFontInstance* pFontInstance = GetFontInstance(); + long nLineHeight = 0; + long nLinePos = 0; + long nLinePos2 = 0; + + if ( eTextLine > LINESTYLE_BOLDWAVE ) + eTextLine = LINESTYLE_SINGLE; + + switch ( eTextLine ) + { + case LINESTYLE_SINGLE: + case LINESTYLE_DOTTED: + case LINESTYLE_DASH: + case LINESTYLE_LONGDASH: + case LINESTYLE_DASHDOT: + case LINESTYLE_DASHDOTDOT: + if ( bIsAbove ) + { + if ( !pFontInstance->mxFontMetric->GetAboveUnderlineSize() ) + ImplInitAboveTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetAboveUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetAboveUnderlineOffset() ); + } + else + { + if ( !pFontInstance->mxFontMetric->GetUnderlineSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetUnderlineOffset() ); + } + break; + case LINESTYLE_BOLD: + case LINESTYLE_BOLDDOTTED: + case LINESTYLE_BOLDDASH: + case LINESTYLE_BOLDLONGDASH: + case LINESTYLE_BOLDDASHDOT: + case LINESTYLE_BOLDDASHDOTDOT: + if ( bIsAbove ) + { + if ( !pFontInstance->mxFontMetric->GetAboveBoldUnderlineSize() ) + ImplInitAboveTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetAboveBoldUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetAboveBoldUnderlineOffset() ); + } + else + { + if ( !pFontInstance->mxFontMetric->GetBoldUnderlineSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetBoldUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetBoldUnderlineOffset() ); + nLinePos += nLineHeight/2; + } + break; + case LINESTYLE_DOUBLE: + if ( bIsAbove ) + { + if ( !pFontInstance->mxFontMetric->GetAboveDoubleUnderlineSize() ) + ImplInitAboveTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetAboveDoubleUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetAboveDoubleUnderlineOffset1() ); + nLinePos2 = HCONV( pFontInstance->mxFontMetric->GetAboveDoubleUnderlineOffset2() ); + } + else + { + if ( !pFontInstance->mxFontMetric->GetDoubleUnderlineSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetDoubleUnderlineSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetDoubleUnderlineOffset1() ); + nLinePos2 = HCONV( pFontInstance->mxFontMetric->GetDoubleUnderlineOffset2() ); + } + break; + default: + break; + } + + if ( !nLineHeight ) + return; + + // outline attribute ? + if (m_aCurrentPDFState.m_aFont.IsOutline() && eTextLine == LINESTYLE_SINGLE) + { + appendStrokingColor(aColor, aLine); // stroke with text color + aLine.append( " " ); + appendNonStrokingColor(COL_WHITE, aLine); // fill with white + aLine.append( "\n" ); + aLine.append( "0.25 w \n" ); // same line thickness as in drawLayout + + // draw rectangle instead + aLine.append( "0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos * 1.5), aLine ); + aLine.append( " " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nWidth), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine ); + aLine.append( " re h B\n" ); + return; + } + + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine ); + aLine.append( " w " ); + appendStrokingColor( aColor, aLine ); + aLine.append( "\n" ); + + switch ( eTextLine ) + { + case LINESTYLE_DOTTED: + case LINESTYLE_BOLDDOTTED: + aLine.append( "[ " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine, false ); + aLine.append( " ] 0 d\n" ); + break; + case LINESTYLE_DASH: + case LINESTYLE_LONGDASH: + case LINESTYLE_BOLDDASH: + case LINESTYLE_BOLDLONGDASH: + { + sal_Int32 nDashLength = 4*nLineHeight; + sal_Int32 nVoidLength = 2*nLineHeight; + if ( ( eTextLine == LINESTYLE_LONGDASH ) || ( eTextLine == LINESTYLE_BOLDLONGDASH ) ) + nDashLength = 8*nLineHeight; + + aLine.append( "[ " ); + m_aPages.back().appendMappedLength( nDashLength, aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( nVoidLength, aLine, false ); + aLine.append( " ] 0 d\n" ); + } + break; + case LINESTYLE_DASHDOT: + case LINESTYLE_BOLDDASHDOT: + { + sal_Int32 nDashLength = 4*nLineHeight; + sal_Int32 nVoidLength = 2*nLineHeight; + aLine.append( "[ " ); + m_aPages.back().appendMappedLength( nDashLength, aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( nVoidLength, aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( nVoidLength, aLine, false ); + aLine.append( " ] 0 d\n" ); + } + break; + case LINESTYLE_DASHDOTDOT: + case LINESTYLE_BOLDDASHDOTDOT: + { + sal_Int32 nDashLength = 4*nLineHeight; + sal_Int32 nVoidLength = 2*nLineHeight; + aLine.append( "[ " ); + m_aPages.back().appendMappedLength( nDashLength, aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( nVoidLength, aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( nVoidLength, aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( nVoidLength, aLine, false ); + aLine.append( " ] 0 d\n" ); + } + break; + default: + break; + } + + aLine.append( "0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos), aLine ); + aLine.append( " m " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nWidth), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos), aLine ); + aLine.append( " l S\n" ); + if ( eTextLine == LINESTYLE_DOUBLE ) + { + aLine.append( "0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos2-nLineHeight), aLine ); + aLine.append( " m " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nWidth), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos2-nLineHeight), aLine ); + aLine.append( " l S\n" ); + } + +} + +void PDFWriterImpl::drawStrikeoutLine( OStringBuffer& aLine, long nWidth, FontStrikeout eStrikeout, Color aColor ) +{ + // note: units in pFontInstance are ref device pixel + const LogicalFontInstance* pFontInstance = GetFontInstance(); + long nLineHeight = 0; + long nLinePos = 0; + long nLinePos2 = 0; + + if ( eStrikeout > STRIKEOUT_X ) + eStrikeout = STRIKEOUT_SINGLE; + + switch ( eStrikeout ) + { + case STRIKEOUT_SINGLE: + if ( !pFontInstance->mxFontMetric->GetStrikeoutSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetStrikeoutSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetStrikeoutOffset() ); + break; + case STRIKEOUT_BOLD: + if ( !pFontInstance->mxFontMetric->GetBoldStrikeoutSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetBoldStrikeoutSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetBoldStrikeoutOffset() ); + break; + case STRIKEOUT_DOUBLE: + if ( !pFontInstance->mxFontMetric->GetDoubleStrikeoutSize() ) + ImplInitTextLineSize(); + nLineHeight = HCONV( pFontInstance->mxFontMetric->GetDoubleStrikeoutSize() ); + nLinePos = HCONV( pFontInstance->mxFontMetric->GetDoubleStrikeoutOffset1() ); + nLinePos2 = HCONV( pFontInstance->mxFontMetric->GetDoubleStrikeoutOffset2() ); + break; + default: + break; + } + + if ( !nLineHeight ) + return; + + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nLineHeight), aLine ); + aLine.append( " w " ); + appendStrokingColor( aColor, aLine ); + aLine.append( "\n" ); + + aLine.append( "0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos), aLine ); + aLine.append( " m " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nWidth), aLine ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos), aLine ); + aLine.append( " l S\n" ); + + if ( eStrikeout == STRIKEOUT_DOUBLE ) + { + aLine.append( "0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos2-nLineHeight), aLine ); + aLine.append( " m " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(nWidth), aLine ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(-nLinePos2-nLineHeight), aLine ); + aLine.append( " l S\n" ); + } + +} + +void PDFWriterImpl::drawStrikeoutChar( const Point& rPos, long nWidth, FontStrikeout eStrikeout ) +{ + //See qadevOOo/testdocs/StrikeThrough.odt for examples if you need + //to tweak this + + OUString aStrikeoutChar = eStrikeout == STRIKEOUT_SLASH ? OUString( "/" ) : OUString( "X" ); + OUString aStrikeout = aStrikeoutChar; + while( GetTextWidth( aStrikeout ) < nWidth ) + aStrikeout += aStrikeout; + + // do not get broader than nWidth modulo 1 character + while( GetTextWidth( aStrikeout ) >= nWidth ) + aStrikeout = aStrikeout.replaceAt( 0, 1, "" ); + aStrikeout += aStrikeoutChar; + bool bShadow = m_aCurrentPDFState.m_aFont.IsShadow(); + if ( bShadow ) + { + Font aFont = m_aCurrentPDFState.m_aFont; + aFont.SetShadow( false ); + setFont( aFont ); + updateGraphicsState(); + } + + // strikeout string is left aligned non-CTL text + ComplexTextLayoutFlags nOrigTLM = GetLayoutMode(); + SetLayoutMode(ComplexTextLayoutFlags::BiDiStrong); + + push( PushFlags::CLIPREGION ); + FontMetric aRefDevFontMetric = GetFontMetric(); + tools::Rectangle aRect; + aRect.SetLeft( rPos.X() ); + aRect.SetRight( aRect.Left()+nWidth ); + aRect.SetBottom( rPos.Y()+aRefDevFontMetric.GetDescent() ); + aRect.SetTop( rPos.Y()-aRefDevFontMetric.GetAscent() ); + + const LogicalFontInstance* pFontInstance = GetFontInstance(); + if (pFontInstance->mnOrientation) + { + tools::Polygon aPoly( aRect ); + aPoly.Rotate( rPos, pFontInstance->mnOrientation); + aRect = aPoly.GetBoundRect(); + } + + intersectClipRegion( aRect ); + drawText( rPos, aStrikeout, 0, aStrikeout.getLength(), false ); + pop(); + + SetLayoutMode( nOrigTLM ); + + if ( bShadow ) + { + Font aFont = m_aCurrentPDFState.m_aFont; + aFont.SetShadow( true ); + setFont( aFont ); + updateGraphicsState(); + } +} + +void PDFWriterImpl::drawTextLine( const Point& rPos, long nWidth, FontStrikeout eStrikeout, FontLineStyle eUnderline, FontLineStyle eOverline, bool bUnderlineAbove ) +{ + if ( !nWidth || + ( ((eStrikeout == STRIKEOUT_NONE)||(eStrikeout == STRIKEOUT_DONTKNOW)) && + ((eUnderline == LINESTYLE_NONE)||(eUnderline == LINESTYLE_DONTKNOW)) && + ((eOverline == LINESTYLE_NONE)||(eOverline == LINESTYLE_DONTKNOW)) ) ) + return; + + MARK( "drawTextLine" ); + updateGraphicsState(); + + // note: units in pFontInstance are ref device pixel + const LogicalFontInstance* pFontInstance = GetFontInstance(); + Color aUnderlineColor = m_aCurrentPDFState.m_aTextLineColor; + Color aOverlineColor = m_aCurrentPDFState.m_aOverlineColor; + Color aStrikeoutColor = m_aCurrentPDFState.m_aFont.GetColor(); + bool bStrikeoutDone = false; + bool bUnderlineDone = false; + bool bOverlineDone = false; + + if ( (eStrikeout == STRIKEOUT_SLASH) || (eStrikeout == STRIKEOUT_X) ) + { + drawStrikeoutChar( rPos, nWidth, eStrikeout ); + bStrikeoutDone = true; + } + + Point aPos( rPos ); + TextAlign eAlign = m_aCurrentPDFState.m_aFont.GetAlignment(); + if( eAlign == ALIGN_TOP ) + aPos.AdjustY(HCONV( pFontInstance->mxFontMetric->GetAscent() )); + else if( eAlign == ALIGN_BOTTOM ) + aPos.AdjustY( -HCONV( pFontInstance->mxFontMetric->GetDescent() ) ); + + OStringBuffer aLine( 512 ); + // save GS + aLine.append( "q " ); + + // rotate and translate matrix + double fAngle = static_cast<double>(m_aCurrentPDFState.m_aFont.GetOrientation()) * M_PI / 1800.0; + Matrix3 aMat; + aMat.rotate( fAngle ); + aMat.translate( aPos.X(), aPos.Y() ); + m_aPages.back().appendMatrix3(aMat, aLine); + aLine.append( " cm\n" ); + + if ( aUnderlineColor.GetTransparency() != 0 ) + aUnderlineColor = aStrikeoutColor; + + if ( (eUnderline == LINESTYLE_SMALLWAVE) || + (eUnderline == LINESTYLE_WAVE) || + (eUnderline == LINESTYLE_DOUBLEWAVE) || + (eUnderline == LINESTYLE_BOLDWAVE) ) + { + drawWaveTextLine( aLine, nWidth, eUnderline, aUnderlineColor, bUnderlineAbove ); + bUnderlineDone = true; + } + + if ( (eOverline == LINESTYLE_SMALLWAVE) || + (eOverline == LINESTYLE_WAVE) || + (eOverline == LINESTYLE_DOUBLEWAVE) || + (eOverline == LINESTYLE_BOLDWAVE) ) + { + drawWaveTextLine( aLine, nWidth, eOverline, aOverlineColor, true ); + bOverlineDone = true; + } + + if ( !bUnderlineDone ) + { + drawStraightTextLine( aLine, nWidth, eUnderline, aUnderlineColor, bUnderlineAbove ); + } + + if ( !bOverlineDone ) + { + drawStraightTextLine( aLine, nWidth, eOverline, aOverlineColor, true ); + } + + if ( !bStrikeoutDone ) + { + drawStrikeoutLine( aLine, nWidth, eStrikeout, aStrikeoutColor ); + } + + aLine.append( "Q\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawPolygon( const tools::Polygon& rPoly ) +{ + MARK( "drawPolygon" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + int nPoints = rPoly.GetSize(); + OStringBuffer aLine( 20 * nPoints ); + m_aPages.back().appendPolygon( rPoly, aLine ); + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor != COL_TRANSPARENT ) + aLine.append( "B*\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( "S\n" ); + else + aLine.append( "f*\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawPolyPolygon( const tools::PolyPolygon& rPolyPoly ) +{ + MARK( "drawPolyPolygon" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + int nPolygons = rPolyPoly.Count(); + + OStringBuffer aLine( 40 * nPolygons ); + m_aPages.back().appendPolyPolygon( rPolyPoly, aLine ); + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor != COL_TRANSPARENT ) + aLine.append( "B*\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( "S\n" ); + else + aLine.append( "f*\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawTransparent( const tools::PolyPolygon& rPolyPoly, sal_uInt32 nTransparentPercent ) +{ + SAL_WARN_IF( nTransparentPercent > 100, "vcl.pdfwriter", "invalid alpha value" ); + nTransparentPercent = nTransparentPercent % 100; + + MARK( "drawTransparent" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + if( m_bIsPDF_A1 || m_aContext.Version < PDFWriter::PDFVersion::PDF_1_4 ) + { + m_aErrors.insert( m_bIsPDF_A1 ? + PDFWriter::Warning_Transparency_Omitted_PDFA : + PDFWriter::Warning_Transparency_Omitted_PDF13 ); + + drawPolyPolygon( rPolyPoly ); + return; + } + + // create XObject + m_aTransparentObjects.emplace_back( ); + // FIXME: polygons with beziers may yield incorrect bound rect + m_aTransparentObjects.back().m_aBoundRect = rPolyPoly.GetBoundRect(); + // convert rectangle to default user space + m_aPages.back().convertRect( m_aTransparentObjects.back().m_aBoundRect ); + m_aTransparentObjects.back().m_nObject = createObject(); + m_aTransparentObjects.back().m_nExtGStateObject = createObject(); + m_aTransparentObjects.back().m_fAlpha = static_cast<double>(100-nTransparentPercent) / 100.0; + m_aTransparentObjects.back().m_pContentStream.reset(new SvMemoryStream( 256, 256 )); + // create XObject's content stream + OStringBuffer aContent( 256 ); + m_aPages.back().appendPolyPolygon( rPolyPoly, aContent ); + if( m_aCurrentPDFState.m_aLineColor != COL_TRANSPARENT && + m_aCurrentPDFState.m_aFillColor != COL_TRANSPARENT ) + aContent.append( " B*\n" ); + else if( m_aCurrentPDFState.m_aLineColor != COL_TRANSPARENT ) + aContent.append( " S\n" ); + else + aContent.append( " f*\n" ); + m_aTransparentObjects.back().m_pContentStream->WriteBytes( + aContent.getStr(), aContent.getLength() ); + + OStringBuffer aObjName( 16 ); + aObjName.append( "Tr" ); + aObjName.append( m_aTransparentObjects.back().m_nObject ); + OString aTrName( aObjName.makeStringAndClear() ); + aObjName.append( "EGS" ); + aObjName.append( m_aTransparentObjects.back().m_nExtGStateObject ); + OString aExtName( aObjName.makeStringAndClear() ); + + OString aLine = + // insert XObject + "q /" + + aExtName + + " gs /" + + aTrName + + " Do Q\n"; + writeBuffer( aLine.getStr(), aLine.getLength() ); + + pushResource( ResourceKind::XObject, aTrName, m_aTransparentObjects.back().m_nObject ); + pushResource( ResourceKind::ExtGState, aExtName, m_aTransparentObjects.back().m_nExtGStateObject ); +} + +void PDFWriterImpl::pushResource( ResourceKind eKind, const OString& rResource, sal_Int32 nObject ) +{ + if( nObject >= 0 ) + { + switch( eKind ) + { + case ResourceKind::XObject: + m_aGlobalResourceDict.m_aXObjects[ rResource ] = nObject; + if( ! m_aOutputStreams.empty() ) + m_aOutputStreams.front().m_aResourceDict.m_aXObjects[ rResource ] = nObject; + break; + case ResourceKind::ExtGState: + m_aGlobalResourceDict.m_aExtGStates[ rResource ] = nObject; + if( ! m_aOutputStreams.empty() ) + m_aOutputStreams.front().m_aResourceDict.m_aExtGStates[ rResource ] = nObject; + break; + case ResourceKind::Shading: + m_aGlobalResourceDict.m_aShadings[ rResource ] = nObject; + if( ! m_aOutputStreams.empty() ) + m_aOutputStreams.front().m_aResourceDict.m_aShadings[ rResource ] = nObject; + break; + case ResourceKind::Pattern: + m_aGlobalResourceDict.m_aPatterns[ rResource ] = nObject; + if( ! m_aOutputStreams.empty() ) + m_aOutputStreams.front().m_aResourceDict.m_aPatterns[ rResource ] = nObject; + break; + } + } +} + +void PDFWriterImpl::beginRedirect( SvStream* pStream, const tools::Rectangle& rTargetRect ) +{ + push( PushFlags::ALL ); + + // force reemitting clip region inside the new stream, and + // prevent emitting an unbalanced "Q" at the start + clearClipRegion(); + // this is needed to point m_aCurrentPDFState at the pushed state + // ... but it's pointless to actually write into the "outer" stream here! + updateGraphicsState(Mode::NOWRITE); + + m_aOutputStreams.push_front( StreamRedirect() ); + m_aOutputStreams.front().m_pStream = pStream; + m_aOutputStreams.front().m_aMapMode = m_aMapMode; + + if( !rTargetRect.IsEmpty() ) + { + m_aOutputStreams.front().m_aTargetRect = + lcl_convert( m_aGraphicsStack.front().m_aMapMode, + m_aMapMode, + this, + rTargetRect ); + Point aDelta = m_aOutputStreams.front().m_aTargetRect.BottomLeft(); + long nPageHeight = pointToPixel(m_aPages[m_nCurrentPage].getHeight()); + aDelta.setY( -(nPageHeight - m_aOutputStreams.front().m_aTargetRect.Bottom()) ); + m_aMapMode.SetOrigin( m_aMapMode.GetOrigin() + aDelta ); + } + + // setup graphics state for independent object stream + + // force reemitting colors + m_aCurrentPDFState.m_aLineColor = COL_TRANSPARENT; + m_aCurrentPDFState.m_aFillColor = COL_TRANSPARENT; +} + +SvStream* PDFWriterImpl::endRedirect() +{ + SvStream* pStream = nullptr; + if( ! m_aOutputStreams.empty() ) + { + pStream = m_aOutputStreams.front().m_pStream; + m_aMapMode = m_aOutputStreams.front().m_aMapMode; + m_aOutputStreams.pop_front(); + } + + pop(); + + m_aCurrentPDFState.m_aLineColor = COL_TRANSPARENT; + m_aCurrentPDFState.m_aFillColor = COL_TRANSPARENT; + + // needed after pop() to set m_aCurrentPDFState + updateGraphicsState(Mode::NOWRITE); + + return pStream; +} + +void PDFWriterImpl::beginTransparencyGroup() +{ + updateGraphicsState(); + if( m_aContext.Version >= PDFWriter::PDFVersion::PDF_1_4 ) + beginRedirect( new SvMemoryStream( 1024, 1024 ), tools::Rectangle() ); +} + +void PDFWriterImpl::endTransparencyGroup( const tools::Rectangle& rBoundingBox, sal_uInt32 nTransparentPercent ) +{ + SAL_WARN_IF( nTransparentPercent > 100, "vcl.pdfwriter", "invalid alpha value" ); + nTransparentPercent = nTransparentPercent % 100; + + if( m_aContext.Version < PDFWriter::PDFVersion::PDF_1_4 ) + return; + + // create XObject + m_aTransparentObjects.emplace_back( ); + m_aTransparentObjects.back().m_aBoundRect = rBoundingBox; + // convert rectangle to default user space + m_aPages.back().convertRect( m_aTransparentObjects.back().m_aBoundRect ); + m_aTransparentObjects.back().m_nObject = createObject(); + m_aTransparentObjects.back().m_fAlpha = static_cast<double>(100-nTransparentPercent) / 100.0; + // get XObject's content stream + m_aTransparentObjects.back().m_pContentStream.reset( static_cast<SvMemoryStream*>(endRedirect()) ); + m_aTransparentObjects.back().m_nExtGStateObject = createObject(); + + OStringBuffer aObjName( 16 ); + aObjName.append( "Tr" ); + aObjName.append( m_aTransparentObjects.back().m_nObject ); + OString aTrName( aObjName.makeStringAndClear() ); + aObjName.append( "EGS" ); + aObjName.append( m_aTransparentObjects.back().m_nExtGStateObject ); + OString aExtName( aObjName.makeStringAndClear() ); + + OString aLine = + // insert XObject + "q /" + + aExtName + + " gs /" + + aTrName + + " Do Q\n"; + writeBuffer( aLine.getStr(), aLine.getLength() ); + + pushResource( ResourceKind::XObject, aTrName, m_aTransparentObjects.back().m_nObject ); + pushResource( ResourceKind::ExtGState, aExtName, m_aTransparentObjects.back().m_nExtGStateObject ); + +} + +void PDFWriterImpl::drawRectangle( const tools::Rectangle& rRect ) +{ + MARK( "drawRectangle" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + OStringBuffer aLine( 40 ); + m_aPages.back().appendRect( rRect, aLine ); + + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor != COL_TRANSPARENT ) + aLine.append( " B*\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( " S\n" ); + else + aLine.append( " f*\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawRectangle( const tools::Rectangle& rRect, sal_uInt32 nHorzRound, sal_uInt32 nVertRound ) +{ + MARK( "drawRectangle with rounded edges" ); + + if( !nHorzRound && !nVertRound ) + drawRectangle( rRect ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + if( nHorzRound > static_cast<sal_uInt32>(rRect.GetWidth())/2 ) + nHorzRound = rRect.GetWidth()/2; + if( nVertRound > static_cast<sal_uInt32>(rRect.GetHeight())/2 ) + nVertRound = rRect.GetHeight()/2; + + Point aPoints[16]; + const double kappa = 0.5522847498; + const sal_uInt32 kx = static_cast<sal_uInt32>((kappa*static_cast<double>(nHorzRound))+0.5); + const sal_uInt32 ky = static_cast<sal_uInt32>((kappa*static_cast<double>(nVertRound))+0.5); + + aPoints[1] = Point( rRect.TopLeft().X() + nHorzRound, rRect.TopLeft().Y() ); + aPoints[0] = Point( aPoints[1].X() - kx, aPoints[1].Y() ); + aPoints[2] = Point( rRect.TopRight().X()+1 - nHorzRound, aPoints[1].Y() ); + aPoints[3] = Point( aPoints[2].X()+kx, aPoints[2].Y() ); + + aPoints[5] = Point( rRect.TopRight().X()+1, rRect.TopRight().Y()+nVertRound ); + aPoints[4] = Point( aPoints[5].X(), aPoints[5].Y()-ky ); + aPoints[6] = Point( aPoints[5].X(), rRect.BottomRight().Y()+1 - nVertRound ); + aPoints[7] = Point( aPoints[6].X(), aPoints[6].Y()+ky ); + + aPoints[9] = Point( rRect.BottomRight().X()+1-nHorzRound, rRect.BottomRight().Y()+1 ); + aPoints[8] = Point( aPoints[9].X()+kx, aPoints[9].Y() ); + aPoints[10] = Point( rRect.BottomLeft().X() + nHorzRound, aPoints[9].Y() ); + aPoints[11] = Point( aPoints[10].X()-kx, aPoints[10].Y() ); + + aPoints[13] = Point( rRect.BottomLeft().X(), rRect.BottomLeft().Y()+1-nVertRound ); + aPoints[12] = Point( aPoints[13].X(), aPoints[13].Y()+ky ); + aPoints[14] = Point( rRect.TopLeft().X(), rRect.TopLeft().Y()+nVertRound ); + aPoints[15] = Point( aPoints[14].X(), aPoints[14].Y()-ky ); + + OStringBuffer aLine( 80 ); + m_aPages.back().appendPoint( aPoints[1], aLine ); + aLine.append( " m " ); + m_aPages.back().appendPoint( aPoints[2], aLine ); + aLine.append( " l " ); + m_aPages.back().appendPoint( aPoints[3], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[4], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[5], aLine ); + aLine.append( " c\n" ); + m_aPages.back().appendPoint( aPoints[6], aLine ); + aLine.append( " l " ); + m_aPages.back().appendPoint( aPoints[7], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[8], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[9], aLine ); + aLine.append( " c\n" ); + m_aPages.back().appendPoint( aPoints[10], aLine ); + aLine.append( " l " ); + m_aPages.back().appendPoint( aPoints[11], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[12], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[13], aLine ); + aLine.append( " c\n" ); + m_aPages.back().appendPoint( aPoints[14], aLine ); + aLine.append( " l " ); + m_aPages.back().appendPoint( aPoints[15], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[0], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[1], aLine ); + aLine.append( " c " ); + + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor != COL_TRANSPARENT ) + aLine.append( "b*\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( "s\n" ); + else + aLine.append( "f*\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawEllipse( const tools::Rectangle& rRect ) +{ + MARK( "drawEllipse" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + Point aPoints[12]; + const double kappa = 0.5522847498; + const sal_uInt32 kx = static_cast<sal_uInt32>((kappa*static_cast<double>(rRect.GetWidth())/2.0)+0.5); + const sal_uInt32 ky = static_cast<sal_uInt32>((kappa*static_cast<double>(rRect.GetHeight())/2.0)+0.5); + + aPoints[1] = Point( rRect.TopLeft().X() + rRect.GetWidth()/2, rRect.TopLeft().Y() ); + aPoints[0] = Point( aPoints[1].X() - kx, aPoints[1].Y() ); + aPoints[2] = Point( aPoints[1].X() + kx, aPoints[1].Y() ); + + aPoints[4] = Point( rRect.TopRight().X()+1, rRect.TopRight().Y() + rRect.GetHeight()/2 ); + aPoints[3] = Point( aPoints[4].X(), aPoints[4].Y() - ky ); + aPoints[5] = Point( aPoints[4].X(), aPoints[4].Y() + ky ); + + aPoints[7] = Point( rRect.BottomLeft().X() + rRect.GetWidth()/2, rRect.BottomLeft().Y()+1 ); + aPoints[6] = Point( aPoints[7].X() + kx, aPoints[7].Y() ); + aPoints[8] = Point( aPoints[7].X() - kx, aPoints[7].Y() ); + + aPoints[10] = Point( rRect.TopLeft().X(), rRect.TopLeft().Y() + rRect.GetHeight()/2 ); + aPoints[9] = Point( aPoints[10].X(), aPoints[10].Y() + ky ); + aPoints[11] = Point( aPoints[10].X(), aPoints[10].Y() - ky ); + + OStringBuffer aLine( 80 ); + m_aPages.back().appendPoint( aPoints[1], aLine ); + aLine.append( " m " ); + m_aPages.back().appendPoint( aPoints[2], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[3], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[4], aLine ); + aLine.append( " c\n" ); + m_aPages.back().appendPoint( aPoints[5], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[6], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[7], aLine ); + aLine.append( " c\n" ); + m_aPages.back().appendPoint( aPoints[8], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[9], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[10], aLine ); + aLine.append( " c\n" ); + m_aPages.back().appendPoint( aPoints[11], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[0], aLine ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( aPoints[1], aLine ); + aLine.append( " c " ); + + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor != COL_TRANSPARENT ) + aLine.append( "b*\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( "s\n" ); + else + aLine.append( "f*\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +static double calcAngle( const tools::Rectangle& rRect, const Point& rPoint ) +{ + Point aOrigin((rRect.Left()+rRect.Right()+1)/2, + (rRect.Top()+rRect.Bottom()+1)/2); + Point aPoint = rPoint - aOrigin; + + double fX = static_cast<double>(aPoint.X()); + double fY = static_cast<double>(-aPoint.Y()); + + if ((rRect.GetHeight() == 0) || (rRect.GetWidth() == 0)) + throw o3tl::divide_by_zero(); + + if( rRect.GetWidth() > rRect.GetHeight() ) + fY = fY*(static_cast<double>(rRect.GetWidth())/static_cast<double>(rRect.GetHeight())); + else if( rRect.GetHeight() > rRect.GetWidth() ) + fX = fX*(static_cast<double>(rRect.GetHeight())/static_cast<double>(rRect.GetWidth())); + return atan2( fY, fX ); +} + +void PDFWriterImpl::drawArc( const tools::Rectangle& rRect, const Point& rStart, const Point& rStop, bool bWithPie, bool bWithChord ) +{ + MARK( "drawArc" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor == COL_TRANSPARENT ) + return; + + // calculate start and stop angles + const double fStartAngle = calcAngle( rRect, rStart ); + double fStopAngle = calcAngle( rRect, rStop ); + while( fStopAngle < fStartAngle ) + fStopAngle += 2.0*M_PI; + const int nFragments = static_cast<int>((fStopAngle-fStartAngle)/(M_PI/2.0))+1; + const double fFragmentDelta = (fStopAngle-fStartAngle)/static_cast<double>(nFragments); + const double kappa = fabs( 4.0 * (1.0-cos(fFragmentDelta/2.0))/sin(fFragmentDelta/2.0) / 3.0); + const double halfWidth = static_cast<double>(rRect.GetWidth())/2.0; + const double halfHeight = static_cast<double>(rRect.GetHeight())/2.0; + + const Point aCenter( (rRect.Left()+rRect.Right()+1)/2, + (rRect.Top()+rRect.Bottom()+1)/2 ); + + OStringBuffer aLine( 30*nFragments ); + Point aPoint( static_cast<int>(halfWidth * cos(fStartAngle) ), + -static_cast<int>(halfHeight * sin(fStartAngle) ) ); + aPoint += aCenter; + m_aPages.back().appendPoint( aPoint, aLine ); + aLine.append( " m " ); + if( !basegfx::fTools::equal(fStartAngle, fStopAngle) ) + { + for( int i = 0; i < nFragments; i++ ) + { + const double fStartFragment = fStartAngle + static_cast<double>(i)*fFragmentDelta; + const double fStopFragment = fStartFragment + fFragmentDelta; + aPoint = Point( static_cast<int>(halfWidth * (cos(fStartFragment) - kappa*sin(fStartFragment) ) ), + -static_cast<int>(halfHeight * (sin(fStartFragment) + kappa*cos(fStartFragment) ) ) ); + aPoint += aCenter; + m_aPages.back().appendPoint( aPoint, aLine ); + aLine.append( ' ' ); + + aPoint = Point( static_cast<int>(halfWidth * (cos(fStopFragment) + kappa*sin(fStopFragment) ) ), + -static_cast<int>(halfHeight * (sin(fStopFragment) - kappa*cos(fStopFragment) ) ) ); + aPoint += aCenter; + m_aPages.back().appendPoint( aPoint, aLine ); + aLine.append( ' ' ); + + aPoint = Point( static_cast<int>(halfWidth * cos(fStopFragment) ), + -static_cast<int>(halfHeight * sin(fStopFragment) ) ); + aPoint += aCenter; + m_aPages.back().appendPoint( aPoint, aLine ); + aLine.append( " c\n" ); + } + } + if( bWithChord || bWithPie ) + { + if( bWithPie ) + { + m_aPages.back().appendPoint( aCenter, aLine ); + aLine.append( " l " ); + } + aLine.append( "h " ); + } + if( ! bWithChord && ! bWithPie ) + aLine.append( "S\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT && + m_aGraphicsStack.front().m_aFillColor != COL_TRANSPARENT ) + aLine.append( "B*\n" ); + else if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( "S\n" ); + else + aLine.append( "f*\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawPolyLine( const tools::Polygon& rPoly ) +{ + MARK( "drawPolyLine" ); + + sal_uInt16 nPoints = rPoly.GetSize(); + if( nPoints < 2 ) + return; + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT ) + return; + + OStringBuffer aLine( 20 * nPoints ); + m_aPages.back().appendPolygon( rPoly, aLine, rPoly[0] == rPoly[nPoints-1] ); + aLine.append( "S\n" ); + + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawPolyLine( const tools::Polygon& rPoly, const LineInfo& rInfo ) +{ + MARK( "drawPolyLine with LineInfo" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT ) + return; + + OStringBuffer aLine; + aLine.append( "q " ); + if( m_aPages.back().appendLineInfo( rInfo, aLine ) ) + { + writeBuffer( aLine.getStr(), aLine.getLength() ); + drawPolyLine( rPoly ); + writeBuffer( "Q\n", 2 ); + } + else + { + PDFWriter::ExtLineInfo aInfo; + convertLineInfoToExtLineInfo( rInfo, aInfo ); + drawPolyLine( rPoly, aInfo ); + } +} + +void PDFWriterImpl::convertLineInfoToExtLineInfo( const LineInfo& rIn, PDFWriter::ExtLineInfo& rOut ) +{ + SAL_WARN_IF( rIn.GetStyle() != LineStyle::Dash, "vcl.pdfwriter", "invalid conversion" ); + rOut.m_fLineWidth = rIn.GetWidth(); + rOut.m_fTransparency = 0.0; + rOut.m_eCap = PDFWriter::capButt; + rOut.m_eJoin = PDFWriter::joinMiter; + rOut.m_fMiterLimit = 10; + rOut.m_aDashArray.clear(); + + // add DashDot to DashArray + const int nDashes = rIn.GetDashCount(); + const int nDashLen = rIn.GetDashLen(); + const int nDistance = rIn.GetDistance(); + + for( int n = 0; n < nDashes; n++ ) + { + rOut.m_aDashArray.push_back( nDashLen ); + rOut.m_aDashArray.push_back( nDistance ); + } + const int nDots = rIn.GetDotCount(); + const int nDotLen = rIn.GetDotLen(); + + for( int n = 0; n < nDots; n++ ) + { + rOut.m_aDashArray.push_back( nDotLen ); + rOut.m_aDashArray.push_back( nDistance ); + } + + // add LineJoin + switch(rIn.GetLineJoin()) + { + case basegfx::B2DLineJoin::Bevel : + { + rOut.m_eJoin = PDFWriter::joinBevel; + break; + } + // Pdf has no 'none' lineJoin, default is miter + case basegfx::B2DLineJoin::NONE : + case basegfx::B2DLineJoin::Miter : + { + rOut.m_eJoin = PDFWriter::joinMiter; + break; + } + case basegfx::B2DLineJoin::Round : + { + rOut.m_eJoin = PDFWriter::joinRound; + break; + } + } + + // add LineCap + switch(rIn.GetLineCap()) + { + default: /* css::drawing::LineCap_BUTT */ + { + rOut.m_eCap = PDFWriter::capButt; + break; + } + case css::drawing::LineCap_ROUND: + { + rOut.m_eCap = PDFWriter::capRound; + break; + } + case css::drawing::LineCap_SQUARE: + { + rOut.m_eCap = PDFWriter::capSquare; + break; + } + } +} + +void PDFWriterImpl::drawPolyLine( const tools::Polygon& rPoly, const PDFWriter::ExtLineInfo& rInfo ) +{ + MARK( "drawPolyLine with ExtLineInfo" ); + + updateGraphicsState(); + + if( m_aGraphicsStack.front().m_aLineColor == COL_TRANSPARENT ) + return; + + if( rInfo.m_fTransparency >= 1.0 ) + return; + + if( rInfo.m_fTransparency != 0.0 ) + beginTransparencyGroup(); + + OStringBuffer aLine; + aLine.append( "q " ); + m_aPages.back().appendMappedLength( rInfo.m_fLineWidth, aLine ); + aLine.append( " w" ); + if( rInfo.m_aDashArray.size() < 10 ) // implementation limit of acrobat reader + { + switch( rInfo.m_eCap ) + { + default: + case PDFWriter::capButt: aLine.append( " 0 J" );break; + case PDFWriter::capRound: aLine.append( " 1 J" );break; + case PDFWriter::capSquare: aLine.append( " 2 J" );break; + } + switch( rInfo.m_eJoin ) + { + default: + case PDFWriter::joinMiter: + { + double fLimit = rInfo.m_fMiterLimit; + if( rInfo.m_fLineWidth < rInfo.m_fMiterLimit ) + fLimit = fLimit / rInfo.m_fLineWidth; + if( fLimit < 1.0 ) + fLimit = 1.0; + aLine.append( " 0 j " ); + appendDouble( fLimit, aLine ); + aLine.append( " M" ); + } + break; + case PDFWriter::joinRound: aLine.append( " 1 j" );break; + case PDFWriter::joinBevel: aLine.append( " 2 j" );break; + } + if( !rInfo.m_aDashArray.empty() ) + { + aLine.append( " [ " ); + for (auto const& dash : rInfo.m_aDashArray) + { + m_aPages.back().appendMappedLength( dash, aLine ); + aLine.append( ' ' ); + } + aLine.append( "] 0 d" ); + } + aLine.append( "\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + drawPolyLine( rPoly ); + } + else + { + basegfx::B2DPolygon aPoly(rPoly.getB2DPolygon()); + basegfx::B2DPolyPolygon aPolyPoly; + + basegfx::utils::applyLineDashing(aPoly, rInfo.m_aDashArray, &aPolyPoly); + + // Old applyLineDashing subdivided the polygon. New one will create bezier curve segments. + // To mimic old behaviour, apply subdivide here. If beziers shall be written (better quality) + // this line needs to be removed and the loop below adapted accordingly + aPolyPoly = basegfx::utils::adaptiveSubdivideByAngle(aPolyPoly); + + const sal_uInt32 nPolygonCount(aPolyPoly.count()); + + for( sal_uInt32 nPoly = 0; nPoly < nPolygonCount; nPoly++ ) + { + aLine.append( (nPoly != 0 && (nPoly & 7) == 0) ? "\n" : " " ); + aPoly = aPolyPoly.getB2DPolygon( nPoly ); + const sal_uInt32 nPointCount(aPoly.count()); + + if(nPointCount) + { + const sal_uInt32 nEdgeCount(aPoly.isClosed() ? nPointCount : nPointCount - 1); + basegfx::B2DPoint aCurrent(aPoly.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + if( a > 0 ) + aLine.append( " " ); + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const basegfx::B2DPoint aNext(aPoly.getB2DPoint(nNextIndex)); + + m_aPages.back().appendPoint( Point( FRound(aCurrent.getX()), + FRound(aCurrent.getY()) ), + aLine ); + aLine.append( " m " ); + m_aPages.back().appendPoint( Point( FRound(aNext.getX()), + FRound(aNext.getY()) ), + aLine ); + aLine.append( " l" ); + + // prepare next edge + aCurrent = aNext; + } + } + } + aLine.append( " S " ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + } + writeBuffer( "Q\n", 2 ); + + if( rInfo.m_fTransparency != 0.0 ) + { + // FIXME: actually this may be incorrect with bezier polygons + tools::Rectangle aBoundRect( rPoly.GetBoundRect() ); + // avoid clipping with thick lines + if( rInfo.m_fLineWidth > 0.0 ) + { + sal_Int32 nLW = sal_Int32(rInfo.m_fLineWidth); + aBoundRect.AdjustTop( -nLW ); + aBoundRect.AdjustLeft( -nLW ); + aBoundRect.AdjustRight(nLW ); + aBoundRect.AdjustBottom(nLW ); + } + endTransparencyGroup( aBoundRect, static_cast<sal_uInt16>(100.0*rInfo.m_fTransparency) ); + } +} + +void PDFWriterImpl::drawPixel( const Point& rPoint, const Color& rColor ) +{ + MARK( "drawPixel" ); + + Color aColor = ( rColor == COL_TRANSPARENT ? m_aGraphicsStack.front().m_aLineColor : rColor ); + + if( aColor == COL_TRANSPARENT ) + return; + + // pixels are drawn in line color, so have to set + // the nonstroking color to line color + Color aOldFillColor = m_aGraphicsStack.front().m_aFillColor; + setFillColor( aColor ); + + updateGraphicsState(); + + OStringBuffer aLine( 20 ); + m_aPages.back().appendPoint( rPoint, aLine ); + aLine.append( ' ' ); + appendDouble( 1.0/double(GetDPIX()), aLine ); + aLine.append( ' ' ); + appendDouble( 1.0/double(GetDPIY()), aLine ); + aLine.append( " re f\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + + setFillColor( aOldFillColor ); +} + +void PDFWriterImpl::writeTransparentObject( TransparencyEmit& rObject ) +{ + CHECK_RETURN2( updateObject( rObject.m_nObject ) ); + + bool bFlateFilter = compressStream( rObject.m_pContentStream.get() ); + sal_uLong nSize = rObject.m_pContentStream->TellEnd(); + rObject.m_pContentStream->Seek( STREAM_SEEK_TO_BEGIN ); + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::writeTransparentObject" ); + } + OStringBuffer aLine( 512 ); + CHECK_RETURN2( updateObject( rObject.m_nObject ) ); + aLine.append( rObject.m_nObject ); + aLine.append( " 0 obj\n" + "<</Type/XObject\n" + "/Subtype/Form\n" + "/BBox[ " ); + appendFixedInt( rObject.m_aBoundRect.Left(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rObject.m_aBoundRect.Top(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rObject.m_aBoundRect.Right(), aLine ); + aLine.append( ' ' ); + appendFixedInt( rObject.m_aBoundRect.Bottom()+1, aLine ); + aLine.append( " ]\n" ); + if( ! rObject.m_pSoftMaskStream ) + { + if( ! m_bIsPDF_A1 ) + { + // 7.8.3 Resource dicts are required for content streams + aLine.append( "/Resources " ); + aLine.append( getResourceDictObj() ); + aLine.append( " 0 R\n" ); + + aLine.append( "/Group<</S/Transparency/CS/DeviceRGB/K true>>\n" ); + } + } + + aLine.append( "/Length " ); + aLine.append( static_cast<sal_Int32>(nSize) ); + aLine.append( "\n" ); + if( bFlateFilter ) + aLine.append( "/Filter/FlateDecode\n" ); + aLine.append( ">>\n" + "stream\n" ); + CHECK_RETURN2( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + checkAndEnableStreamEncryption( rObject.m_nObject ); + CHECK_RETURN2( writeBuffer( rObject.m_pContentStream->GetData(), nSize ) ); + disableStreamEncryption(); + aLine.setLength( 0 ); + aLine.append( "\n" + "endstream\n" + "endobj\n\n" ); + CHECK_RETURN2( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + // write ExtGState dict for this XObject + aLine.setLength( 0 ); + aLine.append( rObject.m_nExtGStateObject ); + aLine.append( " 0 obj\n" + "<<" ); + if( ! rObject.m_pSoftMaskStream ) + { + if( m_bIsPDF_A1 ) + { + aLine.append( "/CA 1.0/ca 1.0" ); + m_aErrors.insert( PDFWriter::Warning_Transparency_Omitted_PDFA ); + } + else + { + aLine.append( "/CA " ); + appendDouble( rObject.m_fAlpha, aLine ); + aLine.append( "\n" + " /ca " ); + appendDouble( rObject.m_fAlpha, aLine ); + } + aLine.append( "\n" ); + } + else + { + if( m_bIsPDF_A1 ) + { + aLine.append( "/SMask/None" ); + m_aErrors.insert( PDFWriter::Warning_Transparency_Omitted_PDFA ); + } + else + { + sal_Int32 nMaskSize = static_cast<sal_Int32>(rObject.m_pSoftMaskStream->TellEnd()); + rObject.m_pSoftMaskStream->Seek( STREAM_SEEK_TO_BEGIN ); + sal_Int32 nMaskObject = createObject(); + aLine.append( "/SMask<</Type/Mask/S/Luminosity/G " ); + aLine.append( nMaskObject ); + aLine.append( " 0 R>>\n" ); + + OStringBuffer aMask; + aMask.append( nMaskObject ); + aMask.append( " 0 obj\n" + "<</Type/XObject\n" + "/Subtype/Form\n" + "/BBox[" ); + appendFixedInt( rObject.m_aBoundRect.Left(), aMask ); + aMask.append( ' ' ); + appendFixedInt( rObject.m_aBoundRect.Top(), aMask ); + aMask.append( ' ' ); + appendFixedInt( rObject.m_aBoundRect.Right(), aMask ); + aMask.append( ' ' ); + appendFixedInt( rObject.m_aBoundRect.Bottom()+1, aMask ); + aMask.append( "]\n" ); + + // 7.8.3 Resource dicts are required for content streams + aMask.append( "/Resources " ); + aMask.append( getResourceDictObj() ); + aMask.append( " 0 R\n" ); + + aMask.append( "/Group<</S/Transparency/CS/DeviceRGB>>\n" ); + aMask.append( "/Length " ); + aMask.append( nMaskSize ); + aMask.append( ">>\n" + "stream\n" ); + CHECK_RETURN2( updateObject( nMaskObject ) ); + checkAndEnableStreamEncryption( nMaskObject ); + CHECK_RETURN2( writeBuffer( aMask.getStr(), aMask.getLength() ) ); + CHECK_RETURN2( writeBuffer( rObject.m_pSoftMaskStream->GetData(), nMaskSize ) ); + disableStreamEncryption(); + aMask.setLength( 0 ); + aMask.append( "\nendstream\n" + "endobj\n\n" ); + CHECK_RETURN2( writeBuffer( aMask.getStr(), aMask.getLength() ) ); + } + } + aLine.append( ">>\n" + "endobj\n\n" ); + CHECK_RETURN2( updateObject( rObject.m_nExtGStateObject ) ); + CHECK_RETURN2( writeBuffer( aLine.getStr(), aLine.getLength() ) ); +} + +bool PDFWriterImpl::writeGradientFunction( GradientEmit const & rObject ) +{ + // LO internal gradient -> PDF shading type: + // * GradientStyle::Linear: axial shading, using sampled-function with 2 samples + // [t=0:colorStart, t=1:colorEnd] + // * GradientStyle::Axial: axial shading, using sampled-function with 3 samples + // [t=0:colorEnd, t=0.5:colorStart, t=1:colorEnd] + // * other styles: function shading with aSize.Width() * aSize.Height() samples + sal_Int32 nFunctionObject = createObject(); + CHECK_RETURN( updateObject( nFunctionObject ) ); + + ScopedVclPtrInstance< VirtualDevice > aDev; + aDev->SetOutputSizePixel( rObject.m_aSize ); + aDev->SetMapMode( MapMode( MapUnit::MapPixel ) ); + if( m_aContext.ColorMode == PDFWriter::DrawGreyscale ) + aDev->SetDrawMode( aDev->GetDrawMode() | + ( DrawModeFlags::GrayLine | DrawModeFlags::GrayFill | DrawModeFlags::GrayText | + DrawModeFlags::GrayBitmap | DrawModeFlags::GrayGradient ) ); + aDev->DrawGradient( tools::Rectangle( Point( 0, 0 ), rObject.m_aSize ), rObject.m_aGradient ); + + Bitmap aSample = aDev->GetBitmap( Point( 0, 0 ), rObject.m_aSize ); + Bitmap::ScopedReadAccess pAccess(aSample); + + Size aSize = aSample.GetSizePixel(); + + sal_Int32 nStreamLengthObject = createObject(); + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::writeGradientFunction" ); + } + OStringBuffer aLine( 120 ); + aLine.append( nFunctionObject ); + aLine.append( " 0 obj\n" + "<</FunctionType 0\n"); + switch (rObject.m_aGradient.GetStyle()) + { + case GradientStyle::Linear: + case GradientStyle::Axial: + aLine.append("/Domain[ 0 1]\n"); + break; + default: + aLine.append("/Domain[ 0 1 0 1]\n"); + } + aLine.append("/Size[ " ); + switch (rObject.m_aGradient.GetStyle()) + { + case GradientStyle::Linear: + aLine.append('2'); + break; + case GradientStyle::Axial: + aLine.append('3'); + break; + default: + aLine.append( static_cast<sal_Int32>(aSize.Width()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(aSize.Height()) ); + } + aLine.append( " ]\n" + "/BitsPerSample 8\n" + "/Range[ 0 1 0 1 0 1 ]\n" + "/Order 3\n" + "/Length " ); + aLine.append( nStreamLengthObject ); + if (!g_bDebugDisableCompression) + aLine.append( " 0 R\n" + "/Filter/FlateDecode" + ">>\n" + "stream\n" ); + else + aLine.append( " 0 R\n" + ">>\n" + "stream\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + sal_uInt64 nStartStreamPos = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nStartStreamPos)) ); + + checkAndEnableStreamEncryption( nFunctionObject ); + beginCompression(); + sal_uInt8 aCol[3]; + switch (rObject.m_aGradient.GetStyle()) + { + case GradientStyle::Axial: + aCol[0] = rObject.m_aGradient.GetEndColor().GetRed(); + aCol[1] = rObject.m_aGradient.GetEndColor().GetGreen(); + aCol[2] = rObject.m_aGradient.GetEndColor().GetBlue(); + CHECK_RETURN( writeBuffer( aCol, 3 ) ); + [[fallthrough]]; + case GradientStyle::Linear: + { + aCol[0] = rObject.m_aGradient.GetStartColor().GetRed(); + aCol[1] = rObject.m_aGradient.GetStartColor().GetGreen(); + aCol[2] = rObject.m_aGradient.GetStartColor().GetBlue(); + CHECK_RETURN( writeBuffer( aCol, 3 ) ); + + aCol[0] = rObject.m_aGradient.GetEndColor().GetRed(); + aCol[1] = rObject.m_aGradient.GetEndColor().GetGreen(); + aCol[2] = rObject.m_aGradient.GetEndColor().GetBlue(); + CHECK_RETURN( writeBuffer( aCol, 3 ) ); + break; + } + default: + for( int y = aSize.Height()-1; y >= 0; y-- ) + { + for( long x = 0; x < aSize.Width(); x++ ) + { + BitmapColor aColor = pAccess->GetColor( y, x ); + aCol[0] = aColor.GetRed(); + aCol[1] = aColor.GetGreen(); + aCol[2] = aColor.GetBlue(); + CHECK_RETURN( writeBuffer( aCol, 3 ) ); + } + } + } + endCompression(); + disableStreamEncryption(); + + sal_uInt64 nEndStreamPos = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nEndStreamPos)) ); + + aLine.setLength( 0 ); + aLine.append( "\nendstream\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + // write stream length + CHECK_RETURN( updateObject( nStreamLengthObject ) ); + aLine.setLength( 0 ); + aLine.append( nStreamLengthObject ); + aLine.append( " 0 obj\n" ); + aLine.append( static_cast<sal_Int64>(nEndStreamPos-nStartStreamPos) ); + aLine.append( "\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + CHECK_RETURN( updateObject( rObject.m_nObject ) ); + aLine.setLength( 0 ); + aLine.append( rObject.m_nObject ); + aLine.append( " 0 obj\n"); + switch (rObject.m_aGradient.GetStyle()) + { + case GradientStyle::Linear: + case GradientStyle::Axial: + aLine.append("<</ShadingType 2\n"); + break; + default: + aLine.append("<</ShadingType 1\n"); + } + aLine.append("/ColorSpace/DeviceRGB\n" + "/AntiAlias true\n"); + + // Determination of shading axis + // See: OutputDevice::ImplDrawLinearGradient for reference + tools::Rectangle aRect; + aRect.SetLeft(0); + aRect.SetTop(0); + aRect.SetRight( aSize.Width() ); + aRect.SetBottom( aSize.Height() ); + + tools::Rectangle aBoundRect; + Point aCenter; + sal_uInt16 nAngle = rObject.m_aGradient.GetAngle() % 3600; + rObject.m_aGradient.GetBoundRect( aRect, aBoundRect, aCenter ); + + const bool bLinear = (rObject.m_aGradient.GetStyle() == GradientStyle::Linear); + double fBorder = aBoundRect.GetHeight() * rObject.m_aGradient.GetBorder() / 100.0; + if ( !bLinear ) + { + fBorder /= 2.0; + } + + aBoundRect.AdjustBottom( -fBorder ); + if (!bLinear) + { + aBoundRect.AdjustTop(fBorder ); + } + + switch (rObject.m_aGradient.GetStyle()) + { + case GradientStyle::Linear: + case GradientStyle::Axial: + { + aLine.append("/Domain[ 0 1 ]\n" + "/Coords[ " ); + tools::Polygon aPoly( 2 ); + aPoly[0] = aBoundRect.BottomCenter(); + aPoly[1] = aBoundRect.TopCenter(); + aPoly.Rotate( aCenter, 3600 - nAngle ); + + aLine.append( static_cast<sal_Int32>(aPoly[0].X()) ); + aLine.append( " " ); + aLine.append( static_cast<sal_Int32>(aPoly[0].Y()) ); + aLine.append( " " ); + aLine.append( static_cast<sal_Int32>(aPoly[1].X())); + aLine.append( " "); + aLine.append( static_cast<sal_Int32>(aPoly[1].Y())); + aLine.append( " ]\n"); + aLine.append("/Extend [true true]\n"); + break; + } + default: + aLine.append("/Domain[ 0 1 0 1 ]\n" + "/Matrix[ " ); + aLine.append( static_cast<sal_Int32>(aSize.Width()) ); + aLine.append( " 0 0 " ); + aLine.append( static_cast<sal_Int32>(aSize.Height()) ); + aLine.append( " 0 0 ]\n"); + } + aLine.append("/Function " ); + aLine.append( nFunctionObject ); + aLine.append( " 0 R\n" + ">>\n" + "endobj\n\n" ); + return writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::writeJPG( JPGEmit& rObject ) +{ + if (!rObject.m_aReferenceXObject.m_aPDFData.empty() && !m_aContext.UseReferenceXObject) + { + writeReferenceXObject(rObject.m_aReferenceXObject); + return; + } + + CHECK_RETURN2( rObject.m_pStream ); + CHECK_RETURN2( updateObject( rObject.m_nObject ) ); + + sal_Int32 nLength = rObject.m_pStream->TellEnd(); + rObject.m_pStream->Seek( STREAM_SEEK_TO_BEGIN ); + + sal_Int32 nMaskObject = 0; + if( !!rObject.m_aMask ) + { + if( rObject.m_aMask.GetBitCount() == 1 || + ( rObject.m_aMask.GetBitCount() == 8 && m_aContext.Version >= PDFWriter::PDFVersion::PDF_1_4 && !m_bIsPDF_A1 ) + ) + { + nMaskObject = createObject(); + } + else if( m_bIsPDF_A1 ) + m_aErrors.insert( PDFWriter::Warning_Transparency_Omitted_PDFA ); + else if( m_aContext.Version < PDFWriter::PDFVersion::PDF_1_4 ) + m_aErrors.insert( PDFWriter::Warning_Transparency_Omitted_PDF13 ); + + } + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::writeJPG" ); + } + + OStringBuffer aLine(200); + aLine.append( rObject.m_nObject ); + aLine.append( " 0 obj\n" + "<</Type/XObject/Subtype/Image/Width " ); + aLine.append( static_cast<sal_Int32>(rObject.m_aID.m_aPixelSize.Width()) ); + aLine.append( " /Height " ); + aLine.append( static_cast<sal_Int32>(rObject.m_aID.m_aPixelSize.Height()) ); + aLine.append( " /BitsPerComponent 8 " ); + if( rObject.m_bTrueColor ) + aLine.append( "/ColorSpace/DeviceRGB" ); + else + aLine.append( "/ColorSpace/DeviceGray" ); + aLine.append( "/Filter/DCTDecode/Length " ); + aLine.append( nLength ); + if( nMaskObject ) + { + aLine.append( rObject.m_aMask.GetBitCount() == 1 ? " /Mask " : " /SMask " ); + aLine.append( nMaskObject ); + aLine.append( " 0 R " ); + } + aLine.append( ">>\nstream\n" ); + CHECK_RETURN2( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + checkAndEnableStreamEncryption( rObject.m_nObject ); + CHECK_RETURN2( writeBuffer( rObject.m_pStream->GetData(), nLength ) ); + disableStreamEncryption(); + + aLine.setLength( 0 ); + aLine.append( "\nendstream\nendobj\n\n" ); + CHECK_RETURN2( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + if( nMaskObject ) + { + BitmapEmit aEmit; + aEmit.m_nObject = nMaskObject; + if( rObject.m_aMask.GetBitCount() == 1 ) + aEmit.m_aBitmap = BitmapEx( rObject.m_aMask, rObject.m_aMask ); + else if( rObject.m_aMask.GetBitCount() == 8 ) + aEmit.m_aBitmap = BitmapEx( rObject.m_aMask, AlphaMask( rObject.m_aMask ) ); + writeBitmapObject( aEmit, true ); + } + + writeReferenceXObject(rObject.m_aReferenceXObject); +} + +sal_Int32 PDFWriterImpl::copyExternalResource(SvMemoryStream& rDocBuffer, filter::PDFObjectElement& rObject, std::map<sal_Int32, sal_Int32>& rCopiedResources) +{ + auto it = rCopiedResources.find(rObject.GetObjectValue()); + if (it != rCopiedResources.end()) + // This resource was already copied once, nothing to do. + return it->second; + + sal_Int32 nObject = createObject(); + // Remember what is the ID of this object in our output. + rCopiedResources[rObject.GetObjectValue()] = nObject; + SAL_INFO("vcl.pdfwriter", "PDFWriterImpl::copyExternalResource: " << rObject.GetObjectValue() << " -> " << nObject); + + OStringBuffer aLine; + aLine.append(nObject); + aLine.append(" 0 obj\n"); + if (rObject.GetDictionary()) + { + aLine.append("<<"); + + // Complex case: can't copy the dictionary byte array as is, as it may contain references. + bool bDone = false; + sal_uInt64 nCopyStart = 0; + for (auto pReference : rObject.GetDictionaryReferences()) + { + if (pReference) + { + filter::PDFObjectElement* pReferenced = pReference->LookupObject(); + if (pReferenced) + { + // Copy the referenced object. + sal_Int32 nRef = copyExternalResource(rDocBuffer, *pReferenced, rCopiedResources); + + sal_uInt64 nReferenceStart = pReference->GetObjectElement().GetLocation(); + sal_uInt64 nReferenceEnd = pReference->GetOffset(); + sal_uInt64 nOffset = 0; + if (nCopyStart == 0) + // Dict start -> reference start. + nOffset = rObject.GetDictionaryOffset(); + else + // Previous reference end -> reference start. + nOffset = nCopyStart; + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + nOffset, nReferenceStart - nOffset); + // Write the updated reference. + aLine.append(" "); + aLine.append(nRef); + aLine.append(" 0 R"); + // Start copying here next time. + nCopyStart = nReferenceEnd; + + bDone = true; + } + } + } + + if (bDone) + { + // Copy the last part here, in the complex case. + sal_uInt64 nDictEnd = rObject.GetDictionaryOffset() + rObject.GetDictionaryLength(); + const sal_Int32 nLen = nDictEnd - nCopyStart; + if (nLen < 0) + SAL_WARN("vcl.pdfwriter", "copyExternalResource() failed"); + else + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + nCopyStart, nLen); + } + else + // Can copy it as-is. + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + rObject.GetDictionaryOffset(), rObject.GetDictionaryLength()); + + aLine.append(">>\n"); + } + + if (filter::PDFStreamElement* pStream = rObject.GetStream()) + { + aLine.append("stream\n"); + SvMemoryStream& rStream = pStream->GetMemory(); + aLine.append(static_cast<const char*>(rStream.GetData()), rStream.GetSize()); + aLine.append("\nendstream\n"); + } + + if (filter::PDFArrayElement* pArray = rObject.GetArray()) + { + aLine.append("["); + + const std::vector<filter::PDFElement*>& rElements = pArray->GetElements(); + bool bDone = false; + // Complex case: can't copy the array byte array as is, as it may contain references. + sal_uInt64 nCopyStart = 0; + for (const auto pElement : rElements) + { + auto pReference = dynamic_cast<filter::PDFReferenceElement*>(pElement); + if (pReference) + { + filter::PDFObjectElement* pReferenced = pReference->LookupObject(); + if (pReferenced) + { + // Copy the referenced object. + sal_Int32 nRef = copyExternalResource(rDocBuffer, *pReferenced, rCopiedResources); + + sal_uInt64 nReferenceStart = pReference->GetObjectElement().GetLocation(); + sal_uInt64 nReferenceEnd = pReference->GetOffset(); + sal_uInt64 nOffset = 0; + if (nCopyStart == 0) + // Array start -> reference start. + nOffset = rObject.GetArrayOffset(); + else + // Previous reference end -> reference start. + nOffset = nCopyStart; + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + nOffset, nReferenceStart - nOffset); + + // Write the updated reference. + aLine.append(" "); + aLine.append(nRef); + aLine.append(" 0 R"); + // Start copying here next time. + nCopyStart = nReferenceEnd; + + bDone = true; + } + } + } + + if (bDone) + { + // Copy the last part here, in the complex case. + sal_uInt64 nArrEnd = rObject.GetArrayOffset() + rObject.GetArrayLength(); + const sal_Int32 nLen = nArrEnd - nCopyStart; + if (nLen < 0) + SAL_WARN("vcl.pdfwriter", "copyExternalResource() failed"); + else + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + nCopyStart, nLen); + } + else + // Can copy it as-is. + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + rObject.GetArrayOffset(), rObject.GetArrayLength()); + + aLine.append("]\n"); + } + + // If the object has a number element outside a dictionary or array, copy that. + if (filter::PDFNumberElement* pNumber = rObject.GetNumberElement()) + { + aLine.append(static_cast<const char*>(rDocBuffer.GetData()) + pNumber->GetLocation(), pNumber->GetLength()); + aLine.append("\n"); + } + + + aLine.append("endobj\n\n"); + + // We have the whole object, now write it to the output. + if (!updateObject(nObject)) + return -1; + if (!writeBuffer(aLine.getStr(), aLine.getLength())) + return -1; + + return nObject; +} + +OString PDFWriterImpl::copyExternalResources(filter::PDFObjectElement& rPage, const OString& rKind, std::map<sal_Int32, sal_Int32>& rCopiedResources) +{ + // A name - object ID map, IDs as they appear in our output, not the + // original ones. + std::map<OString, sal_Int32> aRet; + + // Get the rKind subset of the resource dictionary. + std::map<OString, filter::PDFElement*> aItems; + if (auto pResources = dynamic_cast<filter::PDFDictionaryElement*>(rPage.Lookup("Resources"))) + { + // Resources is a direct dictionary. + filter::PDFElement* pLookup = pResources->LookupElement(rKind); + if (auto pDictionary = dynamic_cast<filter::PDFDictionaryElement*>(pLookup)) + { + // rKind is an inline dictionary. + aItems = pDictionary->GetItems(); + } + else if (auto pReference = dynamic_cast<filter::PDFReferenceElement*>(pLookup)) + { + // rKind refers to a dictionary. + filter::PDFObjectElement* pReferenced = pReference->LookupObject(); + if (!pReferenced) + { + return OString(); + } + + aItems = pReferenced->GetDictionaryItems(); + } + } + else if (filter::PDFObjectElement* pPageResources = rPage.LookupObject("Resources")) + { + // Resources is an indirect object. + filter::PDFElement* pValue = pPageResources->Lookup(rKind); + if (auto pDictionary = dynamic_cast<filter::PDFDictionaryElement*>(pValue)) + // Kind is a direct dictionary. + aItems = pDictionary->GetItems(); + else if (filter::PDFObjectElement* pObject = pPageResources->LookupObject(rKind)) + // Kind is an indirect object. + aItems = pObject->GetDictionaryItems(); + } + if (aItems.empty()) + return OString(); + + SvMemoryStream& rDocBuffer = rPage.GetDocument().GetEditBuffer(); + + for (const auto& rItem : aItems) + { + // For each item copy it over to our output then insert it into aRet. + auto pReference = dynamic_cast<filter::PDFReferenceElement*>(rItem.second); + if (!pReference) + continue; + + filter::PDFObjectElement* pValue = pReference->LookupObject(); + if (!pValue) + continue; + + // Then copying over an object copy its dictionary and its stream. + sal_Int32 nObject = copyExternalResource(rDocBuffer, *pValue, rCopiedResources); + aRet[rItem.first] = nObject; + } + + // Build the dictionary entry string. + OStringBuffer sRet("/" + rKind + "<<"); + for (const auto& rPair : aRet) + { + sRet.append("/").append(rPair.first).append(" ").append(OString::number(rPair.second)).append(" 0 R"); + } + sRet.append(">>"); + + return sRet.makeStringAndClear(); +} + +void PDFWriterImpl::writeReferenceXObject(ReferenceXObjectEmit& rEmit) +{ + if (rEmit.m_nFormObject <= 0) + return; + + // Count /Matrix and /BBox. + // vcl::ImportPDF() works with 96 DPI so use the same values here, too. + sal_Int32 nOldDPIX = GetDPIX(); + SetDPIX(96); + sal_Int32 nOldDPIY = GetDPIY(); + SetDPIY(96); + Size aSize = PixelToLogic(rEmit.m_aPixelSize, MapMode(m_aMapMode.GetMapUnit())); + SetDPIX(nOldDPIX); + SetDPIY(nOldDPIY); + double fScaleX = 1.0 / aSize.Width(); + double fScaleY = 1.0 / aSize.Height(); + + sal_Int32 nWrappedFormObject = 0; + if (!m_aContext.UseReferenceXObject) + { + // Parse the PDF data, we need that to write the PDF dictionary of our + // object. + SvMemoryStream aPDFStream; + aPDFStream.WriteBytes(rEmit.m_aPDFData.data(), rEmit.m_aPDFData.size()); + aPDFStream.Seek(0); + filter::PDFDocument aPDFDocument; + if (!aPDFDocument.Read(aPDFStream)) + { + SAL_WARN("vcl.pdfwriter", "PDFWriterImpl::writeReferenceXObject: reading the PDF document failed"); + return; + } + std::vector<filter::PDFObjectElement*> aPages = aPDFDocument.GetPages(); + if (aPages.empty()) + { + SAL_WARN("vcl.pdfwriter", "PDFWriterImpl::writeReferenceXObject: no pages"); + return; + } + + size_t nPageIndex = rEmit.m_nPDFPageIndex >= 0 ? rEmit.m_nPDFPageIndex : 0; + + filter::PDFObjectElement* pPage = aPages[nPageIndex]; + if (!pPage) + { + SAL_WARN("vcl.pdfwriter", "PDFWriterImpl::writeReferenceXObject: no page"); + return; + } + + std::vector<filter::PDFObjectElement*> aContentStreams; + if (filter::PDFObjectElement* pContentStream = pPage->LookupObject("Contents")) + aContentStreams.push_back(pContentStream); + else if (auto pArray = dynamic_cast<filter::PDFArrayElement*>(pPage->Lookup("Contents"))) + { + for (const auto pElement : pArray->GetElements()) + { + auto pReference = dynamic_cast<filter::PDFReferenceElement*>(pElement); + if (!pReference) + continue; + + filter::PDFObjectElement* pObject = pReference->LookupObject(); + if (!pObject) + continue; + + aContentStreams.push_back(pObject); + } + } + + if (aContentStreams.empty()) + { + SAL_WARN("vcl.pdfwriter", "PDFWriterImpl::writeReferenceXObject: no content stream"); + return; + } + + // Maps from source object id (PDF image) to target object id (export result). + std::map<sal_Int32, sal_Int32> aCopiedResources; + + nWrappedFormObject = createObject(); + // Write the form XObject wrapped below. This is a separate object from + // the wrapper, this way there is no need to alter the stream contents. + + OStringBuffer aLine; + aLine.append(nWrappedFormObject); + aLine.append(" 0 obj\n"); + aLine.append("<< /Type /XObject"); + aLine.append(" /Subtype /Form"); + + long nWidth = aSize.Width(); + long nHeight = aSize.Height(); + if (auto pRotate = dynamic_cast<filter::PDFNumberElement*>(pPage->Lookup("Rotate"))) + { + // The original page was rotated, then construct a transformation matrix which does the + // same with our form object. + if (rtl::math::approxEqual(pRotate->GetValue(), 90)) + { + std::swap(nWidth, nHeight); + basegfx::B2DHomMatrix aMat; + aMat.rotate(basegfx::deg2rad(pRotate->GetValue())); + // Rotate around the origo (bottom left corner) counter-clockwise, then translate + // horizontally to effectively keep the bottom left corner unchanged. + aLine.append(" /Matrix [ "); + aLine.append(aMat.get(0, 0)); + aLine.append(" "); + aLine.append(aMat.get(0, 1)); + aLine.append(" "); + aLine.append(aMat.get(1, 0)); + aLine.append(" "); + aLine.append(aMat.get(1, 1)); + aLine.append(" 0 "); + aLine.append(nWidth); + aLine.append(" ] "); + } + } + + aLine.append(" /Resources <<"); + static const std::initializer_list<OString> aKeys = + { + "ColorSpace", + "ExtGState", + "Font", + "XObject", + "Shading" + }; + for (const auto& rKey : aKeys) + aLine.append(copyExternalResources(*pPage, rKey, aCopiedResources)); + aLine.append(">>"); + aLine.append(" /BBox [ 0 0 "); + aLine.append(nWidth); + aLine.append(" "); + aLine.append(nHeight); + aLine.append(" ]"); + + if (!g_bDebugDisableCompression) + aLine.append(" /Filter/FlateDecode"); + aLine.append(" /Length "); + + SvMemoryStream aStream; + for (auto pContent : aContentStreams) + { + filter::PDFStreamElement* pPageStream = pContent->GetStream(); + if (!pPageStream) + { + SAL_WARN("vcl.pdfwriter", "PDFWriterImpl::writeReferenceXObject: contents has no stream"); + continue; + } + + SvMemoryStream& rPageStream = pPageStream->GetMemory(); + + auto pFilter = dynamic_cast<filter::PDFNameElement*>(pContent->Lookup("Filter")); + if (pFilter) + { + if (pFilter->GetValue() != "FlateDecode") + continue; + + SvMemoryStream aMemoryStream; + ZCodec aZCodec; + rPageStream.Seek(0); + aZCodec.BeginCompression(); + aZCodec.Decompress(rPageStream, aMemoryStream); + if (!aZCodec.EndCompression()) + { + SAL_WARN("vcl.pdfwriter", "PDFWriterImpl::writeReferenceXObject: decompression failed"); + continue; + } + + aStream.WriteBytes(aMemoryStream.GetData(), aMemoryStream.GetSize()); + } + else + aStream.WriteBytes(rPageStream.GetData(), rPageStream.GetSize()); + } + + compressStream(&aStream); + sal_Int32 nLength = aStream.Tell(); + aLine.append(nLength); + + aLine.append(">>\nstream\n"); + // Copy the original page streams to the form XObject stream. + aLine.append(static_cast<const char*>(aStream.GetData()), aStream.GetSize()); + aLine.append("\nendstream\nendobj\n\n"); + if (!updateObject(nWrappedFormObject)) + return; + if (!writeBuffer(aLine.getStr(), aLine.getLength())) + return; + } + + OStringBuffer aLine; + if (!updateObject(rEmit.m_nFormObject)) + return; + + // Now have all the info to write the form XObject. + aLine.append(rEmit.m_nFormObject); + aLine.append(" 0 obj\n"); + aLine.append("<< /Type /XObject"); + aLine.append(" /Subtype /Form"); + aLine.append(" /Resources << /XObject<<"); + + sal_Int32 nObject = m_aContext.UseReferenceXObject ? rEmit.m_nBitmapObject : nWrappedFormObject; + aLine.append(" /Im"); + aLine.append(nObject); + aLine.append(" "); + aLine.append(nObject); + aLine.append(" 0 R"); + + aLine.append(">> >>"); + aLine.append(" /Matrix [ "); + appendDouble(fScaleX, aLine); + aLine.append(" 0 0 "); + appendDouble(fScaleY, aLine); + aLine.append(" 0 0 ]"); + aLine.append(" /BBox [ 0 0 "); + aLine.append(aSize.Width()); + aLine.append(" "); + aLine.append(aSize.Height()); + aLine.append(" ]\n"); + + if (m_aContext.UseReferenceXObject && rEmit.m_nEmbeddedObject > 0) + { + // Write the reference dictionary. + aLine.append("/Ref<< /F << /Type /Filespec /F (<embedded file>) /EF << /F "); + aLine.append(rEmit.m_nEmbeddedObject); + aLine.append(" 0 R >> >> /Page 0 >>\n"); + } + + aLine.append("/Length "); + + OStringBuffer aStream; + aStream.append("q "); + if (m_aContext.UseReferenceXObject) + { + // Reference XObject markup is used, just refer to the fallback bitmap + // here. + aStream.append(aSize.Width()); + aStream.append(" 0 0 "); + aStream.append(aSize.Height()); + aStream.append(" 0 0 cm\n"); + aStream.append("/Im"); + aStream.append(rEmit.m_nBitmapObject); + aStream.append(" Do\n"); + } + else + { + // Reset line width to the default. + aStream.append(" 1 w\n"); + + // vcl::RenderPDFBitmaps() effectively renders a white background for transparent input, be + // consistent with that. + aStream.append("1 1 1 rg\n"); + aStream.append("0 0 "); + aStream.append(aSize.Width()); + aStream.append(" "); + aStream.append(aSize.Height()); + aStream.append(" re\n"); + aStream.append("f*\n"); + + // No reference XObject, draw the form XObject containing the original + // page streams. + aStream.append("/Im"); + aStream.append(nWrappedFormObject); + aStream.append(" Do\n"); + } + aStream.append("Q"); + aLine.append(aStream.getLength()); + + aLine.append(">>\nstream\n"); + aLine.append(aStream.getStr()); + aLine.append("\nendstream\nendobj\n\n"); + CHECK_RETURN2(writeBuffer(aLine.getStr(), aLine.getLength())); +} + +namespace +{ + unsigned char reverseByte(unsigned char b) + { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; + } + + //tdf#103051 convert any N1BitLsbPal to N1BitMsbPal + Bitmap getExportBitmap(const Bitmap &rBitmap) + { + Bitmap::ScopedReadAccess pAccess(const_cast<Bitmap&>(rBitmap)); + const ScanlineFormat eFormat = pAccess->GetScanlineFormat(); + if (eFormat != ScanlineFormat::N1BitLsbPal) + return rBitmap; + Bitmap aNewBmp(rBitmap); + BitmapScopedWriteAccess xWriteAcc(aNewBmp); + const int nScanLineBytes = (pAccess->Width() + 7U) / 8U; + for (long nY = 0L; nY < xWriteAcc->Height(); ++nY) + { + Scanline pBitSwap = xWriteAcc->GetScanline(nY); + for (int x = 0; x < nScanLineBytes; ++x) + pBitSwap[x] = reverseByte(pBitSwap[x]); + } + return aNewBmp; + } +} + +bool PDFWriterImpl::writeBitmapObject( BitmapEmit& rObject, bool bMask ) +{ + if (!rObject.m_aReferenceXObject.m_aPDFData.empty() && !m_aContext.UseReferenceXObject) + { + writeReferenceXObject(rObject.m_aReferenceXObject); + return true; + } + + CHECK_RETURN( updateObject( rObject.m_nObject ) ); + + Bitmap aBitmap; + Color aTransparentColor( COL_TRANSPARENT ); + bool bWriteMask = false; + if( ! bMask ) + { + aBitmap = getExportBitmap(rObject.m_aBitmap.GetBitmap()); + if( rObject.m_aBitmap.IsAlpha() ) + { + if( m_aContext.Version >= PDFWriter::PDFVersion::PDF_1_4 ) + bWriteMask = true; + // else draw without alpha channel + } + else + { + switch( rObject.m_aBitmap.GetTransparentType() ) + { + case TransparentType::NONE: + break; + case TransparentType::Color: + aTransparentColor = rObject.m_aBitmap.GetTransparentColor(); + break; + case TransparentType::Bitmap: + bWriteMask = true; + break; + } + } + } + else + { + if( m_aContext.Version < PDFWriter::PDFVersion::PDF_1_4 || ! rObject.m_aBitmap.IsAlpha() ) + { + aBitmap = getExportBitmap(rObject.m_aBitmap.GetMask()); + aBitmap.Convert( BmpConversion::N1BitThreshold ); + SAL_WARN_IF( aBitmap.GetBitCount() != 1, "vcl.pdfwriter", "mask conversion failed" ); + } + else if( aBitmap.GetBitCount() != 8 ) + { + aBitmap = getExportBitmap(rObject.m_aBitmap.GetAlpha().GetBitmap()); + aBitmap.Convert( BmpConversion::N8BitGreys ); + SAL_WARN_IF( aBitmap.GetBitCount() != 8, "vcl.pdfwriter", "alpha mask conversion failed" ); + } + } + + Bitmap::ScopedReadAccess pAccess(aBitmap); + + bool bTrueColor; + sal_Int32 nBitsPerComponent; + switch( aBitmap.GetBitCount() ) + { + case 1: + case 2: + case 4: + case 8: + bTrueColor = false; + nBitsPerComponent = aBitmap.GetBitCount(); + break; + default: + bTrueColor = true; + nBitsPerComponent = 8; + break; + } + + sal_Int32 nStreamLengthObject = createObject(); + sal_Int32 nMaskObject = 0; + + if (g_bDebugDisableCompression) + { + emitComment( "PDFWriterImpl::writeBitmapObject" ); + } + OStringBuffer aLine(1024); + aLine.append( rObject.m_nObject ); + aLine.append( " 0 obj\n" + "<</Type/XObject/Subtype/Image/Width " ); + aLine.append( static_cast<sal_Int32>(aBitmap.GetSizePixel().Width()) ); + aLine.append( "/Height " ); + aLine.append( static_cast<sal_Int32>(aBitmap.GetSizePixel().Height()) ); + aLine.append( "/BitsPerComponent " ); + aLine.append( nBitsPerComponent ); + aLine.append( "/Length " ); + aLine.append( nStreamLengthObject ); + aLine.append( " 0 R\n" ); + if (!g_bDebugDisableCompression) + { + if( nBitsPerComponent != 1 ) + { + aLine.append( "/Filter/FlateDecode" ); + } + else + { + aLine.append( "/Filter/CCITTFaxDecode/DecodeParms<</K -1/BlackIs1 true/Columns " ); + aLine.append( static_cast<sal_Int32>(aBitmap.GetSizePixel().Width()) ); + aLine.append( ">>\n" ); + } + } + if( ! bMask ) + { + aLine.append( "/ColorSpace" ); + if( bTrueColor ) + aLine.append( "/DeviceRGB\n" ); + else if( aBitmap.HasGreyPaletteAny() ) + { + aLine.append( "/DeviceGray\n" ); + if( aBitmap.GetBitCount() == 1 ) + { + // #i47395# 1 bit bitmaps occasionally have an inverted grey palette + sal_uInt16 nBlackIndex = pAccess->GetBestPaletteIndex( BitmapColor( COL_BLACK ) ); + assert( nBlackIndex == 0 || nBlackIndex == 1); + sal_uInt16 nWhiteIndex = pAccess->GetBestPaletteIndex( BitmapColor( COL_WHITE ) ); + if( pAccess->GetPalette()[nBlackIndex] == BitmapColor( COL_BLACK ) && + pAccess->GetPalette()[nWhiteIndex] == BitmapColor( COL_WHITE ) ) + { + // It is black and white + if( nBlackIndex == 1 ) + aLine.append( "/Decode[1 0]\n" ); + } + else + { + // It is two levels of grey + aLine.append( "/Decode[" ); + assert( pAccess->GetPalette()[0].GetRed() == pAccess->GetPalette()[0].GetGreen() && + pAccess->GetPalette()[0].GetRed() == pAccess->GetPalette()[0].GetBlue() && + pAccess->GetPalette()[1].GetRed() == pAccess->GetPalette()[1].GetGreen() && + pAccess->GetPalette()[1].GetRed() == pAccess->GetPalette()[1].GetBlue() ); + aLine.append( pAccess->GetPalette()[0].GetRed() / 255.0 ); + aLine.append( " " ); + aLine.append( pAccess->GetPalette()[1].GetRed() / 255.0 ); + aLine.append( "]\n" ); + } + } + } + else + { + aLine.append( "[ /Indexed/DeviceRGB " ); + aLine.append( static_cast<sal_Int32>(pAccess->GetPaletteEntryCount()-1) ); + aLine.append( "\n<" ); + if( m_aContext.Encryption.Encrypt() ) + { + enableStringEncryption( rObject.m_nObject ); + //check encryption buffer size + m_vEncryptionBuffer.resize(pAccess->GetPaletteEntryCount()*3); + int nChar = 0; + //fill the encryption buffer + for( sal_uInt16 i = 0; i < pAccess->GetPaletteEntryCount(); i++ ) + { + const BitmapColor& rColor = pAccess->GetPaletteColor( i ); + m_vEncryptionBuffer[nChar++] = rColor.GetRed(); + m_vEncryptionBuffer[nChar++] = rColor.GetGreen(); + m_vEncryptionBuffer[nChar++] = rColor.GetBlue(); + } + //encrypt the colorspace lookup table + rtl_cipher_encodeARCFOUR( m_aCipher, m_vEncryptionBuffer.data(), nChar, m_vEncryptionBuffer.data(), nChar ); + //now queue the data for output + nChar = 0; + for( sal_uInt16 i = 0; i < pAccess->GetPaletteEntryCount(); i++ ) + { + appendHex(m_vEncryptionBuffer[nChar++], aLine ); + appendHex(m_vEncryptionBuffer[nChar++], aLine ); + appendHex(m_vEncryptionBuffer[nChar++], aLine ); + } + } + else //no encryption requested (PDF/A-1a program flow drops here) + { + for( sal_uInt16 i = 0; i < pAccess->GetPaletteEntryCount(); i++ ) + { + const BitmapColor& rColor = pAccess->GetPaletteColor( i ); + appendHex( rColor.GetRed(), aLine ); + appendHex( rColor.GetGreen(), aLine ); + appendHex( rColor.GetBlue(), aLine ); + } + } + aLine.append( ">\n]\n" ); + } + } + else + { + if( aBitmap.GetBitCount() == 1 ) + { + aLine.append( "/ImageMask true\n" ); + sal_Int32 nBlackIndex = pAccess->GetBestPaletteIndex( BitmapColor( COL_BLACK ) ); + SAL_WARN_IF( nBlackIndex != 0 && nBlackIndex != 1, "vcl.pdfwriter", "wrong black index" ); + if( nBlackIndex ) + aLine.append( "/Decode[ 1 0 ]\n" ); + else + aLine.append( "/Decode[ 0 1 ]\n" ); + } + else if( aBitmap.GetBitCount() == 8 ) + { + aLine.append( "/ColorSpace/DeviceGray\n" + "/Decode [ 1 0 ]\n" ); + } + } + + if( ! bMask && m_aContext.Version > PDFWriter::PDFVersion::PDF_1_2 && !m_bIsPDF_A1 ) + { + if( bWriteMask ) + { + nMaskObject = createObject(); + if( rObject.m_aBitmap.IsAlpha() && m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 ) + aLine.append( "/SMask " ); + else + aLine.append( "/Mask " ); + aLine.append( nMaskObject ); + aLine.append( " 0 R\n" ); + } + else if( aTransparentColor != COL_TRANSPARENT ) + { + aLine.append( "/Mask[ " ); + if( bTrueColor ) + { + aLine.append( static_cast<sal_Int32>(aTransparentColor.GetRed()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(aTransparentColor.GetRed()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(aTransparentColor.GetGreen()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(aTransparentColor.GetGreen()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(aTransparentColor.GetBlue()) ); + aLine.append( ' ' ); + aLine.append( static_cast<sal_Int32>(aTransparentColor.GetBlue()) ); + } + else + { + sal_Int32 nIndex = pAccess->GetBestPaletteIndex( BitmapColor( aTransparentColor ) ); + aLine.append( nIndex ); + } + aLine.append( " ]\n" ); + } + } + else if( m_bIsPDF_A1 && (bWriteMask || aTransparentColor != COL_TRANSPARENT) ) + m_aErrors.insert( PDFWriter::Warning_Transparency_Omitted_PDFA ); + + aLine.append( ">>\n" + "stream\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + sal_uInt64 nStartPos = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nStartPos)) ); + + checkAndEnableStreamEncryption( rObject.m_nObject ); + if (!g_bDebugDisableCompression && nBitsPerComponent == 1) + { + writeG4Stream(pAccess.get()); + } + else + { + beginCompression(); + if( ! bTrueColor || pAccess->GetScanlineFormat() == ScanlineFormat::N24BitTcRgb ) + { + //With PDF bitmaps, each row is padded to a BYTE boundary (multiple of 8 bits). + const int nScanLineBytes = ((pAccess->GetBitCount() * pAccess->Width()) + 7U) / 8U; + + for( long i = 0; i < pAccess->Height(); i++ ) + { + CHECK_RETURN( writeBuffer( pAccess->GetScanline( i ), nScanLineBytes ) ); + } + } + else + { + const int nScanLineBytes = pAccess->Width()*3; + std::unique_ptr<sal_uInt8[]> xCol(new sal_uInt8[nScanLineBytes]); + for( long y = 0; y < pAccess->Height(); y++ ) + { + for( long x = 0; x < pAccess->Width(); x++ ) + { + BitmapColor aColor = pAccess->GetColor( y, x ); + xCol[3*x+0] = aColor.GetRed(); + xCol[3*x+1] = aColor.GetGreen(); + xCol[3*x+2] = aColor.GetBlue(); + } + CHECK_RETURN(writeBuffer(xCol.get(), nScanLineBytes)); + } + } + endCompression(); + } + disableStreamEncryption(); + + sal_uInt64 nEndPos = 0; + CHECK_RETURN( (osl::File::E_None == m_aFile.getPos(nEndPos)) ); + aLine.setLength( 0 ); + aLine.append( "\nendstream\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + CHECK_RETURN( updateObject( nStreamLengthObject ) ); + aLine.setLength( 0 ); + aLine.append( nStreamLengthObject ); + aLine.append( " 0 obj\n" ); + aLine.append( static_cast<sal_Int64>(nEndPos-nStartPos) ); + aLine.append( "\nendobj\n\n" ); + CHECK_RETURN( writeBuffer( aLine.getStr(), aLine.getLength() ) ); + + if( nMaskObject ) + { + BitmapEmit aEmit; + aEmit.m_nObject = nMaskObject; + aEmit.m_aBitmap = rObject.m_aBitmap; + return writeBitmapObject( aEmit, true ); + } + + writeReferenceXObject(rObject.m_aReferenceXObject); + + return true; +} + +void PDFWriterImpl::createEmbeddedFile(const Graphic& rGraphic, ReferenceXObjectEmit& rEmit, sal_Int32 nBitmapObject) +{ + // The bitmap object is always a valid identifier, even if the graphic has + // no pdf data. + rEmit.m_nBitmapObject = nBitmapObject; + + if (!rGraphic.getVectorGraphicData() || rGraphic.getVectorGraphicData()->getVectorGraphicDataType() != VectorGraphicDataType::Pdf) + return; + + sal_uInt32 nLength = rGraphic.getVectorGraphicData()->getVectorGraphicDataArrayLength(); + auto const & rArray = rGraphic.getVectorGraphicData()->getVectorGraphicDataArray(); + + auto pPDFData = std::make_shared<std::vector<sal_Int8>>(rArray.getConstArray(), rArray.getConstArray() + nLength); + + if (m_aContext.UseReferenceXObject) + { + // Store the original PDF data as an embedded file. + m_aEmbeddedFiles.emplace_back(); + m_aEmbeddedFiles.back().m_nObject = createObject(); + m_aEmbeddedFiles.back().m_pData = pPDFData; + rEmit.m_nEmbeddedObject = m_aEmbeddedFiles.back().m_nObject; + } + else + { + rEmit.m_nPDFPageIndex = rGraphic.getVectorGraphicData()->getPageIndex(); + rEmit.m_aPDFData = *pPDFData; + } + + rEmit.m_nFormObject = createObject(); + rEmit.m_aPixelSize = rGraphic.GetPrefSize(); +} + +void PDFWriterImpl::drawJPGBitmap( SvStream& rDCTData, bool bIsTrueColor, const Size& rSizePixel, const tools::Rectangle& rTargetArea, const Bitmap& rMask, const Graphic& rGraphic ) +{ + MARK( "drawJPGBitmap" ); + + OStringBuffer aLine( 80 ); + updateGraphicsState(); + + // #i40055# sanity check + if( ! (rTargetArea.GetWidth() && rTargetArea.GetHeight() ) ) + return; + if( ! (rSizePixel.Width() && rSizePixel.Height()) ) + return; + + rDCTData.Seek( 0 ); + if( bIsTrueColor && m_aContext.ColorMode == PDFWriter::DrawGreyscale ) + { + // need to convert to grayscale; + // load stream to bitmap and draw the bitmap instead + Graphic aGraphic; + GraphicConverter::Import( rDCTData, aGraphic, ConvertDataFormat::JPG ); + if( !!rMask && rMask.GetSizePixel() == aGraphic.GetSizePixel() ) + { + Bitmap aBmp( aGraphic.GetBitmapEx().GetBitmap() ); + BitmapEx aBmpEx( aBmp, rMask ); + drawBitmap( rTargetArea.TopLeft(), rTargetArea.GetSize(), aBmpEx ); + } + else + drawBitmap( rTargetArea.TopLeft(), rTargetArea.GetSize(), aGraphic.GetBitmapEx() ); + return; + } + + std::unique_ptr<SvMemoryStream> pStream(new SvMemoryStream); + pStream->WriteStream( rDCTData ); + pStream->Seek( STREAM_SEEK_TO_END ); + + BitmapID aID; + aID.m_aPixelSize = rSizePixel; + aID.m_nSize = pStream->Tell(); + pStream->Seek( STREAM_SEEK_TO_BEGIN ); + aID.m_nChecksum = vcl_get_checksum( 0, pStream->GetData(), aID.m_nSize ); + if( ! rMask.IsEmpty() ) + aID.m_nMaskChecksum = rMask.GetChecksum(); + + std::vector< JPGEmit >::const_iterator it = std::find_if(m_aJPGs.begin(), m_aJPGs.end(), + [&](const JPGEmit& arg) { return aID == arg.m_aID; }); + if( it == m_aJPGs.end() ) + { + m_aJPGs.emplace( m_aJPGs.begin() ); + JPGEmit& rEmit = m_aJPGs.front(); + if (!rGraphic.getVectorGraphicData() || rGraphic.getVectorGraphicData()->getVectorGraphicDataType() != VectorGraphicDataType::Pdf || m_aContext.UseReferenceXObject) + rEmit.m_nObject = createObject(); + rEmit.m_aID = aID; + rEmit.m_pStream = std::move( pStream ); + rEmit.m_bTrueColor = bIsTrueColor; + if( !! rMask && rMask.GetSizePixel() == rSizePixel ) + rEmit.m_aMask = rMask; + createEmbeddedFile(rGraphic, rEmit.m_aReferenceXObject, rEmit.m_nObject); + + it = m_aJPGs.begin(); + } + + aLine.append( "q " ); + sal_Int32 nCheckWidth = 0; + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rTargetArea.GetWidth()), aLine, false, &nCheckWidth ); + aLine.append( " 0 0 " ); + sal_Int32 nCheckHeight = 0; + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rTargetArea.GetHeight()), aLine, true, &nCheckHeight ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( rTargetArea.BottomLeft(), aLine ); + aLine.append( " cm\n/Im" ); + sal_Int32 nObject = it->m_aReferenceXObject.getObject(); + aLine.append(nObject); + aLine.append( " Do Q\n" ); + if( nCheckWidth == 0 || nCheckHeight == 0 ) + { + // #i97512# avoid invalid current matrix + aLine.setLength( 0 ); + aLine.append( "\n%jpeg image /Im" ); + aLine.append( it->m_nObject ); + aLine.append( " scaled to zero size, omitted\n" ); + } + writeBuffer( aLine.getStr(), aLine.getLength() ); + + OString aObjName = "Im" + OString::number(nObject); + pushResource( ResourceKind::XObject, aObjName, nObject ); + +} + +void PDFWriterImpl::drawBitmap( const Point& rDestPoint, const Size& rDestSize, const BitmapEmit& rBitmap, const Color& rFillColor ) +{ + OStringBuffer aLine( 80 ); + updateGraphicsState(); + + aLine.append( "q " ); + if( rFillColor != COL_TRANSPARENT ) + { + appendNonStrokingColor( rFillColor, aLine ); + aLine.append( ' ' ); + } + sal_Int32 nCheckWidth = 0; + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rDestSize.Width()), aLine, false, &nCheckWidth ); + aLine.append( " 0 0 " ); + sal_Int32 nCheckHeight = 0; + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rDestSize.Height()), aLine, true, &nCheckHeight ); + aLine.append( ' ' ); + m_aPages.back().appendPoint( rDestPoint + Point( 0, rDestSize.Height()-1 ), aLine ); + aLine.append( " cm\n/Im" ); + sal_Int32 nObject = rBitmap.m_aReferenceXObject.getObject(); + aLine.append(nObject); + aLine.append( " Do Q\n" ); + if( nCheckWidth == 0 || nCheckHeight == 0 ) + { + // #i97512# avoid invalid current matrix + aLine.setLength( 0 ); + aLine.append( "\n%bitmap image /Im" ); + aLine.append( rBitmap.m_nObject ); + aLine.append( " scaled to zero size, omitted\n" ); + } + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +const BitmapEmit& PDFWriterImpl::createBitmapEmit( const BitmapEx& i_rBitmap, const Graphic& rGraphic ) +{ + BitmapEx aBitmap( i_rBitmap ); + if( m_aContext.ColorMode == PDFWriter::DrawGreyscale ) + { + BmpConversion eConv = BmpConversion::N8BitGreys; + int nDepth = aBitmap.GetBitmap().GetBitCount(); + if( nDepth <= 4 ) + eConv = BmpConversion::N4BitGreys; + if( nDepth > 1 ) + aBitmap.Convert( eConv ); + } + BitmapID aID; + aID.m_aPixelSize = aBitmap.GetSizePixel(); + aID.m_nSize = aBitmap.GetBitCount(); + aID.m_nChecksum = aBitmap.GetBitmap().GetChecksum(); + aID.m_nMaskChecksum = 0; + if( aBitmap.IsAlpha() ) + aID.m_nMaskChecksum = aBitmap.GetAlpha().GetChecksum(); + else + { + Bitmap aMask = aBitmap.GetMask(); + if( ! aMask.IsEmpty() ) + aID.m_nMaskChecksum = aMask.GetChecksum(); + } + std::list< BitmapEmit >::const_iterator it = std::find_if(m_aBitmaps.begin(), m_aBitmaps.end(), + [&](const BitmapEmit& arg) { return aID == arg.m_aID; }); + if( it == m_aBitmaps.end() ) + { + m_aBitmaps.push_front( BitmapEmit() ); + m_aBitmaps.front().m_aID = aID; + m_aBitmaps.front().m_aBitmap = aBitmap; + if (!rGraphic.getVectorGraphicData() || rGraphic.getVectorGraphicData()->getVectorGraphicDataType() != VectorGraphicDataType::Pdf || m_aContext.UseReferenceXObject) + m_aBitmaps.front().m_nObject = createObject(); + createEmbeddedFile(rGraphic, m_aBitmaps.front().m_aReferenceXObject, m_aBitmaps.front().m_nObject); + it = m_aBitmaps.begin(); + } + + sal_Int32 nObject = it->m_aReferenceXObject.getObject(); + OString aObjName = "Im" + OString::number(nObject); + pushResource( ResourceKind::XObject, aObjName, nObject ); + + return *it; +} + +void PDFWriterImpl::drawBitmap( const Point& rDestPoint, const Size& rDestSize, const Bitmap& rBitmap, const Graphic& rGraphic ) +{ + MARK( "drawBitmap (Bitmap)" ); + + // #i40055# sanity check + if( ! (rDestSize.Width() && rDestSize.Height()) ) + return; + + const BitmapEmit& rEmit = createBitmapEmit( BitmapEx( rBitmap ), rGraphic ); + drawBitmap( rDestPoint, rDestSize, rEmit, COL_TRANSPARENT ); +} + +void PDFWriterImpl::drawBitmap( const Point& rDestPoint, const Size& rDestSize, const BitmapEx& rBitmap ) +{ + MARK( "drawBitmap (BitmapEx)" ); + + // #i40055# sanity check + if( ! (rDestSize.Width() && rDestSize.Height()) ) + return; + + const BitmapEmit& rEmit = createBitmapEmit( rBitmap, Graphic() ); + drawBitmap( rDestPoint, rDestSize, rEmit, COL_TRANSPARENT ); +} + +sal_Int32 PDFWriterImpl::createGradient( const Gradient& rGradient, const Size& rSize ) +{ + Size aPtSize( lcl_convert( m_aGraphicsStack.front().m_aMapMode, + MapMode( MapUnit::MapPoint ), + this, + rSize ) ); + // check if we already have this gradient + // rounding to point will generally lose some pixels + // round up to point boundary + aPtSize.AdjustWidth( 1 ); + aPtSize.AdjustHeight( 1 ); + std::list< GradientEmit >::const_iterator it = std::find_if(m_aGradients.begin(), m_aGradients.end(), + [&](const GradientEmit& arg) { return ((rGradient == arg.m_aGradient) && (aPtSize == arg.m_aSize) ); }); + + if( it == m_aGradients.end() ) + { + m_aGradients.push_front( GradientEmit() ); + m_aGradients.front().m_aGradient = rGradient; + m_aGradients.front().m_nObject = createObject(); + m_aGradients.front().m_aSize = aPtSize; + it = m_aGradients.begin(); + } + + OStringBuffer aObjName( 16 ); + aObjName.append( 'P' ); + aObjName.append( it->m_nObject ); + pushResource( ResourceKind::Shading, aObjName.makeStringAndClear(), it->m_nObject ); + + return it->m_nObject; +} + +void PDFWriterImpl::drawGradient( const tools::Rectangle& rRect, const Gradient& rGradient ) +{ + MARK( "drawGradient (Rectangle)" ); + + if( m_aContext.Version == PDFWriter::PDFVersion::PDF_1_2 ) + { + drawRectangle( rRect ); + return; + } + + sal_Int32 nGradient = createGradient( rGradient, rRect.GetSize() ); + + Point aTranslate( rRect.BottomLeft() ); + aTranslate += Point( 0, 1 ); + + updateGraphicsState(); + + OStringBuffer aLine( 80 ); + aLine.append( "q 1 0 0 1 " ); + m_aPages.back().appendPoint( aTranslate, aLine ); + aLine.append( " cm " ); + // if a stroke is appended reset the clip region before stroke + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + aLine.append( "q " ); + aLine.append( "0 0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rRect.GetWidth()), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rRect.GetHeight()), aLine ); + aLine.append( " re W n\n" ); + + aLine.append( "/P" ); + aLine.append( nGradient ); + aLine.append( " sh " ); + if( m_aGraphicsStack.front().m_aLineColor != COL_TRANSPARENT ) + { + aLine.append( "Q 0 0 " ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rRect.GetWidth()), aLine, false ); + aLine.append( ' ' ); + m_aPages.back().appendMappedLength( static_cast<sal_Int32>(rRect.GetHeight()), aLine ); + aLine.append( " re S " ); + } + aLine.append( "Q\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +void PDFWriterImpl::drawHatch( const tools::PolyPolygon& rPolyPoly, const Hatch& rHatch ) +{ + MARK( "drawHatch" ); + + updateGraphicsState(); + + if( rPolyPoly.Count() ) + { + tools::PolyPolygon aPolyPoly( rPolyPoly ); + + aPolyPoly.Optimize( PolyOptimizeFlags::NO_SAME ); + push( PushFlags::LINECOLOR ); + setLineColor( rHatch.GetColor() ); + DrawHatch( aPolyPoly, rHatch, false ); + pop(); + } +} + +void PDFWriterImpl::drawWallpaper( const tools::Rectangle& rRect, const Wallpaper& rWall ) +{ + MARK( "drawWallpaper" ); + + bool bDrawColor = false; + bool bDrawGradient = false; + bool bDrawBitmap = false; + + BitmapEx aBitmap; + Point aBmpPos = rRect.TopLeft(); + Size aBmpSize; + if( rWall.IsBitmap() ) + { + aBitmap = rWall.GetBitmap(); + aBmpSize = lcl_convert( aBitmap.GetPrefMapMode(), + getMapMode(), + this, + aBitmap.GetPrefSize() ); + tools::Rectangle aRect( rRect ); + if( rWall.IsRect() ) + { + aRect = rWall.GetRect(); + aBmpPos = aRect.TopLeft(); + aBmpSize = aRect.GetSize(); + } + if( rWall.GetStyle() != WallpaperStyle::Scale ) + { + if( rWall.GetStyle() != WallpaperStyle::Tile ) + { + bDrawBitmap = true; + if( rWall.IsGradient() ) + bDrawGradient = true; + else + bDrawColor = true; + switch( rWall.GetStyle() ) + { + case WallpaperStyle::TopLeft: + break; + case WallpaperStyle::Top: + aBmpPos.AdjustX((aRect.GetWidth()-aBmpSize.Width())/2 ); + break; + case WallpaperStyle::Left: + aBmpPos.AdjustY((aRect.GetHeight()-aBmpSize.Height())/2 ); + break; + case WallpaperStyle::TopRight: + aBmpPos.AdjustX(aRect.GetWidth()-aBmpSize.Width() ); + break; + case WallpaperStyle::Center: + aBmpPos.AdjustX((aRect.GetWidth()-aBmpSize.Width())/2 ); + aBmpPos.AdjustY((aRect.GetHeight()-aBmpSize.Height())/2 ); + break; + case WallpaperStyle::Right: + aBmpPos.AdjustX(aRect.GetWidth()-aBmpSize.Width() ); + aBmpPos.AdjustY((aRect.GetHeight()-aBmpSize.Height())/2 ); + break; + case WallpaperStyle::BottomLeft: + aBmpPos.AdjustY(aRect.GetHeight()-aBmpSize.Height() ); + break; + case WallpaperStyle::Bottom: + aBmpPos.AdjustX((aRect.GetWidth()-aBmpSize.Width())/2 ); + aBmpPos.AdjustY(aRect.GetHeight()-aBmpSize.Height() ); + break; + case WallpaperStyle::BottomRight: + aBmpPos.AdjustX(aRect.GetWidth()-aBmpSize.Width() ); + aBmpPos.AdjustY(aRect.GetHeight()-aBmpSize.Height() ); + break; + default: ; + } + } + else + { + // push the bitmap + const BitmapEmit& rEmit = createBitmapEmit( aBitmap, Graphic() ); + + // convert to page coordinates; this needs to be done here + // since the emit does not know the page anymore + tools::Rectangle aConvertRect( aBmpPos, aBmpSize ); + m_aPages.back().convertRect( aConvertRect ); + + OString aImageName = "Im" + OString::number( rEmit.m_nObject ); + + // push the pattern + OStringBuffer aTilingStream( 32 ); + appendFixedInt( aConvertRect.GetWidth(), aTilingStream ); + aTilingStream.append( " 0 0 " ); + appendFixedInt( aConvertRect.GetHeight(), aTilingStream ); + aTilingStream.append( " 0 0 cm\n/" ); + aTilingStream.append( aImageName ); + aTilingStream.append( " Do\n" ); + + m_aTilings.emplace_back( ); + m_aTilings.back().m_nObject = createObject(); + m_aTilings.back().m_aRectangle = tools::Rectangle( Point( 0, 0 ), aConvertRect.GetSize() ); + m_aTilings.back().m_pTilingStream.reset(new SvMemoryStream()); + m_aTilings.back().m_pTilingStream->WriteBytes( + aTilingStream.getStr(), aTilingStream.getLength() ); + // phase the tiling so wallpaper begins on upper left + if ((aConvertRect.GetWidth() == 0) || (aConvertRect.GetHeight() == 0)) + throw o3tl::divide_by_zero(); + m_aTilings.back().m_aTransform.matrix[2] = double(aConvertRect.Left() % aConvertRect.GetWidth()) / fDivisor; + m_aTilings.back().m_aTransform.matrix[5] = double(aConvertRect.Top() % aConvertRect.GetHeight()) / fDivisor; + m_aTilings.back().m_aResources.m_aXObjects[aImageName] = rEmit.m_nObject; + + updateGraphicsState(); + + OStringBuffer aObjName( 16 ); + aObjName.append( 'P' ); + aObjName.append( m_aTilings.back().m_nObject ); + OString aPatternName( aObjName.makeStringAndClear() ); + pushResource( ResourceKind::Pattern, aPatternName, m_aTilings.back().m_nObject ); + + // fill a rRect with the pattern + OStringBuffer aLine( 100 ); + aLine.append( "q /Pattern cs /" ); + aLine.append( aPatternName ); + aLine.append( " scn " ); + m_aPages.back().appendRect( rRect, aLine ); + aLine.append( " f Q\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + } + } + else + { + aBmpPos = aRect.TopLeft(); + aBmpSize = aRect.GetSize(); + bDrawBitmap = true; + } + + if( aBitmap.IsTransparent() ) + { + if( rWall.IsGradient() ) + bDrawGradient = true; + else + bDrawColor = true; + } + } + else if( rWall.IsGradient() ) + bDrawGradient = true; + else + bDrawColor = true; + + if( bDrawGradient ) + { + drawGradient( rRect, rWall.GetGradient() ); + } + if( bDrawColor ) + { + Color aOldLineColor = m_aGraphicsStack.front().m_aLineColor; + Color aOldFillColor = m_aGraphicsStack.front().m_aFillColor; + setLineColor( COL_TRANSPARENT ); + setFillColor( rWall.GetColor() ); + drawRectangle( rRect ); + setLineColor( aOldLineColor ); + setFillColor( aOldFillColor ); + } + if( bDrawBitmap ) + { + // set temporary clip region since aBmpPos and aBmpSize + // may be outside rRect + OStringBuffer aLine( 20 ); + aLine.append( "q " ); + m_aPages.back().appendRect( rRect, aLine ); + aLine.append( " W n\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + drawBitmap( aBmpPos, aBmpSize, aBitmap ); + writeBuffer( "Q\n", 2 ); + } +} + +void PDFWriterImpl::updateGraphicsState(Mode const mode) +{ + OStringBuffer aLine( 256 ); + GraphicsState& rNewState = m_aGraphicsStack.front(); + // first set clip region since it might invalidate everything else + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::ClipRegion ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::ClipRegion; + + if( m_aCurrentPDFState.m_bClipRegion != rNewState.m_bClipRegion || + ( rNewState.m_bClipRegion && m_aCurrentPDFState.m_aClipRegion != rNewState.m_aClipRegion ) ) + { + if( m_aCurrentPDFState.m_bClipRegion ) + { + aLine.append( "Q " ); + // invalidate everything but the clip region + m_aCurrentPDFState = GraphicsState(); + rNewState.m_nUpdateFlags = ~GraphicsStateUpdateFlags::ClipRegion; + } + if( rNewState.m_bClipRegion ) + { + // clip region is always stored in private PDF mapmode + MapMode aNewMapMode = rNewState.m_aMapMode; + rNewState.m_aMapMode = m_aMapMode; + SetMapMode( rNewState.m_aMapMode ); + m_aCurrentPDFState.m_aMapMode = rNewState.m_aMapMode; + + aLine.append("q "); + if ( rNewState.m_aClipRegion.count() ) + { + m_aPages.back().appendPolyPolygon( rNewState.m_aClipRegion, aLine ); + } + else + { + // tdf#130150 Need to revert tdf#99680, that breaks the + // rule that an set but empty clip-region clips everything + // aka draws nothing -> nothing is in an empty clip-region + aLine.append( "0 0 m h " ); // NULL clip, i.e. nothing visible + } + aLine.append( "W* n\n" ); + + rNewState.m_aMapMode = aNewMapMode; + SetMapMode( rNewState.m_aMapMode ); + m_aCurrentPDFState.m_aMapMode = rNewState.m_aMapMode; + } + } + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::MapMode ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::MapMode; + SetMapMode( rNewState.m_aMapMode ); + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::Font ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::Font; + SetFont( rNewState.m_aFont ); + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::LayoutMode ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::LayoutMode; + SetLayoutMode( rNewState.m_nLayoutMode ); + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::DigitLanguage ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::DigitLanguage; + SetDigitLanguage( rNewState.m_aDigitLanguage ); + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::LineColor ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::LineColor; + if( m_aCurrentPDFState.m_aLineColor != rNewState.m_aLineColor && + rNewState.m_aLineColor != COL_TRANSPARENT ) + { + appendStrokingColor( rNewState.m_aLineColor, aLine ); + aLine.append( "\n" ); + } + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::FillColor ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::FillColor; + if( m_aCurrentPDFState.m_aFillColor != rNewState.m_aFillColor && + rNewState.m_aFillColor != COL_TRANSPARENT ) + { + appendNonStrokingColor( rNewState.m_aFillColor, aLine ); + aLine.append( "\n" ); + } + } + + if( rNewState.m_nUpdateFlags & GraphicsStateUpdateFlags::TransparentPercent ) + { + rNewState.m_nUpdateFlags &= ~GraphicsStateUpdateFlags::TransparentPercent; + if( m_aContext.Version >= PDFWriter::PDFVersion::PDF_1_4 ) + { + // TODO: switch extended graphicsstate + } + } + + // everything is up to date now + m_aCurrentPDFState = m_aGraphicsStack.front(); + if ((mode != Mode::NOWRITE) && !aLine.isEmpty()) + writeBuffer( aLine.getStr(), aLine.getLength() ); +} + +/* #i47544# imitate OutputDevice behaviour: +* if a font with a nontransparent color is set, it overwrites the current +* text color. OTOH setting the text color will overwrite the color of the font. +*/ +void PDFWriterImpl::setFont( const vcl::Font& rFont ) +{ + Color aColor = rFont.GetColor(); + if( aColor == COL_TRANSPARENT ) + aColor = m_aGraphicsStack.front().m_aFont.GetColor(); + m_aGraphicsStack.front().m_aFont = rFont; + m_aGraphicsStack.front().m_aFont.SetColor( aColor ); + m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::Font; +} + +void PDFWriterImpl::push( PushFlags nFlags ) +{ + OSL_ENSURE( !m_aGraphicsStack.empty(), "invalid graphics stack" ); + m_aGraphicsStack.push_front( m_aGraphicsStack.front() ); + m_aGraphicsStack.front().m_nFlags = nFlags; +} + +void PDFWriterImpl::pop() +{ + OSL_ENSURE( m_aGraphicsStack.size() > 1, "pop without push" ); + if( m_aGraphicsStack.size() < 2 ) + return; + + GraphicsState aState = m_aGraphicsStack.front(); + m_aGraphicsStack.pop_front(); + GraphicsState& rOld = m_aGraphicsStack.front(); + + // move those parameters back that were not pushed + // in the first place + if( ! (aState.m_nFlags & PushFlags::LINECOLOR) ) + setLineColor( aState.m_aLineColor ); + if( ! (aState.m_nFlags & PushFlags::FILLCOLOR) ) + setFillColor( aState.m_aFillColor ); + if( ! (aState.m_nFlags & PushFlags::FONT) ) + setFont( aState.m_aFont ); + if( ! (aState.m_nFlags & PushFlags::TEXTCOLOR) ) + setTextColor( aState.m_aFont.GetColor() ); + if( ! (aState.m_nFlags & PushFlags::MAPMODE) ) + setMapMode( aState.m_aMapMode ); + if( ! (aState.m_nFlags & PushFlags::CLIPREGION) ) + { + // do not use setClipRegion here + // it would convert again assuming the current mapmode + rOld.m_aClipRegion = aState.m_aClipRegion; + rOld.m_bClipRegion = aState.m_bClipRegion; + } + if( ! (aState.m_nFlags & PushFlags::TEXTLINECOLOR ) ) + setTextLineColor( aState.m_aTextLineColor ); + if( ! (aState.m_nFlags & PushFlags::OVERLINECOLOR ) ) + setOverlineColor( aState.m_aOverlineColor ); + if( ! (aState.m_nFlags & PushFlags::TEXTALIGN ) ) + setTextAlign( aState.m_aFont.GetAlignment() ); + if( ! (aState.m_nFlags & PushFlags::TEXTFILLCOLOR) ) + setTextFillColor( aState.m_aFont.GetFillColor() ); + if( ! (aState.m_nFlags & PushFlags::REFPOINT) ) + { + // what ? + } + // invalidate graphics state + m_aGraphicsStack.front().m_nUpdateFlags = GraphicsStateUpdateFlags::All; +} + +void PDFWriterImpl::setMapMode( const MapMode& rMapMode ) +{ + m_aGraphicsStack.front().m_aMapMode = rMapMode; + SetMapMode( rMapMode ); + m_aCurrentPDFState.m_aMapMode = rMapMode; +} + +void PDFWriterImpl::setClipRegion( const basegfx::B2DPolyPolygon& rRegion ) +{ + // tdf#130150 improve coordinate manipulations to double precision transformations + const basegfx::B2DHomMatrix aCurrentTransform( + GetInverseViewTransformation(m_aMapMode) * GetViewTransformation(m_aGraphicsStack.front().m_aMapMode)); + basegfx::B2DPolyPolygon aRegion(rRegion); + + aRegion.transform(aCurrentTransform); + m_aGraphicsStack.front().m_aClipRegion = aRegion; + m_aGraphicsStack.front().m_bClipRegion = true; + m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::ClipRegion; +} + +void PDFWriterImpl::moveClipRegion( sal_Int32 nX, sal_Int32 nY ) +{ + if( m_aGraphicsStack.front().m_bClipRegion && m_aGraphicsStack.front().m_aClipRegion.count() ) + { + // tdf#130150 improve coordinate manipulations to double precision transformations + basegfx::B2DHomMatrix aConvertA; + + if(MapUnit::MapPixel == m_aGraphicsStack.front().m_aMapMode.GetMapUnit()) + { + aConvertA = GetInverseViewTransformation(m_aMapMode); + } + else + { + aConvertA = LogicToLogic(m_aGraphicsStack.front().m_aMapMode, m_aMapMode); + } + + basegfx::B2DPoint aB2DPointA(nX, nY); + basegfx::B2DPoint aB2DPointB(0.0, 0.0); + aB2DPointA *= aConvertA; + aB2DPointB *= aConvertA; + aB2DPointA -= aB2DPointB; + basegfx::B2DHomMatrix aMat; + + aMat.translate(aB2DPointA.getX(), aB2DPointA.getY()); + m_aGraphicsStack.front().m_aClipRegion.transform( aMat ); + m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::ClipRegion; + } +} + +void PDFWriterImpl::intersectClipRegion( const tools::Rectangle& rRect ) +{ + basegfx::B2DPolyPolygon aRect( basegfx::utils::createPolygonFromRect( + vcl::unotools::b2DRectangleFromRectangle(rRect) ) ); + intersectClipRegion( aRect ); +} + +void PDFWriterImpl::intersectClipRegion( const basegfx::B2DPolyPolygon& rRegion ) +{ + // tdf#130150 improve coordinate manipulations to double precision transformations + const basegfx::B2DHomMatrix aCurrentTransform( + GetInverseViewTransformation(m_aMapMode) * GetViewTransformation(m_aGraphicsStack.front().m_aMapMode)); + basegfx::B2DPolyPolygon aRegion(rRegion); + + aRegion.transform(aCurrentTransform); + m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::ClipRegion; + + if( m_aGraphicsStack.front().m_bClipRegion ) + { + basegfx::B2DPolyPolygon aOld( basegfx::utils::prepareForPolygonOperation( m_aGraphicsStack.front().m_aClipRegion ) ); + aRegion = basegfx::utils::prepareForPolygonOperation( aRegion ); + m_aGraphicsStack.front().m_aClipRegion = basegfx::utils::solvePolygonOperationAnd( aOld, aRegion ); + } + else + { + m_aGraphicsStack.front().m_aClipRegion = aRegion; + m_aGraphicsStack.front().m_bClipRegion = true; + } +} + +void PDFWriterImpl::createNote( const tools::Rectangle& rRect, const PDFNote& rNote, sal_Int32 nPageNr ) +{ + if( nPageNr < 0 ) + nPageNr = m_nCurrentPage; + + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) ) + return; + + m_aNotes.emplace_back( ); + m_aNotes.back().m_nObject = createObject(); + m_aNotes.back().m_aContents = rNote; + m_aNotes.back().m_aRect = rRect; + // convert to default user space now, since the mapmode may change + m_aPages[nPageNr].convertRect( m_aNotes.back().m_aRect ); + + // insert note to page's annotation list + m_aPages[ nPageNr ].m_aAnnotations.push_back( m_aNotes.back().m_nObject ); +} + +sal_Int32 PDFWriterImpl::createLink( const tools::Rectangle& rRect, sal_Int32 nPageNr ) +{ + if( nPageNr < 0 ) + nPageNr = m_nCurrentPage; + + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) ) + return -1; + + sal_Int32 nRet = m_aLinks.size(); + + m_aLinks.emplace_back( ); + m_aLinks.back().m_nObject = createObject(); + m_aLinks.back().m_nPage = nPageNr; + m_aLinks.back().m_aRect = rRect; + // convert to default user space now, since the mapmode may change + m_aPages[nPageNr].convertRect( m_aLinks.back().m_aRect ); + + // insert link to page's annotation list + m_aPages[ nPageNr ].m_aAnnotations.push_back( m_aLinks.back().m_nObject ); + + return nRet; +} + +sal_Int32 PDFWriterImpl::createScreen(const tools::Rectangle& rRect, sal_Int32 nPageNr) +{ + if (nPageNr < 0) + nPageNr = m_nCurrentPage; + + if (nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size())) + return -1; + + sal_Int32 nRet = m_aScreens.size(); + + m_aScreens.emplace_back(); + m_aScreens.back().m_nObject = createObject(); + m_aScreens.back().m_nPage = nPageNr; + m_aScreens.back().m_aRect = rRect; + // Convert to default user space now, since the mapmode may change. + m_aPages[nPageNr].convertRect(m_aScreens.back().m_aRect); + + // Insert link to page's annotation list. + m_aPages[nPageNr].m_aAnnotations.push_back(m_aScreens.back().m_nObject); + + return nRet; +} + +sal_Int32 PDFWriterImpl::createNamedDest( const OUString& sDestName, const tools::Rectangle& rRect, sal_Int32 nPageNr, PDFWriter::DestAreaType eType ) +{ + if( nPageNr < 0 ) + nPageNr = m_nCurrentPage; + + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) ) + return -1; + + sal_Int32 nRet = m_aNamedDests.size(); + + m_aNamedDests.emplace_back( ); + m_aNamedDests.back().m_aDestName = sDestName; + m_aNamedDests.back().m_nPage = nPageNr; + m_aNamedDests.back().m_eType = eType; + m_aNamedDests.back().m_aRect = rRect; + // convert to default user space now, since the mapmode may change + m_aPages[nPageNr].convertRect( m_aNamedDests.back().m_aRect ); + + return nRet; +} + +sal_Int32 PDFWriterImpl::createDest( const tools::Rectangle& rRect, sal_Int32 nPageNr, PDFWriter::DestAreaType eType ) +{ + if( nPageNr < 0 ) + nPageNr = m_nCurrentPage; + + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) ) + return -1; + + sal_Int32 nRet = m_aDests.size(); + + m_aDests.emplace_back( ); + m_aDests.back().m_nPage = nPageNr; + m_aDests.back().m_eType = eType; + m_aDests.back().m_aRect = rRect; + // convert to default user space now, since the mapmode may change + m_aPages[nPageNr].convertRect( m_aDests.back().m_aRect ); + + return nRet; +} + +sal_Int32 PDFWriterImpl::registerDestReference( sal_Int32 nDestId, const tools::Rectangle& rRect, sal_Int32 nPageNr, PDFWriter::DestAreaType eType ) +{ + m_aDestinationIdTranslation[ nDestId ] = createDest( rRect, nPageNr, eType ); + return m_aDestinationIdTranslation[ nDestId ]; +} + +void PDFWriterImpl::setLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId ) +{ + if( nLinkId < 0 || nLinkId >= static_cast<sal_Int32>(m_aLinks.size()) ) + return; + if( nDestId < 0 || nDestId >= static_cast<sal_Int32>(m_aDests.size()) ) + return; + + m_aLinks[ nLinkId ].m_nDest = nDestId; +} + +void PDFWriterImpl::setLinkURL( sal_Int32 nLinkId, const OUString& rURL ) +{ + if( nLinkId < 0 || nLinkId >= static_cast<sal_Int32>(m_aLinks.size()) ) + return; + + m_aLinks[ nLinkId ].m_nDest = -1; + + using namespace ::com::sun::star; + + if (!m_xTrans.is()) + { + uno::Reference< uno::XComponentContext > xContext( comphelper::getProcessComponentContext() ); + m_xTrans = util::URLTransformer::create(xContext); + } + + util::URL aURL; + aURL.Complete = rURL; + + m_xTrans->parseStrict( aURL ); + + m_aLinks[ nLinkId ].m_aURL = aURL.Complete; +} + +void PDFWriterImpl::setScreenURL(sal_Int32 nScreenId, const OUString& rURL) +{ + if (nScreenId < 0 || nScreenId >= static_cast<sal_Int32>(m_aScreens.size())) + return; + + m_aScreens[nScreenId].m_aURL = rURL; +} + +void PDFWriterImpl::setScreenStream(sal_Int32 nScreenId, const OUString& rURL) +{ + if (nScreenId < 0 || nScreenId >= static_cast<sal_Int32>(m_aScreens.size())) + return; + + m_aScreens[nScreenId].m_aTempFileURL = rURL; + m_aScreens[nScreenId].m_nTempFileObject = createObject(); +} + +void PDFWriterImpl::setLinkPropertyId( sal_Int32 nLinkId, sal_Int32 nPropertyId ) +{ + m_aLinkPropertyMap[ nPropertyId ] = nLinkId; +} + +sal_Int32 PDFWriterImpl::createOutlineItem( sal_Int32 nParent, const OUString& rText, sal_Int32 nDestID ) +{ + // create new item + sal_Int32 nNewItem = m_aOutline.size(); + m_aOutline.emplace_back( ); + + // set item attributes + setOutlineItemParent( nNewItem, nParent ); + setOutlineItemText( nNewItem, rText ); + setOutlineItemDest( nNewItem, nDestID ); + + return nNewItem; +} + +void PDFWriterImpl::setOutlineItemParent( sal_Int32 nItem, sal_Int32 nNewParent ) +{ + if( nItem < 1 || nItem >= static_cast<sal_Int32>(m_aOutline.size()) ) + return; + + if( nNewParent < 0 || nNewParent >= static_cast<sal_Int32>(m_aOutline.size()) || nNewParent == nItem ) + { + nNewParent = 0; + } + // insert item to new parent's list of children + m_aOutline[ nNewParent ].m_aChildren.push_back( nItem ); +} + +void PDFWriterImpl::setOutlineItemText( sal_Int32 nItem, const OUString& rText ) +{ + if( nItem < 1 || nItem >= static_cast<sal_Int32>(m_aOutline.size()) ) + return; + + m_aOutline[ nItem ].m_aTitle = psp::WhitespaceToSpace( rText ); +} + +void PDFWriterImpl::setOutlineItemDest( sal_Int32 nItem, sal_Int32 nDestID ) +{ + if( nItem < 1 || nItem >= static_cast<sal_Int32>(m_aOutline.size()) ) // item does not exist + return; + if( nDestID < 0 || nDestID >= static_cast<sal_Int32>(m_aDests.size()) ) // dest does not exist + return; + m_aOutline[nItem].m_nDestID = nDestID; +} + +const char* PDFWriterImpl::getStructureTag( PDFWriter::StructElement eType ) +{ + static std::map< PDFWriter::StructElement, const char* > aTagStrings; + if( aTagStrings.empty() ) + { + aTagStrings[ PDFWriter::NonStructElement] = "NonStruct"; + aTagStrings[ PDFWriter::Document ] = "Document"; + aTagStrings[ PDFWriter::Part ] = "Part"; + aTagStrings[ PDFWriter::Article ] = "Art"; + aTagStrings[ PDFWriter::Section ] = "Sect"; + aTagStrings[ PDFWriter::Division ] = "Div"; + aTagStrings[ PDFWriter::BlockQuote ] = "BlockQuote"; + aTagStrings[ PDFWriter::Caption ] = "Caption"; + aTagStrings[ PDFWriter::TOC ] = "TOC"; + aTagStrings[ PDFWriter::TOCI ] = "TOCI"; + aTagStrings[ PDFWriter::Index ] = "Index"; + aTagStrings[ PDFWriter::Paragraph ] = "P"; + aTagStrings[ PDFWriter::Heading ] = "H"; + aTagStrings[ PDFWriter::H1 ] = "H1"; + aTagStrings[ PDFWriter::H2 ] = "H2"; + aTagStrings[ PDFWriter::H3 ] = "H3"; + aTagStrings[ PDFWriter::H4 ] = "H4"; + aTagStrings[ PDFWriter::H5 ] = "H5"; + aTagStrings[ PDFWriter::H6 ] = "H6"; + aTagStrings[ PDFWriter::List ] = "L"; + aTagStrings[ PDFWriter::ListItem ] = "LI"; + aTagStrings[ PDFWriter::LILabel ] = "Lbl"; + aTagStrings[ PDFWriter::LIBody ] = "LBody"; + aTagStrings[ PDFWriter::Table ] = "Table"; + aTagStrings[ PDFWriter::TableRow ] = "TR"; + aTagStrings[ PDFWriter::TableHeader ] = "TH"; + aTagStrings[ PDFWriter::TableData ] = "TD"; + aTagStrings[ PDFWriter::Span ] = "Span"; + aTagStrings[ PDFWriter::Quote ] = "Quote"; + aTagStrings[ PDFWriter::Note ] = "Note"; + aTagStrings[ PDFWriter::Reference ] = "Reference"; + aTagStrings[ PDFWriter::BibEntry ] = "BibEntry"; + aTagStrings[ PDFWriter::Code ] = "Code"; + aTagStrings[ PDFWriter::Link ] = "Link"; + aTagStrings[ PDFWriter::Figure ] = "Figure"; + aTagStrings[ PDFWriter::Formula ] = "Formula"; + aTagStrings[ PDFWriter::Form ] = "Form"; + } + + std::map< PDFWriter::StructElement, const char* >::const_iterator it = aTagStrings.find( eType ); + + return it != aTagStrings.end() ? it->second : "Div"; +} + +void PDFWriterImpl::addRoleMap(OString aAlias, PDFWriter::StructElement eType) +{ + OString aTag = getStructureTag(eType); + // For PDF/UA it's not allowed to map an alias with the same name. + // Not sure if this allowed, necessary or recommended otherwise, so + // only enable filtering when PDF/UA is enabled. + if (!m_bIsPDF_UA || aAlias != aTag) + m_aRoleMap[aAlias] = aTag; +} + +void PDFWriterImpl::beginStructureElementMCSeq() +{ + if( m_bEmitStructure && + m_nCurrentStructElement > 0 && // StructTreeRoot + ! m_aStructure[ m_nCurrentStructElement ].m_bOpenMCSeq // already opened sequence + ) + { + PDFStructureElement& rEle = m_aStructure[ m_nCurrentStructElement ]; + OStringBuffer aLine( 128 ); + sal_Int32 nMCID = m_aPages[ m_nCurrentPage ].m_aMCIDParents.size(); + aLine.append( "/" ); + if( !rEle.m_aAlias.isEmpty() ) + aLine.append( rEle.m_aAlias ); + else + aLine.append( getStructureTag( rEle.m_eType ) ); + aLine.append( "<</MCID " ); + aLine.append( nMCID ); + aLine.append( ">>BDC\n" ); + writeBuffer( aLine.getStr(), aLine.getLength() ); + + // update the element's content list + SAL_INFO("vcl.pdfwriter", "beginning marked content id " << nMCID << " on page object " + << m_aPages[ m_nCurrentPage ].m_nPageObject << ", structure first page = " + << rEle.m_nFirstPageObject); + rEle.m_aKids.emplace_back( nMCID, m_aPages[m_nCurrentPage].m_nPageObject ); + // update the page's mcid parent list + m_aPages[ m_nCurrentPage ].m_aMCIDParents.push_back( rEle.m_nObject ); + // mark element MC sequence as open + rEle.m_bOpenMCSeq = true; + } + // handle artifacts + else if( ! m_bEmitStructure && m_aContext.Tagged && + m_nCurrentStructElement > 0 && + m_aStructure[ m_nCurrentStructElement ].m_eType == PDFWriter::NonStructElement && + ! m_aStructure[ m_nCurrentStructElement ].m_bOpenMCSeq // already opened sequence + ) + { + OString aLine = "/Artifact BMC\n"; + writeBuffer( aLine.getStr(), aLine.getLength() ); + // mark element MC sequence as open + m_aStructure[ m_nCurrentStructElement ].m_bOpenMCSeq = true; + } +} + +void PDFWriterImpl::endStructureElementMCSeq() +{ + if( m_nCurrentStructElement > 0 && // StructTreeRoot + ( m_bEmitStructure || m_aStructure[ m_nCurrentStructElement ].m_eType == PDFWriter::NonStructElement ) && + m_aStructure[ m_nCurrentStructElement ].m_bOpenMCSeq // must have an opened MC sequence + ) + { + writeBuffer( "EMC\n", 4 ); + m_aStructure[ m_nCurrentStructElement ].m_bOpenMCSeq = false; + } +} + +bool PDFWriterImpl::checkEmitStructure() +{ + bool bEmit = false; + if( m_aContext.Tagged ) + { + bEmit = true; + sal_Int32 nEle = m_nCurrentStructElement; + while( nEle > 0 && nEle < sal_Int32(m_aStructure.size()) ) + { + if( m_aStructure[ nEle ].m_eType == PDFWriter::NonStructElement ) + { + bEmit = false; + break; + } + nEle = m_aStructure[ nEle ].m_nParentElement; + } + } + return bEmit; +} + +sal_Int32 PDFWriterImpl::beginStructureElement( PDFWriter::StructElement eType, const OUString& rAlias ) +{ + if( m_nCurrentPage < 0 ) + return -1; + + if( ! m_aContext.Tagged ) + return -1; + + // close eventual current MC sequence + endStructureElementMCSeq(); + + if( m_nCurrentStructElement == 0 && + eType != PDFWriter::Document && eType != PDFWriter::NonStructElement ) + { + // struct tree root hit, but not beginning document + // this might happen with setCurrentStructureElement + // silently insert structure into document again if one properly exists + if( ! m_aStructure[ 0 ].m_aChildren.empty() ) + { + const std::list< sal_Int32 >& rRootChildren = m_aStructure[0].m_aChildren; + auto it = std::find_if(rRootChildren.begin(), rRootChildren.end(), + [&](sal_Int32 nElement) { return m_aStructure[ nElement ].m_eType == PDFWriter::Document; }); + if( it != rRootChildren.end() ) + { + m_nCurrentStructElement = *it; + SAL_WARN( "vcl.pdfwriter", "Structure element inserted to StructTreeRoot that is not a document" ); + } + else { + OSL_FAIL( "document structure in disorder !" ); + } + } + else { + OSL_FAIL( "PDF document structure MUST be contained in a Document element" ); + } + } + + sal_Int32 nNewId = sal_Int32(m_aStructure.size()); + m_aStructure.emplace_back( ); + PDFStructureElement& rEle = m_aStructure.back(); + rEle.m_eType = eType; + rEle.m_nOwnElement = nNewId; + rEle.m_nParentElement = m_nCurrentStructElement; + rEle.m_nFirstPageObject = m_aPages[ m_nCurrentPage ].m_nPageObject; + m_aStructure[ m_nCurrentStructElement ].m_aChildren.push_back( nNewId ); + m_nCurrentStructElement = nNewId; + + // handle alias names + if( !rAlias.isEmpty() && eType != PDFWriter::NonStructElement ) + { + OStringBuffer aNameBuf( rAlias.getLength() ); + appendName( rAlias, aNameBuf ); + OString aAliasName( aNameBuf.makeStringAndClear() ); + rEle.m_aAlias = aAliasName; + addRoleMap(aAliasName, eType); + } + + if (g_bDebugDisableCompression) + { + OStringBuffer aLine( "beginStructureElement " ); + aLine.append( m_nCurrentStructElement ); + aLine.append( ": " ); + aLine.append( getStructureTag( eType ) ); + if( !rEle.m_aAlias.isEmpty() ) + { + aLine.append( " aliased as \"" ); + aLine.append( rEle.m_aAlias ); + aLine.append( '\"' ); + } + emitComment( aLine.getStr() ); + } + + // check whether to emit structure henceforth + m_bEmitStructure = checkEmitStructure(); + + if( m_bEmitStructure ) // don't create nonexistent objects + { + rEle.m_nObject = createObject(); + // update parent's kids list + m_aStructure[ rEle.m_nParentElement ].m_aKids.emplace_back(rEle.m_nObject); + } + return nNewId; +} + +void PDFWriterImpl::endStructureElement() +{ + if( m_nCurrentPage < 0 ) + return; + + if( ! m_aContext.Tagged ) + return; + + if( m_nCurrentStructElement == 0 ) + { + // hit the struct tree root, that means there is an endStructureElement + // without corresponding beginStructureElement + return; + } + + // end the marked content sequence + endStructureElementMCSeq(); + + OStringBuffer aLine; + if (g_bDebugDisableCompression) + { + aLine.append( "endStructureElement " ); + aLine.append( m_nCurrentStructElement ); + aLine.append( ": " ); + aLine.append( getStructureTag( m_aStructure[ m_nCurrentStructElement ].m_eType ) ); + if( !m_aStructure[ m_nCurrentStructElement ].m_aAlias.isEmpty() ) + { + aLine.append( " aliased as \"" ); + aLine.append( m_aStructure[ m_nCurrentStructElement ].m_aAlias ); + aLine.append( '\"' ); + } + } + + // "end" the structure element, the parent becomes current element + m_nCurrentStructElement = m_aStructure[ m_nCurrentStructElement ].m_nParentElement; + + // check whether to emit structure henceforth + m_bEmitStructure = checkEmitStructure(); + + if (g_bDebugDisableCompression && m_bEmitStructure) + { + emitComment( aLine.getStr() ); + } +} + +/* + * This function adds an internal structure list container to overcome the 8191 elements array limitation + * in kids element emission. + * Recursive function + * + */ +void PDFWriterImpl::addInternalStructureContainer( PDFStructureElement& rEle ) +{ + if( rEle.m_eType == PDFWriter::NonStructElement && + rEle.m_nOwnElement != rEle.m_nParentElement ) + return; + + for (auto const& child : rEle.m_aChildren) + { + if( child > 0 && child < sal_Int32(m_aStructure.size()) ) + { + PDFStructureElement& rChild = m_aStructure[ child ]; + if( rChild.m_eType != PDFWriter::NonStructElement ) + { + //triggered when a child of the rEle element is found + if( rChild.m_nParentElement == rEle.m_nOwnElement ) + addInternalStructureContainer( rChild );//examine the child + else + { + OSL_FAIL( "PDFWriterImpl::addInternalStructureContainer: invalid child structure element" ); + SAL_INFO("vcl.pdfwriter", "PDFWriterImpl::addInternalStructureContainer: invalid child structure element with id " << child ); + } + } + } + else + { + OSL_FAIL( "PDFWriterImpl::emitStructure: invalid child structure id" ); + SAL_INFO("vcl.pdfwriter", "PDFWriterImpl::addInternalStructureContainer: invalid child structure id " << child ); + } + } + + if( rEle.m_nOwnElement != rEle.m_nParentElement ) + { + if( !rEle.m_aKids.empty() ) + { + if( rEle.m_aKids.size() > ncMaxPDFArraySize ) { + //then we need to add the containers for the kids elements + // a list to be used for the new kid element + std::list< PDFStructureElementKid > aNewKids; + std::list< sal_Int32 > aNewChildren; + + // add Div in RoleMap, in case no one else did (TODO: is it needed? Is it dangerous?) + OString aAliasName("Div"); + addRoleMap(aAliasName, PDFWriter::Division); + + while( rEle.m_aKids.size() > ncMaxPDFArraySize ) + { + sal_Int32 nCurrentStructElement = rEle.m_nOwnElement; + sal_Int32 nNewId = sal_Int32(m_aStructure.size()); + m_aStructure.emplace_back( ); + PDFStructureElement& rEleNew = m_aStructure.back(); + rEleNew.m_aAlias = aAliasName; + rEleNew.m_eType = PDFWriter::Division; // a new Div type container + rEleNew.m_nOwnElement = nNewId; + rEleNew.m_nParentElement = nCurrentStructElement; + //inherit the same page as the first child to be reparented + rEleNew.m_nFirstPageObject = m_aStructure[ rEle.m_aChildren.front() ].m_nFirstPageObject; + rEleNew.m_nObject = createObject();//assign a PDF object number + //add the object to the kid list of the parent + aNewKids.emplace_back( rEleNew.m_nObject ); + aNewChildren.push_back( nNewId ); + + std::list< sal_Int32 >::iterator aChildEndIt( rEle.m_aChildren.begin() ); + std::list< PDFStructureElementKid >::iterator aKidEndIt( rEle.m_aKids.begin() ); + advance( aChildEndIt, ncMaxPDFArraySize ); + advance( aKidEndIt, ncMaxPDFArraySize ); + + rEleNew.m_aKids.splice( rEleNew.m_aKids.begin(), + rEle.m_aKids, + rEle.m_aKids.begin(), + aKidEndIt ); + rEleNew.m_aChildren.splice( rEleNew.m_aChildren.begin(), + rEle.m_aChildren, + rEle.m_aChildren.begin(), + aChildEndIt ); + // set the kid's new parent + for (auto const& child : rEleNew.m_aChildren) + { + m_aStructure[ child ].m_nParentElement = nNewId; + } + } + //finally add the new kids resulting from the container added + rEle.m_aKids.insert( rEle.m_aKids.begin(), aNewKids.begin(), aNewKids.end() ); + rEle.m_aChildren.insert( rEle.m_aChildren.begin(), aNewChildren.begin(), aNewChildren.end() ); + } + } + } +} + +bool PDFWriterImpl::setCurrentStructureElement( sal_Int32 nEle ) +{ + bool bSuccess = false; + + if( m_aContext.Tagged && nEle >= 0 && nEle < sal_Int32(m_aStructure.size()) ) + { + // end eventual previous marked content sequence + endStructureElementMCSeq(); + + m_nCurrentStructElement = nEle; + m_bEmitStructure = checkEmitStructure(); + if (g_bDebugDisableCompression) + { + OStringBuffer aLine( "setCurrentStructureElement " ); + aLine.append( m_nCurrentStructElement ); + aLine.append( ": " ); + aLine.append( getStructureTag( m_aStructure[ m_nCurrentStructElement ].m_eType ) ); + if( !m_aStructure[ m_nCurrentStructElement ].m_aAlias.isEmpty() ) + { + aLine.append( " aliased as \"" ); + aLine.append( m_aStructure[ m_nCurrentStructElement ].m_aAlias ); + aLine.append( '\"' ); + } + if( ! m_bEmitStructure ) + aLine.append( " (inside NonStruct)" ); + emitComment( aLine.getStr() ); + } + bSuccess = true; + } + + return bSuccess; +} + +bool PDFWriterImpl::setStructureAttribute( enum PDFWriter::StructAttribute eAttr, enum PDFWriter::StructAttributeValue eVal ) +{ + if( !m_aContext.Tagged ) + return false; + + bool bInsert = false; + if( m_nCurrentStructElement > 0 && m_bEmitStructure ) + { + PDFWriter::StructElement eType = m_aStructure[ m_nCurrentStructElement ].m_eType; + switch( eAttr ) + { + case PDFWriter::Placement: + if( eVal == PDFWriter::Block || + eVal == PDFWriter::Inline || + eVal == PDFWriter::Before || + eVal == PDFWriter::Start || + eVal == PDFWriter::End ) + bInsert = true; + break; + case PDFWriter::WritingMode: + if( eVal == PDFWriter::LrTb || + eVal == PDFWriter::RlTb || + eVal == PDFWriter::TbRl ) + { + bInsert = true; + } + break; + case PDFWriter::TextAlign: + if( eVal == PDFWriter::Start || + eVal == PDFWriter::Center || + eVal == PDFWriter::End || + eVal == PDFWriter::Justify ) + { + if( eType == PDFWriter::Paragraph || + eType == PDFWriter::Heading || + eType == PDFWriter::H1 || + eType == PDFWriter::H2 || + eType == PDFWriter::H3 || + eType == PDFWriter::H4 || + eType == PDFWriter::H5 || + eType == PDFWriter::H6 || + eType == PDFWriter::List || + eType == PDFWriter::ListItem || + eType == PDFWriter::LILabel || + eType == PDFWriter::LIBody || + eType == PDFWriter::Table || + eType == PDFWriter::TableRow || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + } + break; + case PDFWriter::Width: + case PDFWriter::Height: + if( eVal == PDFWriter::Auto ) + { + if( eType == PDFWriter::Figure || + eType == PDFWriter::Formula || + eType == PDFWriter::Form || + eType == PDFWriter::Table || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + } + break; + case PDFWriter::BlockAlign: + if( eVal == PDFWriter::Before || + eVal == PDFWriter::Middle || + eVal == PDFWriter::After || + eVal == PDFWriter::Justify ) + { + if( eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + } + break; + case PDFWriter::InlineAlign: + if( eVal == PDFWriter::Start || + eVal == PDFWriter::Center || + eVal == PDFWriter::End ) + { + if( eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + } + break; + case PDFWriter::LineHeight: + if( eVal == PDFWriter::Normal || + eVal == PDFWriter::Auto ) + { + // only for ILSE and BLSE + if( eType == PDFWriter::Paragraph || + eType == PDFWriter::Heading || + eType == PDFWriter::H1 || + eType == PDFWriter::H2 || + eType == PDFWriter::H3 || + eType == PDFWriter::H4 || + eType == PDFWriter::H5 || + eType == PDFWriter::H6 || + eType == PDFWriter::List || + eType == PDFWriter::ListItem || + eType == PDFWriter::LILabel || + eType == PDFWriter::LIBody || + eType == PDFWriter::Table || + eType == PDFWriter::TableRow || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData || + eType == PDFWriter::Span || + eType == PDFWriter::Quote || + eType == PDFWriter::Note || + eType == PDFWriter::Reference || + eType == PDFWriter::BibEntry || + eType == PDFWriter::Code || + eType == PDFWriter::Link ) + { + bInsert = true; + } + } + break; + case PDFWriter::TextDecorationType: + if( eVal == PDFWriter::NONE || + eVal == PDFWriter::Underline || + eVal == PDFWriter::Overline || + eVal == PDFWriter::LineThrough ) + { + // only for ILSE and BLSE + if( eType == PDFWriter::Paragraph || + eType == PDFWriter::Heading || + eType == PDFWriter::H1 || + eType == PDFWriter::H2 || + eType == PDFWriter::H3 || + eType == PDFWriter::H4 || + eType == PDFWriter::H5 || + eType == PDFWriter::H6 || + eType == PDFWriter::List || + eType == PDFWriter::ListItem || + eType == PDFWriter::LILabel || + eType == PDFWriter::LIBody || + eType == PDFWriter::Table || + eType == PDFWriter::TableRow || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData || + eType == PDFWriter::Span || + eType == PDFWriter::Quote || + eType == PDFWriter::Note || + eType == PDFWriter::Reference || + eType == PDFWriter::BibEntry || + eType == PDFWriter::Code || + eType == PDFWriter::Link ) + { + bInsert = true; + } + } + break; + case PDFWriter::ListNumbering: + if( eVal == PDFWriter::NONE || + eVal == PDFWriter::Disc || + eVal == PDFWriter::Circle || + eVal == PDFWriter::Square || + eVal == PDFWriter::Decimal || + eVal == PDFWriter::UpperRoman || + eVal == PDFWriter::LowerRoman || + eVal == PDFWriter::UpperAlpha || + eVal == PDFWriter::LowerAlpha ) + { + if( eType == PDFWriter::List ) + bInsert = true; + } + break; + default: break; + } + } + + if( bInsert ) + m_aStructure[ m_nCurrentStructElement ].m_aAttributes[ eAttr ] = PDFStructureAttribute( eVal ); + else if( m_nCurrentStructElement > 0 && m_bEmitStructure ) + SAL_INFO("vcl.pdfwriter", + "rejecting setStructureAttribute( " << getAttributeTag( eAttr ) + << ", " << getAttributeValueTag( eVal ) + << " ) on " << getStructureTag( m_aStructure[ m_nCurrentStructElement ].m_eType ) + << " (" << m_aStructure[ m_nCurrentStructElement ].m_aAlias + << ") element"); + + return bInsert; +} + +bool PDFWriterImpl::setStructureAttributeNumerical( enum PDFWriter::StructAttribute eAttr, sal_Int32 nValue ) +{ + if( ! m_aContext.Tagged ) + return false; + + bool bInsert = false; + if( m_nCurrentStructElement > 0 && m_bEmitStructure ) + { + if( eAttr == PDFWriter::Language ) + { + m_aStructure[ m_nCurrentStructElement ].m_aLocale = LanguageTag( LanguageType(nValue) ).getLocale(); + return true; + } + + PDFWriter::StructElement eType = m_aStructure[ m_nCurrentStructElement ].m_eType; + switch( eAttr ) + { + case PDFWriter::SpaceBefore: + case PDFWriter::SpaceAfter: + case PDFWriter::StartIndent: + case PDFWriter::EndIndent: + // just for BLSE + if( eType == PDFWriter::Paragraph || + eType == PDFWriter::Heading || + eType == PDFWriter::H1 || + eType == PDFWriter::H2 || + eType == PDFWriter::H3 || + eType == PDFWriter::H4 || + eType == PDFWriter::H5 || + eType == PDFWriter::H6 || + eType == PDFWriter::List || + eType == PDFWriter::ListItem || + eType == PDFWriter::LILabel || + eType == PDFWriter::LIBody || + eType == PDFWriter::Table || + eType == PDFWriter::TableRow || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + break; + case PDFWriter::TextIndent: + // paragraph like BLSE and additional elements + if( eType == PDFWriter::Paragraph || + eType == PDFWriter::Heading || + eType == PDFWriter::H1 || + eType == PDFWriter::H2 || + eType == PDFWriter::H3 || + eType == PDFWriter::H4 || + eType == PDFWriter::H5 || + eType == PDFWriter::H6 || + eType == PDFWriter::LILabel || + eType == PDFWriter::LIBody || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + break; + case PDFWriter::Width: + case PDFWriter::Height: + if( eType == PDFWriter::Figure || + eType == PDFWriter::Formula || + eType == PDFWriter::Form || + eType == PDFWriter::Table || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + break; + case PDFWriter::LineHeight: + case PDFWriter::BaselineShift: + // only for ILSE and BLSE + if( eType == PDFWriter::Paragraph || + eType == PDFWriter::Heading || + eType == PDFWriter::H1 || + eType == PDFWriter::H2 || + eType == PDFWriter::H3 || + eType == PDFWriter::H4 || + eType == PDFWriter::H5 || + eType == PDFWriter::H6 || + eType == PDFWriter::List || + eType == PDFWriter::ListItem || + eType == PDFWriter::LILabel || + eType == PDFWriter::LIBody || + eType == PDFWriter::Table || + eType == PDFWriter::TableRow || + eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData || + eType == PDFWriter::Span || + eType == PDFWriter::Quote || + eType == PDFWriter::Note || + eType == PDFWriter::Reference || + eType == PDFWriter::BibEntry || + eType == PDFWriter::Code || + eType == PDFWriter::Link ) + { + bInsert = true; + } + break; + case PDFWriter::RowSpan: + case PDFWriter::ColSpan: + // only for table cells + if( eType == PDFWriter::TableHeader || + eType == PDFWriter::TableData ) + { + bInsert = true; + } + break; + case PDFWriter::LinkAnnotation: + if( eType == PDFWriter::Link ) + bInsert = true; + break; + default: break; + } + } + + if( bInsert ) + m_aStructure[ m_nCurrentStructElement ].m_aAttributes[ eAttr ] = PDFStructureAttribute( nValue ); + else if( m_nCurrentStructElement > 0 && m_bEmitStructure ) + SAL_INFO("vcl.pdfwriter", + "rejecting setStructureAttributeNumerical( " << getAttributeTag( eAttr ) + << ", " << static_cast<int>(nValue) + << " ) on " << getStructureTag( m_aStructure[ m_nCurrentStructElement ].m_eType ) + << " (" << m_aStructure[ m_nCurrentStructElement ].m_aAlias + << ") element"); + + return bInsert; +} + +void PDFWriterImpl::setStructureBoundingBox( const tools::Rectangle& rRect ) +{ + sal_Int32 nPageNr = m_nCurrentPage; + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) || !m_aContext.Tagged ) + return; + + if( m_nCurrentStructElement > 0 && m_bEmitStructure ) + { + PDFWriter::StructElement eType = m_aStructure[ m_nCurrentStructElement ].m_eType; + if( eType == PDFWriter::Figure || + eType == PDFWriter::Formula || + eType == PDFWriter::Form || + eType == PDFWriter::Table ) + { + m_aStructure[ m_nCurrentStructElement ].m_aBBox = rRect; + // convert to default user space now, since the mapmode may change + m_aPages[nPageNr].convertRect( m_aStructure[ m_nCurrentStructElement ].m_aBBox ); + } + } +} + +void PDFWriterImpl::setActualText( const OUString& rText ) +{ + if( m_aContext.Tagged && m_nCurrentStructElement > 0 && m_bEmitStructure ) + { + m_aStructure[ m_nCurrentStructElement ].m_aActualText = rText; + } +} + +void PDFWriterImpl::setAlternateText( const OUString& rText ) +{ + if( m_aContext.Tagged && m_nCurrentStructElement > 0 && m_bEmitStructure ) + { + m_aStructure[ m_nCurrentStructElement ].m_aAltText = rText; + } +} + +void PDFWriterImpl::setPageTransition( PDFWriter::PageTransition eType, sal_uInt32 nMilliSec, sal_Int32 nPageNr ) +{ + if( nPageNr < 0 ) + nPageNr = m_nCurrentPage; + + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) ) + return; + + m_aPages[ nPageNr ].m_eTransition = eType; + m_aPages[ nPageNr ].m_nTransTime = nMilliSec; +} + +void PDFWriterImpl::ensureUniqueRadioOnValues() +{ + // loop over radio groups + for (auto const& group : m_aRadioGroupWidgets) + { + PDFWidget& rGroupWidget = m_aWidgets[ group.second ]; + // check whether all kids have a unique OnValue + std::unordered_map< OUString, sal_Int32 > aOnValues; + bool bIsUnique = true; + for (auto const& nKidIndex : rGroupWidget.m_aKidsIndex) + { + const OUString& rVal = m_aWidgets[nKidIndex].m_aOnValue; + SAL_INFO("vcl.pdfwriter", "OnValue: " << rVal); + if( aOnValues.find( rVal ) == aOnValues.end() ) + { + aOnValues[ rVal ] = 1; + } + else + { + bIsUnique = false; + break; + } + } + if( ! bIsUnique ) + { + SAL_INFO("vcl.pdfwriter", "enforcing unique OnValues" ); + // make unique by using ascending OnValues + int nKid = 0; + for (auto const& nKidIndex : rGroupWidget.m_aKidsIndex) + { + PDFWidget& rKid = m_aWidgets[nKidIndex]; + rKid.m_aOnValue = OUString::number( nKid+1 ); + if( rKid.m_aValue != "Off" ) + rKid.m_aValue = rKid.m_aOnValue; + ++nKid; + } + } + // finally move the "Yes" appearance to the OnValue appearance + for (auto const& nKidIndex : rGroupWidget.m_aKidsIndex) + { + PDFWidget& rKid = m_aWidgets[nKidIndex]; + auto app_it = rKid.m_aAppearances.find( "N" ); + if( app_it != rKid.m_aAppearances.end() ) + { + auto stream_it = app_it->second.find( "Yes" ); + if( stream_it != app_it->second.end() ) + { + SvMemoryStream* pStream = stream_it->second; + app_it->second.erase( stream_it ); + OStringBuffer aBuf( rKid.m_aOnValue.getLength()*2 ); + appendName( rKid.m_aOnValue, aBuf ); + (app_it->second)[ aBuf.makeStringAndClear() ] = pStream; + } + else + SAL_INFO("vcl.pdfwriter", "error: RadioButton without \"Yes\" stream" ); + } + // update selected radio button + if( rKid.m_aValue != "Off" ) + { + rGroupWidget.m_aValue = rKid.m_aValue; + } + } + } +} + +sal_Int32 PDFWriterImpl::findRadioGroupWidget( const PDFWriter::RadioButtonWidget& rBtn ) +{ + sal_Int32 nRadioGroupWidget = -1; + + std::map< sal_Int32, sal_Int32 >::const_iterator it = m_aRadioGroupWidgets.find( rBtn.RadioGroup ); + + if( it == m_aRadioGroupWidgets.end() ) + { + m_aRadioGroupWidgets[ rBtn.RadioGroup ] = nRadioGroupWidget = + sal_Int32(m_aWidgets.size()); + + // new group, insert the radiobutton + m_aWidgets.emplace_back( ); + m_aWidgets.back().m_nObject = createObject(); + m_aWidgets.back().m_nPage = m_nCurrentPage; + m_aWidgets.back().m_eType = PDFWriter::RadioButton; + m_aWidgets.back().m_nRadioGroup = rBtn.RadioGroup; + m_aWidgets.back().m_nFlags |= 0x0000C000; // NoToggleToOff and Radio bits + + createWidgetFieldName( sal_Int32(m_aWidgets.size()-1), rBtn ); + } + else + nRadioGroupWidget = it->second; + + return nRadioGroupWidget; +} + +sal_Int32 PDFWriterImpl::createControl( const PDFWriter::AnyWidget& rControl, sal_Int32 nPageNr ) +{ + if( nPageNr < 0 ) + nPageNr = m_nCurrentPage; + + if( nPageNr < 0 || nPageNr >= static_cast<sal_Int32>(m_aPages.size()) ) + return -1; + + bool sigHidden(true); + sal_Int32 nNewWidget = m_aWidgets.size(); + m_aWidgets.emplace_back( ); + + m_aWidgets.back().m_nObject = createObject(); + m_aWidgets.back().m_aRect = rControl.Location; + m_aWidgets.back().m_nPage = nPageNr; + m_aWidgets.back().m_eType = rControl.getType(); + + sal_Int32 nRadioGroupWidget = -1; + // for unknown reasons the radio buttons of a radio group must not have a + // field name, else the buttons are in fact check boxes - + // that is multiple buttons of the radio group can be selected + if( rControl.getType() == PDFWriter::RadioButton ) + nRadioGroupWidget = findRadioGroupWidget( static_cast<const PDFWriter::RadioButtonWidget&>(rControl) ); + else + { + createWidgetFieldName( nNewWidget, rControl ); + } + + // caution: m_aWidgets must not be changed after here or rNewWidget may be invalid + PDFWidget& rNewWidget = m_aWidgets[nNewWidget]; + rNewWidget.m_aDescription = rControl.Description; + rNewWidget.m_aText = rControl.Text; + rNewWidget.m_nTextStyle = rControl.TextStyle & + ( DrawTextFlags::Left | DrawTextFlags::Center | DrawTextFlags::Right | DrawTextFlags::Top | + DrawTextFlags::VCenter | DrawTextFlags::Bottom | + DrawTextFlags::MultiLine | DrawTextFlags::WordBreak ); + rNewWidget.m_nTabOrder = rControl.TabOrder; + + // various properties are set via the flags (/Ff) property of the field dict + if( rControl.ReadOnly ) + rNewWidget.m_nFlags |= 1; + if( rControl.getType() == PDFWriter::PushButton ) + { + const PDFWriter::PushButtonWidget& rBtn = static_cast<const PDFWriter::PushButtonWidget&>(rControl); + if( rNewWidget.m_nTextStyle == DrawTextFlags::NONE ) + rNewWidget.m_nTextStyle = + DrawTextFlags::Center | DrawTextFlags::VCenter | + DrawTextFlags::MultiLine | DrawTextFlags::WordBreak; + + rNewWidget.m_nFlags |= 0x00010000; + if( !rBtn.URL.isEmpty() ) + rNewWidget.m_aListEntries.push_back( rBtn.URL ); + rNewWidget.m_bSubmit = rBtn.Submit; + rNewWidget.m_bSubmitGet = rBtn.SubmitGet; + rNewWidget.m_nDest = rBtn.Dest; + createDefaultPushButtonAppearance( rNewWidget, rBtn ); + } + else if( rControl.getType() == PDFWriter::RadioButton ) + { + const PDFWriter::RadioButtonWidget& rBtn = static_cast<const PDFWriter::RadioButtonWidget&>(rControl); + if( rNewWidget.m_nTextStyle == DrawTextFlags::NONE ) + rNewWidget.m_nTextStyle = + DrawTextFlags::VCenter | DrawTextFlags::MultiLine | DrawTextFlags::WordBreak; + /* PDF sees a RadioButton group as one radio button with + * children which are in turn check boxes + * + * so we need to create a radio button on demand for a new group + * and insert a checkbox for each RadioButtonWidget as its child + */ + rNewWidget.m_eType = PDFWriter::CheckBox; + rNewWidget.m_nRadioGroup = rBtn.RadioGroup; + + SAL_WARN_IF( nRadioGroupWidget < 0 || nRadioGroupWidget >= static_cast<sal_Int32>(m_aWidgets.size()), "vcl.pdfwriter", "no radio group parent" ); + + PDFWidget& rRadioButton = m_aWidgets[nRadioGroupWidget]; + rRadioButton.m_aKids.push_back( rNewWidget.m_nObject ); + rRadioButton.m_aKidsIndex.push_back( nNewWidget ); + rNewWidget.m_nParent = rRadioButton.m_nObject; + + rNewWidget.m_aValue = "Off"; + rNewWidget.m_aOnValue = rBtn.OnValue; + if( rRadioButton.m_aValue.isEmpty() && rBtn.Selected ) + { + rNewWidget.m_aValue = rNewWidget.m_aOnValue; + rRadioButton.m_aValue = rNewWidget.m_aOnValue; + } + createDefaultRadioButtonAppearance( rNewWidget, rBtn ); + + // union rect of radio group + tools::Rectangle aRect = rNewWidget.m_aRect; + m_aPages[ nPageNr ].convertRect( aRect ); + rRadioButton.m_aRect.Union( aRect ); + } + else if( rControl.getType() == PDFWriter::CheckBox ) + { + const PDFWriter::CheckBoxWidget& rBox = static_cast<const PDFWriter::CheckBoxWidget&>(rControl); + if( rNewWidget.m_nTextStyle == DrawTextFlags::NONE ) + rNewWidget.m_nTextStyle = + DrawTextFlags::VCenter | DrawTextFlags::MultiLine | DrawTextFlags::WordBreak; + + rNewWidget.m_aValue = rBox.Checked ? OUStringLiteral("Yes") : OUStringLiteral("Off" ); + // create default appearance before m_aRect gets transformed + createDefaultCheckBoxAppearance( rNewWidget, rBox ); + } + else if( rControl.getType() == PDFWriter::ListBox ) + { + if( rNewWidget.m_nTextStyle == DrawTextFlags::NONE ) + rNewWidget.m_nTextStyle = DrawTextFlags::VCenter; + + const PDFWriter::ListBoxWidget& rLstBox = static_cast<const PDFWriter::ListBoxWidget&>(rControl); + rNewWidget.m_aListEntries = rLstBox.Entries; + rNewWidget.m_aSelectedEntries = rLstBox.SelectedEntries; + rNewWidget.m_aValue = rLstBox.Text; + if( rLstBox.DropDown ) + rNewWidget.m_nFlags |= 0x00020000; + if( rLstBox.MultiSelect && !rLstBox.DropDown && m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 ) + rNewWidget.m_nFlags |= 0x00200000; + + createDefaultListBoxAppearance( rNewWidget, rLstBox ); + } + else if( rControl.getType() == PDFWriter::ComboBox ) + { + if( rNewWidget.m_nTextStyle == DrawTextFlags::NONE ) + rNewWidget.m_nTextStyle = DrawTextFlags::VCenter; + + const PDFWriter::ComboBoxWidget& rBox = static_cast<const PDFWriter::ComboBoxWidget&>(rControl); + rNewWidget.m_aValue = rBox.Text; + rNewWidget.m_aListEntries = rBox.Entries; + rNewWidget.m_nFlags |= 0x00060000; // combo and edit flag + + PDFWriter::ListBoxWidget aLBox; + aLBox.Name = rBox.Name; + aLBox.Description = rBox.Description; + aLBox.Text = rBox.Text; + aLBox.TextStyle = rBox.TextStyle; + aLBox.ReadOnly = rBox.ReadOnly; + aLBox.Border = rBox.Border; + aLBox.BorderColor = rBox.BorderColor; + aLBox.Background = rBox.Background; + aLBox.BackgroundColor = rBox.BackgroundColor; + aLBox.TextFont = rBox.TextFont; + aLBox.TextColor = rBox.TextColor; + aLBox.DropDown = true; + aLBox.MultiSelect = false; + aLBox.Entries = rBox.Entries; + + createDefaultListBoxAppearance( rNewWidget, aLBox ); + } + else if( rControl.getType() == PDFWriter::Edit ) + { + if( rNewWidget.m_nTextStyle == DrawTextFlags::NONE ) + rNewWidget.m_nTextStyle = DrawTextFlags::Left | DrawTextFlags::VCenter; + + const PDFWriter::EditWidget& rEdit = static_cast<const PDFWriter::EditWidget&>(rControl); + if( rEdit.MultiLine ) + { + rNewWidget.m_nFlags |= 0x00001000; + rNewWidget.m_nTextStyle |= DrawTextFlags::MultiLine | DrawTextFlags::WordBreak; + } + if( rEdit.Password ) + rNewWidget.m_nFlags |= 0x00002000; + if( rEdit.FileSelect && m_aContext.Version > PDFWriter::PDFVersion::PDF_1_3 ) + rNewWidget.m_nFlags |= 0x00100000; + rNewWidget.m_nMaxLen = rEdit.MaxLen; + rNewWidget.m_aValue = rEdit.Text; + + createDefaultEditAppearance( rNewWidget, rEdit ); + } +#if HAVE_FEATURE_NSS + else if( rControl.getType() == PDFWriter::Signature) + { + sigHidden = true; + + rNewWidget.m_aRect = tools::Rectangle(0, 0, 0, 0); + + m_nSignatureObject = createObject(); + rNewWidget.m_aValue = OUString::number( m_nSignatureObject ); + rNewWidget.m_aValue += " 0 R"; + // let's add a fake appearance + rNewWidget.m_aAppearances[ "N" ][ "Standard" ] = new SvMemoryStream(); + } +#endif + + // if control is a hidden signature, do not convert coordinates since we + // need /Rect [ 0 0 0 0 ] + if ( ! ( ( rControl.getType() == PDFWriter::Signature ) && sigHidden ) ) + { + // convert to default user space now, since the mapmode may change + // note: create default appearances before m_aRect gets transformed + m_aPages[ nPageNr ].convertRect( rNewWidget.m_aRect ); + } + + // insert widget to page's annotation list + m_aPages[ nPageNr ].m_aAnnotations.push_back( rNewWidget.m_nObject ); + + return nNewWidget; +} + +void PDFWriterImpl::addStream( const OUString& rMimeType, PDFOutputStream* pStream ) +{ + if( pStream ) + { + m_aAdditionalStreams.emplace_back( ); + PDFAddStream& rStream = m_aAdditionalStreams.back(); + rStream.m_aMimeType = !rMimeType.isEmpty() + ? rMimeType + : OUString( "application/octet-stream" ); + rStream.m_pStream = pStream; + rStream.m_bCompress = false; + } +} + +void PDFWriterImpl::MARK( const char* pString ) +{ + beginStructureElementMCSeq(); + if (g_bDebugDisableCompression) + emitComment( pString ); +} + +sal_Int32 ReferenceXObjectEmit::getObject() const +{ + if (m_nFormObject > 0) + return m_nFormObject; + else + return m_nBitmapObject; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |