/* -*- 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 #include #include #include #include #include #include struct ImplTileInfo { ImplTileInfo() : nTilesEmptyX(0), nTilesEmptyY(0) {} Point aTileTopLeft; // top, left position of the rendered tile Point aNextTileTopLeft; // top, left position for next recursion // level's tile Size aTileSizePixel; // size of the generated tile (might // differ from // aNextTileTopLeft-aTileTopLeft, because // this is nExponent*prevTileSize. The // generated tile is always nExponent // times the previous tile, such that it // can be used in the next stage. The // required area coverage is often // less. The extraneous area covered is // later overwritten by the next stage) int nTilesEmptyX; // number of original tiles empty right of // this tile. This counts from // aNextTileTopLeft, i.e. the additional // area covered by aTileSizePixel is not // considered here. This is for // unification purposes, as the iterative // calculation of the next level's empty // tiles has to be based on this value. int nTilesEmptyY; // as above, for Y }; bool GraphicObject::ImplRenderTempTile( VirtualDevice& rVDev, int nNumTilesX, int nNumTilesY, const Size& rTileSizePixel, const GraphicAttr* pAttr ) { // how many tiles to generate per recursion step const int nExponent = 2; // determine MSB factor int nMSBFactor( 1 ); while( nNumTilesX / nMSBFactor != 0 || nNumTilesY / nMSBFactor != 0 ) { nMSBFactor *= nExponent; } // one less if(nMSBFactor > 1) { nMSBFactor /= nExponent; } ImplTileInfo aTileInfo; // #105229# Switch off mapping (converting to logic and back to // pixel might cause roundoff errors) bool bOldMap( rVDev.IsMapModeEnabled() ); rVDev.EnableMapMode( false ); bool bRet( ImplRenderTileRecursive( rVDev, nExponent, nMSBFactor, nNumTilesX, nNumTilesY, nNumTilesX, nNumTilesY, rTileSizePixel, pAttr, aTileInfo ) ); rVDev.EnableMapMode( bOldMap ); return bRet; } // define for debug drawings //#define DBG_TEST // see header comment. this works similar to base conversion of a // number, i.e. if the exponent is 10, then the number for every tile // size is given by the decimal place of the corresponding decimal // representation. bool GraphicObject::ImplRenderTileRecursive( VirtualDevice& rVDev, int nExponent, int nMSBFactor, int nNumOrigTilesX, int nNumOrigTilesY, int nRemainderTilesX, int nRemainderTilesY, const Size& rTileSizePixel, const GraphicAttr* pAttr, ImplTileInfo& rTileInfo ) { // gets loaded with our tile bitmap std::unique_ptr xTmpGraphic; GraphicObject* pTileGraphic; // stores a flag that renders the zero'th tile position // (i.e. (0,0)+rCurrPos) only if we're at the bottom of the // recursion stack. All other position already have that tile // rendered, because the lower levels painted their generated tile // there. bool bNoFirstTileDraw( false ); // what's left when we're done with our tile size const int nNewRemainderX( nRemainderTilesX % nMSBFactor ); const int nNewRemainderY( nRemainderTilesY % nMSBFactor ); // gets filled out from the recursive call with info of what's // been generated ImplTileInfo aTileInfo; // check for recursion's end condition: LSB place reached? if( nMSBFactor == 1 ) { pTileGraphic = this; // set initial tile size -> orig size aTileInfo.aTileSizePixel = rTileSizePixel; aTileInfo.nTilesEmptyX = nNumOrigTilesX; aTileInfo.nTilesEmptyY = nNumOrigTilesY; } else if( ImplRenderTileRecursive( rVDev, nExponent, nMSBFactor/nExponent, nNumOrigTilesX, nNumOrigTilesY, nNewRemainderX, nNewRemainderY, rTileSizePixel, pAttr, aTileInfo ) ) { // extract generated tile -> see comment on the first loop below BitmapEx aTileBitmap( rVDev.GetBitmap( aTileInfo.aTileTopLeft, aTileInfo.aTileSizePixel ) ); xTmpGraphic.reset(new GraphicObject(std::move(aTileBitmap))); pTileGraphic = xTmpGraphic.get(); // fill stripes left over from upstream levels: // x0000 // 0 // 0 // 0 // 0 // where x denotes the place filled by our recursive predecessors // check whether we have to fill stripes here. Although not // obvious, there is one case where we can skip this step: if // the previous recursion level (the one who filled our // aTileInfo) had zero area to fill, then there are no white // stripes left, naturally. This happens if the digit // associated to that level has a zero, and can be checked via // aTileTopLeft==aNextTileTopLeft. if( aTileInfo.aTileTopLeft != aTileInfo.aNextTileTopLeft ) { // now fill one row from aTileInfo.aNextTileTopLeft.X() all // the way to the right // current output position while drawing Point aCurrPos(aTileInfo.aNextTileTopLeft.X(), aTileInfo.aTileTopLeft.Y()); for (int nX=0; nX < aTileInfo.nTilesEmptyX; nX += nMSBFactor) { if (!pTileGraphic->Draw(rVDev, aCurrPos, aTileInfo.aTileSizePixel, pAttr)) return false; aCurrPos.AdjustX(aTileInfo.aTileSizePixel.Width() ); } #ifdef DBG_TEST // rVDev.SetFillCOL_WHITE ); rVDev.SetFillColor(); rVDev.SetLineColor( Color( 255 * nExponent / nMSBFactor, 255 - 255 * nExponent / nMSBFactor, 128 - 255 * nExponent / nMSBFactor ) ); rVDev.DrawEllipse( tools::Rectangle(aTileInfo.aNextTileTopLeft.X(), aTileInfo.aTileTopLeft.Y(), aTileInfo.aNextTileTopLeft.X() - 1 + (aTileInfo.nTilesEmptyX/nMSBFactor)*aTileInfo.aTileSizePixel.Width(), aTileInfo.aTileTopLeft.Y() + aTileInfo.aTileSizePixel.Height() - 1) ); #endif // now fill one column from aTileInfo.aNextTileTopLeft.Y() all // the way to the bottom aCurrPos.setX( aTileInfo.aTileTopLeft.X() ); aCurrPos.setY( aTileInfo.aNextTileTopLeft.Y() ); for (int nY=0; nY < aTileInfo.nTilesEmptyY; nY += nMSBFactor) { if (!pTileGraphic->Draw(rVDev, aCurrPos, aTileInfo.aTileSizePixel, pAttr)) return false; aCurrPos.AdjustY(aTileInfo.aTileSizePixel.Height() ); } #ifdef DBG_TEST rVDev.DrawEllipse( tools::Rectangle(aTileInfo.aTileTopLeft.X(), aTileInfo.aNextTileTopLeft.Y(), aTileInfo.aTileTopLeft.X() + aTileInfo.aTileSizePixel.Width() - 1, aTileInfo.aNextTileTopLeft.Y() - 1 + (aTileInfo.nTilesEmptyY/nMSBFactor)*aTileInfo.aTileSizePixel.Height()) ); #endif } else { // Thought that aTileInfo.aNextTileTopLeft tile has always // been drawn already, but that's wrong: typically, // _parts_ of that tile have been drawn, since the // previous level generated the tile there. But when // aTileInfo.aNextTileTopLeft!=aTileInfo.aTileTopLeft, the // difference between these two values is missing in the // lower right corner of this first tile. So, can do that // only here. bNoFirstTileDraw = true; } } else { return false; } // calc number of original tiles in our drawing area without // remainder nRemainderTilesX -= nNewRemainderX; nRemainderTilesY -= nNewRemainderY; // fill tile info for calling method rTileInfo.aTileTopLeft = aTileInfo.aNextTileTopLeft; rTileInfo.aNextTileTopLeft = Point( rTileInfo.aTileTopLeft.X() + rTileSizePixel.Width()*nRemainderTilesX, rTileInfo.aTileTopLeft.Y() + rTileSizePixel.Height()*nRemainderTilesY ); rTileInfo.aTileSizePixel = Size( rTileSizePixel.Width()*nMSBFactor*nExponent, rTileSizePixel.Height()*nMSBFactor*nExponent ); rTileInfo.nTilesEmptyX = aTileInfo.nTilesEmptyX - nRemainderTilesX; rTileInfo.nTilesEmptyY = aTileInfo.nTilesEmptyY - nRemainderTilesY; // init output position Point aCurrPos = aTileInfo.aNextTileTopLeft; // fill our drawing area. Fill possibly more, to create the next // bigger tile size -> see bitmap extraction above. This does no // harm, since everything right or below our actual area is // overdrawn by our caller. Just in case we're in the last level, // we don't draw beyond the right or bottom border. for (int nY=0; nY < aTileInfo.nTilesEmptyY && nY < nExponent*nMSBFactor; nY += nMSBFactor) { aCurrPos.setX( aTileInfo.aNextTileTopLeft.X() ); for (int nX=0; nX < aTileInfo.nTilesEmptyX && nX < nExponent*nMSBFactor; nX += nMSBFactor) { if( bNoFirstTileDraw ) bNoFirstTileDraw = false; // don't draw first tile position else if (!pTileGraphic->Draw(rVDev, aCurrPos, aTileInfo.aTileSizePixel, pAttr)) return false; aCurrPos.AdjustX(aTileInfo.aTileSizePixel.Width() ); } aCurrPos.AdjustY(aTileInfo.aTileSizePixel.Height() ); } #ifdef DBG_TEST // rVDev.SetFillCOL_WHITE ); rVDev.SetFillColor(); rVDev.SetLineColor( Color( 255 * nExponent / nMSBFactor, 255 - 255 * nExponent / nMSBFactor, 128 - 255 * nExponent / nMSBFactor ) ); rVDev.DrawRect( tools::Rectangle((rTileInfo.aTileTopLeft.X())*rTileSizePixel.Width(), (rTileInfo.aTileTopLeft.Y())*rTileSizePixel.Height(), (rTileInfo.aNextTileTopLeft.X())*rTileSizePixel.Width()-1, (rTileInfo.aNextTileTopLeft.Y())*rTileSizePixel.Height()-1) ); #endif return true; } bool GraphicObject::ImplDrawTiled(OutputDevice& rOut, const tools::Rectangle& rArea, const Size& rSizePixel, const Size& rOffset, const GraphicAttr* pAttr, int nTileCacheSize1D) { const MapMode aOutMapMode(rOut.GetMapMode()); const MapMode aMapMode( aOutMapMode.GetMapUnit(), Point(), aOutMapMode.GetScaleX(), aOutMapMode.GetScaleY() ); bool bRet( false ); // #i42643# Casting to Int64, to avoid integer overflow for // huge-DPI output devices if( GetGraphic().GetType() == GraphicType::Bitmap && static_cast(rSizePixel.Width()) * rSizePixel.Height() < static_cast(nTileCacheSize1D)*nTileCacheSize1D ) { // First combine very small bitmaps into a larger tile ScopedVclPtrInstance< VirtualDevice > aVDev; const int nNumTilesInCacheX( (nTileCacheSize1D + rSizePixel.Width()-1) / rSizePixel.Width() ); const int nNumTilesInCacheY( (nTileCacheSize1D + rSizePixel.Height()-1) / rSizePixel.Height() ); aVDev->SetOutputSizePixel( Size( nNumTilesInCacheX*rSizePixel.Width(), nNumTilesInCacheY*rSizePixel.Height() ) ); aVDev->SetMapMode( aMapMode ); // draw bitmap content if( ImplRenderTempTile( *aVDev, nNumTilesInCacheX, nNumTilesInCacheY, rSizePixel, pAttr ) ) { BitmapEx aTileBitmap( aVDev->GetBitmap( Point(0,0), aVDev->GetOutputSize() ) ); // draw alpha content, if any if( IsTransparent() ) { GraphicObject aAlphaGraphic; if( GetGraphic().IsAlpha() ) aAlphaGraphic.SetGraphic(BitmapEx(GetGraphic().GetBitmapEx().GetAlphaMask().GetBitmap())); else aAlphaGraphic.SetGraphic(BitmapEx(Bitmap())); if( aAlphaGraphic.ImplRenderTempTile( *aVDev, nNumTilesInCacheX, nNumTilesInCacheY, rSizePixel, pAttr ) ) { // Combine bitmap and alpha/mask if( GetGraphic().IsAlpha() ) aTileBitmap = BitmapEx( aTileBitmap.GetBitmap(), AlphaMask( aVDev->GetBitmap( Point(0,0), aVDev->GetOutputSize() ) ) ); else aTileBitmap = BitmapEx( aTileBitmap.GetBitmap(), aVDev->GetBitmap( Point(0,0), aVDev->GetOutputSize() ).CreateAlphaMask( COL_WHITE ) ); } } // paint generated tile GraphicObject aTmpGraphic( aTileBitmap ); bRet = aTmpGraphic.ImplDrawTiled(rOut, rArea, aTileBitmap.GetSizePixel(), rOffset, pAttr, nTileCacheSize1D); } } else { const Size aOutOffset( rOut.LogicToPixel( rOffset, aOutMapMode ) ); const tools::Rectangle aOutArea( rOut.LogicToPixel( rArea, aOutMapMode ) ); // number of invisible (because out-of-area) tiles int nInvisibleTilesX; int nInvisibleTilesY; // round towards -infty for negative offset if( aOutOffset.Width() < 0 ) nInvisibleTilesX = (aOutOffset.Width() - rSizePixel.Width() + 1) / rSizePixel.Width(); else nInvisibleTilesX = aOutOffset.Width() / rSizePixel.Width(); // round towards -infty for negative offset if( aOutOffset.Height() < 0 ) nInvisibleTilesY = (aOutOffset.Height() - rSizePixel.Height() + 1) / rSizePixel.Height(); else nInvisibleTilesY = aOutOffset.Height() / rSizePixel.Height(); // origin from where to 'virtually' start drawing in pixel const Point aOutOrigin( rOut.LogicToPixel( Point( rArea.Left() - rOffset.Width(), rArea.Top() - rOffset.Height() ) ) ); // position in pixel from where to really start output const Point aOutStart( aOutOrigin.X() + nInvisibleTilesX*rSizePixel.Width(), aOutOrigin.Y() + nInvisibleTilesY*rSizePixel.Height() ); rOut.Push( vcl::PushFlags::CLIPREGION ); rOut.IntersectClipRegion( rArea ); // Paint all tiles auto nOutAreaWidth = aOutArea.GetWidth(); auto nOutAreaHeight = aOutArea.GetHeight(); assert(nOutAreaWidth >= 0 && nOutAreaHeight >= 0 && "coverity 2023.12.2"); bRet = ImplDrawTiled(rOut, aOutStart, (nOutAreaWidth + aOutArea.Left() - aOutStart.X() + rSizePixel.Width() - 1) / rSizePixel.Width(), (nOutAreaHeight + aOutArea.Top() - aOutStart.Y() + rSizePixel.Height() - 1) / rSizePixel.Height(), rSizePixel, pAttr); rOut.Pop(); } return bRet; } bool GraphicObject::ImplDrawTiled( OutputDevice& rOut, const Point& rPosPixel, int nNumTilesX, int nNumTilesY, const Size& rTileSizePixel, const GraphicAttr* pAttr ) const { Point aCurrPos( rPosPixel ); Size aTileSizeLogic( rOut.PixelToLogic( rTileSizePixel ) ); int nX, nY; // #107607# Use logical coordinates for metafile playing, too bool bDrawInPixel( rOut.GetConnectMetaFile() == nullptr && GraphicType::Bitmap == GetType() ); bool bRet = false; // #105229# Switch off mapping (converting to logic and back to // pixel might cause roundoff errors) bool bOldMap( rOut.IsMapModeEnabled() ); if( bDrawInPixel ) rOut.EnableMapMode( false ); for( nY=0; nY < nNumTilesY; ++nY ) { aCurrPos.setX( rPosPixel.X() ); for( nX=0; nX < nNumTilesX; ++nX ) { // #105229# work with pixel coordinates here, mapping is disabled! // #104004# don't disable mapping for metafile recordings // #108412# don't quit the loop if one draw fails // update return value. This method should return true, if // at least one of the looped Draws succeeded. bRet |= Draw(rOut, bDrawInPixel ? aCurrPos : rOut.PixelToLogic(aCurrPos), bDrawInPixel ? rTileSizePixel : aTileSizeLogic, pAttr); aCurrPos.AdjustX(rTileSizePixel.Width() ); } aCurrPos.AdjustY(rTileSizePixel.Height() ); } if( bDrawInPixel ) rOut.EnableMapMode( bOldMap ); return bRet; } void GraphicObject::ImplTransformBitmap( BitmapEx& rBmpEx, const GraphicAttr& rAttr, const Size& rCropLeftTop, const Size& rCropRightBottom, const tools::Rectangle& rCropRect, const Size& rDstSize, bool bEnlarge ) const { // #107947# Extracted from svdograf.cxx // #104115# Crop the bitmap if( rAttr.IsCropped() ) { rBmpEx.Crop( rCropRect ); // #104115# Negative crop sizes mean: enlarge bitmap and pad if( bEnlarge && ( rCropLeftTop.Width() < 0 || rCropLeftTop.Height() < 0 || rCropRightBottom.Width() < 0 || rCropRightBottom.Height() < 0 ) ) { Size aBmpSize( rBmpEx.GetSizePixel() ); sal_Int32 nPadLeft( rCropLeftTop.Width() < 0 ? -rCropLeftTop.Width() : 0 ); sal_Int32 nPadTop( rCropLeftTop.Height() < 0 ? -rCropLeftTop.Height() : 0 ); sal_Int32 nPadTotalWidth( aBmpSize.Width() + nPadLeft + (rCropRightBottom.Width() < 0 ? -rCropRightBottom.Width() : 0) ); sal_Int32 nPadTotalHeight( aBmpSize.Height() + nPadTop + (rCropRightBottom.Height() < 0 ? -rCropRightBottom.Height() : 0) ); BitmapEx aBmpEx2; if( rBmpEx.IsAlpha() ) { aBmpEx2 = rBmpEx; } else { // #104115# Generate mask bitmap and init to zero AlphaMask aMask(aBmpSize); aMask.Erase(255); // #104115# Always generate transparent bitmap, we need the border transparent aBmpEx2 = BitmapEx( rBmpEx.GetBitmap(), aMask ); // #104115# Add opaque mask to source bitmap, otherwise the destination remains transparent rBmpEx = aBmpEx2; } aBmpEx2.Scale(Size(nPadTotalWidth, nPadTotalHeight)); aBmpEx2.Erase( Color(ColorAlpha,0,0,0,0) ); aBmpEx2.CopyPixel( tools::Rectangle( Point(nPadLeft, nPadTop), aBmpSize ), tools::Rectangle( Point(0, 0), aBmpSize ), rBmpEx ); rBmpEx = aBmpEx2; } } const Size aSizePixel( rBmpEx.GetSizePixel() ); if( rAttr.GetRotation() == 0_deg10 || IsAnimated() ) return; if( !(aSizePixel.Width() && aSizePixel.Height() && rDstSize.Width() && rDstSize.Height()) ) return; double fSrcWH = static_cast(aSizePixel.Width()) / aSizePixel.Height(); double fDstWH = static_cast(rDstSize.Width()) / rDstSize.Height(); double fScaleX = 1.0, fScaleY = 1.0; // always choose scaling to shrink bitmap if( fSrcWH < fDstWH ) fScaleY = aSizePixel.Width() / ( fDstWH * aSizePixel.Height() ); else fScaleX = fDstWH * aSizePixel.Height() / aSizePixel.Width(); rBmpEx.Scale( fScaleX, fScaleY ); } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */