File indexing completed on 2024-04-28 04:33:06

0001 /*
0002     SPDX-FileCopyrightText: 2005 Enrico Ros <eros.kde@email.it>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "pagepainter.h"
0008 
0009 // qt / kde includes
0010 #include <QApplication>
0011 #include <QDebug>
0012 #include <QIcon>
0013 #include <QPainter>
0014 #include <QPalette>
0015 #include <QPixmap>
0016 #include <QRect>
0017 #include <QTransform>
0018 #include <QVarLengthArray>
0019 
0020 // system includes
0021 #include <math.h>
0022 
0023 // local includes
0024 #include "core/annotations.h"
0025 #include "core/observer.h"
0026 #include "core/page.h"
0027 #include "core/page_p.h"
0028 #include "core/tile.h"
0029 #include "core/utils.h"
0030 #include "debug_ui.h"
0031 #include "guiutils.h"
0032 #include "settings.h"
0033 #include "settings_core.h"
0034 
0035 Q_GLOBAL_STATIC_WITH_ARGS(QPixmap, busyPixmap, (QIcon::fromTheme(QLatin1String("okular")).pixmap(48)))
0036 
0037 #define TEXTANNOTATION_ICONSIZE 24
0038 
0039 inline QPen buildPen(const Okular::Annotation *ann, double width, const QColor &color)
0040 {
0041     QColor c = color;
0042     c.setAlphaF(ann->style().opacity());
0043     QPen p(QBrush(c), width, ann->style().lineStyle() == Okular::Annotation::Dashed ? Qt::DashLine : Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
0044     return p;
0045 }
0046 
0047 void PagePainter::paintPageOnPainter(QPainter *destPainter, const Okular::Page *page, Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect limits)
0048 {
0049     paintCroppedPageOnPainter(destPainter, page, observer, flags, scaledWidth, scaledHeight, limits, Okular::NormalizedRect(0, 0, 1, 1), nullptr);
0050 }
0051 
0052 void PagePainter::paintCroppedPageOnPainter(QPainter *destPainter,
0053                                             const Okular::Page *page,
0054                                             Okular::DocumentObserver *observer,
0055                                             int flags,
0056                                             int scaledWidth,
0057                                             int scaledHeight,
0058                                             const QRect limits,
0059                                             const Okular::NormalizedRect &crop,
0060                                             Okular::NormalizedPoint *viewPortPoint)
0061 {
0062     qreal dpr = destPainter->device()->devicePixelRatioF();
0063 
0064     /* Calculate the cropped geometry of the page */
0065     QRect scaledCrop = crop.geometry(scaledWidth, scaledHeight);
0066 
0067     /* variables prefixed with d are in the device pixels coordinate system, which translates to the rendered output - that means,
0068      * multiplied with the device pixel ratio of the target PaintDevice */
0069     const QRect dScaledCrop(QRectF(scaledCrop.x() * dpr, scaledCrop.y() * dpr, scaledCrop.width() * dpr, scaledCrop.height() * dpr).toAlignedRect());
0070 
0071     int croppedWidth = scaledCrop.width();
0072     int croppedHeight = scaledCrop.height();
0073 
0074     int dScaledWidth = ceil(scaledWidth * dpr);
0075     int dScaledHeight = ceil(scaledHeight * dpr);
0076     const QRect dLimits(QRectF(limits.x() * dpr, limits.y() * dpr, limits.width() * dpr, limits.height() * dpr).toAlignedRect());
0077 
0078     QColor paperColor = Qt::white;
0079     QColor backgroundColor = paperColor;
0080     if (Okular::SettingsCore::changeColors()) {
0081         switch (Okular::SettingsCore::renderMode()) {
0082         case Okular::SettingsCore::EnumRenderMode::Inverted:
0083         case Okular::SettingsCore::EnumRenderMode::InvertLightness:
0084         case Okular::SettingsCore::EnumRenderMode::InvertLuma:
0085         case Okular::SettingsCore::EnumRenderMode::InvertLumaSymmetric:
0086             backgroundColor = Qt::black;
0087             break;
0088         case Okular::SettingsCore::EnumRenderMode::Paper:
0089             paperColor = Okular::SettingsCore::paperColor();
0090             backgroundColor = paperColor;
0091             break;
0092         case Okular::SettingsCore::EnumRenderMode::Recolor:
0093             backgroundColor = Okular::Settings::recolorBackground();
0094             break;
0095         default:;
0096         }
0097     }
0098     destPainter->fillRect(limits, backgroundColor);
0099 
0100     const bool hasTilesManager = page->hasTilesManager(observer);
0101     QPixmap pixmap;
0102 
0103     if (!hasTilesManager) {
0104         /** 1 - RETRIEVE THE 'PAGE+ID' PIXMAP OR A SIMILAR 'PAGE' ONE **/
0105         const QPixmap *p = page->_o_nearestPixmap(observer, dScaledWidth, dScaledHeight);
0106 
0107         if (p != nullptr) {
0108             pixmap = *p;
0109         }
0110 
0111         /** 1B - IF NO PIXMAP, DRAW EMPTY PAGE **/
0112         double pixmapRescaleRatio = !pixmap.isNull() ? dScaledWidth / (double)pixmap.width() : -1;
0113         long pixmapPixels = !pixmap.isNull() ? (long)pixmap.width() * (long)pixmap.height() : 0;
0114         if (pixmap.isNull() || pixmapRescaleRatio > 20.0 || pixmapRescaleRatio < 0.25 || (dScaledWidth > pixmap.width() && pixmapPixels > 60000000L)) {
0115             // draw something on the blank page: the okular icon or a cross (as a fallback)
0116             if (!busyPixmap()->isNull()) {
0117                 busyPixmap->setDevicePixelRatio(dpr);
0118                 destPainter->drawPixmap(QPoint(10, 10), *busyPixmap());
0119             } else {
0120                 destPainter->setPen(Qt::gray);
0121                 destPainter->drawLine(0, 0, croppedWidth - 1, croppedHeight - 1);
0122                 destPainter->drawLine(0, croppedHeight - 1, croppedWidth - 1, 0);
0123             }
0124             return;
0125         }
0126     }
0127 
0128     /** 2 - FIND OUT WHAT TO PAINT (Flags + Configuration + Presence) **/
0129     bool canDrawHighlights = (flags & Highlights) && !page->m_highlights.isEmpty();
0130     bool canDrawTextSelection = (flags & TextSelection) && page->textSelection();
0131     bool canDrawAnnotations = (flags & Annotations) && !page->m_annotations.isEmpty();
0132     bool enhanceLinks = (flags & EnhanceLinks) && Okular::Settings::highlightLinks();
0133     bool enhanceImages = (flags & EnhanceImages) && Okular::Settings::highlightImages();
0134 
0135     // vectors containing objects to draw
0136     // make this a qcolor, rect map, since we don't need
0137     // to know s_id here! we are only drawing this right?
0138     QList<QPair<QColor, Okular::NormalizedRect>> *bufferedHighlights = nullptr;
0139     QList<Okular::Annotation *> *bufferedAnnotations = nullptr;
0140     QList<Okular::Annotation *> *unbufferedAnnotations = nullptr;
0141     Okular::Annotation *boundingRectOnlyAnn = nullptr; // Paint the bounding rect of this annotation
0142     // fill up lists with visible annotation/highlight objects/text selections
0143     if (canDrawHighlights || canDrawTextSelection || canDrawAnnotations) {
0144         // precalc normalized 'limits rect' for intersection
0145         double nXMin = ((double)limits.left() / scaledWidth) + crop.left, nXMax = ((double)limits.right() / scaledWidth) + crop.left, nYMin = ((double)limits.top() / scaledHeight) + crop.top,
0146                nYMax = ((double)limits.bottom() / scaledHeight) + crop.top;
0147         // append all highlights inside limits to their list
0148         if (canDrawHighlights) {
0149             if (!bufferedHighlights) {
0150                 bufferedHighlights = new QList<QPair<QColor, Okular::NormalizedRect>>();
0151             }
0152             /*            else
0153                         {*/
0154 
0155             Okular::NormalizedRect *limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax);
0156             Okular::HighlightAreaRect::const_iterator hIt;
0157             for (const Okular::HighlightAreaRect *highlight : page->m_highlights) {
0158                 for (hIt = highlight->constBegin(); hIt != highlight->constEnd(); ++hIt) {
0159                     if ((*hIt).intersects(limitRect)) {
0160                         bufferedHighlights->append(qMakePair(highlight->color, *hIt));
0161                     }
0162                 }
0163             }
0164             delete limitRect;
0165             //}
0166         }
0167         if (canDrawTextSelection) {
0168             if (!bufferedHighlights) {
0169                 bufferedHighlights = new QList<QPair<QColor, Okular::NormalizedRect>>();
0170             }
0171             /*            else
0172                         {*/
0173             Okular::NormalizedRect *limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax);
0174             const Okular::RegularAreaRect *textSelection = page->textSelection();
0175             Okular::HighlightAreaRect::const_iterator hIt = textSelection->constBegin(), hEnd = textSelection->constEnd();
0176             for (; hIt != hEnd; ++hIt) {
0177                 if ((*hIt).intersects(limitRect)) {
0178                     bufferedHighlights->append(qMakePair(page->textSelectionColor(), *hIt));
0179                 }
0180             }
0181             delete limitRect;
0182             //}
0183         }
0184         // append annotations inside limits to the un/buffered list
0185         if (canDrawAnnotations) {
0186             for (Okular::Annotation *ann : page->m_annotations) {
0187                 int flags = ann->flags();
0188 
0189                 if (flags & Okular::Annotation::Hidden) {
0190                     continue;
0191                 }
0192 
0193                 if (flags & Okular::Annotation::ExternallyDrawn) {
0194                     // ExternallyDrawn annots are never rendered by PagePainter.
0195                     // Just paint the boundingRect if the annot is moved or resized.
0196                     if (flags & (Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized)) {
0197                         boundingRectOnlyAnn = ann;
0198                     }
0199                     continue;
0200                 }
0201 
0202                 bool intersects = ann->transformedBoundingRectangle().intersects(nXMin, nYMin, nXMax, nYMax);
0203                 if (ann->subType() == Okular::Annotation::AText) {
0204                     Okular::TextAnnotation *ta = static_cast<Okular::TextAnnotation *>(ann);
0205                     if (ta->textType() == Okular::TextAnnotation::Linked) {
0206                         Okular::NormalizedRect iconrect(ann->transformedBoundingRectangle().left,
0207                                                         ann->transformedBoundingRectangle().top,
0208                                                         ann->transformedBoundingRectangle().left + TEXTANNOTATION_ICONSIZE / page->width(),
0209                                                         ann->transformedBoundingRectangle().top + TEXTANNOTATION_ICONSIZE / page->height());
0210                         intersects = iconrect.intersects(nXMin, nYMin, nXMax, nYMax);
0211                     }
0212                 }
0213                 if (intersects) {
0214                     Okular::Annotation::SubType type = ann->subType();
0215                     if (type == Okular::Annotation::ALine || type == Okular::Annotation::AHighlight || type == Okular::Annotation::AInk /*|| (type == Annotation::AGeom && ann->style().opacity() < 0.99)*/) {
0216                         if (!bufferedAnnotations) {
0217                             bufferedAnnotations = new QList<Okular::Annotation *>();
0218                         }
0219                         bufferedAnnotations->append(ann);
0220                     } else {
0221                         if (!unbufferedAnnotations) {
0222                             unbufferedAnnotations = new QList<Okular::Annotation *>();
0223                         }
0224                         unbufferedAnnotations->append(ann);
0225                     }
0226                 }
0227             }
0228         }
0229         // end of intersections checking
0230     }
0231 
0232     /** 3 - ENABLE BACKBUFFERING IF DIRECT IMAGE MANIPULATION IS NEEDED **/
0233     bool bufferAccessibility = (flags & Accessibility) && Okular::SettingsCore::changeColors() && (Okular::SettingsCore::renderMode() != Okular::SettingsCore::EnumRenderMode::Paper);
0234     bool useBackBuffer = bufferAccessibility || bufferedHighlights || bufferedAnnotations || viewPortPoint;
0235     QPixmap *backPixmap = nullptr;
0236     QPainter *mixedPainter = nullptr;
0237     QRect limitsInPixmap = limits.translated(scaledCrop.topLeft());
0238     QRect dLimitsInPixmap = dLimits.translated(dScaledCrop.topLeft());
0239 
0240     // limits within full (scaled but uncropped) pixmap
0241 
0242     /** 4A -- REGULAR FLOW. PAINT PIXMAP NORMAL OR RESCALED USING GIVEN QPAINTER **/
0243     if (!useBackBuffer) {
0244         if (hasTilesManager) {
0245             const Okular::NormalizedRect normalizedLimits(limitsInPixmap, scaledWidth, scaledHeight);
0246             const QList<Okular::Tile> tiles = page->tilesAt(observer, normalizedLimits);
0247             QList<Okular::Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
0248             while (tIt != tEnd) {
0249                 const Okular::Tile &tile = *tIt;
0250                 QRectF tileRect = tile.rect().geometryF(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft());
0251                 QRect dTileRect = tile.rect().geometry(dScaledWidth, dScaledHeight).translated(-dScaledCrop.topLeft());
0252                 QRectF limitsInTile = QRectF(limits) & tileRect;
0253                 QRect dLimitsInTile = dLimits & dTileRect;
0254 
0255                 if (!limitsInTile.isEmpty()) {
0256                     QPixmap *tilePixmap = tile.pixmap();
0257 
0258                     if (tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height()) {
0259                         destPainter->drawPixmap(limitsInTile, *tilePixmap, dLimitsInTile.translated(-dTileRect.topLeft()));
0260                     } else {
0261                         destPainter->drawPixmap(tileRect, *tilePixmap, tilePixmap->rect());
0262                     }
0263                 }
0264                 tIt++;
0265             }
0266         } else {
0267             destPainter->drawPixmap(limits, pixmap.scaled(dScaledWidth, dScaledHeight), dLimitsInPixmap);
0268         }
0269 
0270         // 4A.2. active painter is the one passed to this method
0271         mixedPainter = destPainter;
0272     }
0273     /** 4B -- BUFFERED FLOW. IMAGE PAINTING + OPERATIONS. QPAINTER OVER PIXMAP  **/
0274     else {
0275         // the image over which we are going to draw
0276         QImage backImage = QImage(dLimits.width(), dLimits.height(), QImage::Format_ARGB32_Premultiplied);
0277         backImage.setDevicePixelRatio(dpr);
0278         backImage.fill(paperColor);
0279         QPainter p(&backImage);
0280 
0281         if (hasTilesManager) {
0282             const Okular::NormalizedRect normalizedLimits(limitsInPixmap, scaledWidth, scaledHeight);
0283             const QList<Okular::Tile> tiles = page->tilesAt(observer, normalizedLimits);
0284             QList<Okular::Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
0285             while (tIt != tEnd) {
0286                 const Okular::Tile &tile = *tIt;
0287                 QRectF tileRect = tile.rect().geometryF(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft());
0288                 QRect dTileRect = tile.rect().geometry(dScaledWidth, dScaledHeight).translated(-dScaledCrop.topLeft());
0289                 QRectF limitsInTile = QRectF(limits) & tileRect;
0290                 QRect dLimitsInTile = dLimits & dTileRect;
0291 
0292                 if (!limitsInTile.isEmpty()) {
0293                     QPixmap *tilePixmap = tile.pixmap();
0294 
0295                     if (tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height()) {
0296                         p.drawPixmap(limitsInTile.translated(-limits.topLeft()), *tilePixmap, dLimitsInTile.translated(-dTileRect.topLeft()));
0297                     } else {
0298                         double xScale = tilePixmap->width() / (double)dTileRect.width();
0299                         double yScale = tilePixmap->height() / (double)dTileRect.height();
0300                         QTransform transform(xScale, 0, 0, yScale, 0, 0);
0301                         p.drawPixmap(limitsInTile.translated(-limits.topLeft()), *tilePixmap, transform.mapRect(dLimitsInTile).translated(-transform.mapRect(dTileRect).topLeft()));
0302                     }
0303                 }
0304                 ++tIt;
0305             }
0306         } else {
0307             // 4B.1. draw the page pixmap: normal or scaled
0308 
0309             p.drawPixmap(QRectF(0, 0, limits.width(), limits.height()), pixmap.scaled(dScaledWidth, dScaledHeight), dLimitsInPixmap);
0310         }
0311 
0312         p.end();
0313 
0314         // 4B.2. modify pixmap following accessibility settings
0315         if (bufferAccessibility) {
0316             switch (Okular::SettingsCore::renderMode()) {
0317             case Okular::SettingsCore::EnumRenderMode::Inverted:
0318                 // Invert image pixels using QImage internal function
0319                 backImage.invertPixels(QImage::InvertRgb);
0320                 break;
0321             case Okular::SettingsCore::EnumRenderMode::Recolor:
0322                 recolor(&backImage, Okular::Settings::recolorForeground(), Okular::Settings::recolorBackground());
0323                 break;
0324             case Okular::SettingsCore::EnumRenderMode::BlackWhite:
0325                 blackWhite(&backImage, Okular::Settings::bWContrast(), Okular::Settings::bWThreshold());
0326                 break;
0327             case Okular::SettingsCore::EnumRenderMode::InvertLightness:
0328                 invertLightness(&backImage);
0329                 break;
0330             case Okular::SettingsCore::EnumRenderMode::InvertLuma:
0331                 invertLuma(&backImage, 0.2126, 0.7152, 0.0722); // sRGB / Rec. 709 luma coefficients
0332                 break;
0333             case Okular::SettingsCore::EnumRenderMode::InvertLumaSymmetric:
0334                 invertLuma(&backImage, 0.3333, 0.3334, 0.3333); // Symmetric coefficients, to keep colors saturated.
0335                 break;
0336             case Okular::SettingsCore::EnumRenderMode::HueShiftPositive:
0337                 hueShiftPositive(&backImage);
0338                 break;
0339             case Okular::SettingsCore::EnumRenderMode::HueShiftNegative:
0340                 hueShiftNegative(&backImage);
0341                 break;
0342             }
0343         }
0344 
0345         // 4B.3. highlight rects in page
0346         if (bufferedHighlights) {
0347             // draw highlights that are inside the 'limits' paint region
0348             for (const auto &highlight : std::as_const(*bufferedHighlights)) {
0349                 const Okular::NormalizedRect &r = highlight.second;
0350                 // find out the rect to highlight on pixmap
0351                 QRect highlightRect = r.geometry(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft()).intersected(limits);
0352                 highlightRect.translate(-limits.left(), -limits.top());
0353 
0354                 const QColor highlightColor = highlight.first;
0355                 QPainter painter(&backImage);
0356                 painter.setCompositionMode(QPainter::CompositionMode_Multiply);
0357                 painter.fillRect(highlightRect, highlightColor);
0358 
0359                 auto frameColor = highlightColor.darker(150);
0360                 const QRect frameRect = r.geometry(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft()).translated(-limits.left(), -limits.top());
0361                 painter.setPen(frameColor);
0362                 painter.drawRect(frameRect);
0363             }
0364         }
0365 
0366         // 4B.4. paint annotations [COMPOSITED ONES]
0367         if (bufferedAnnotations) {
0368             // Albert: This is quite "heavy" but all the backImage that reach here are QImage::Format_ARGB32_Premultiplied
0369             // and have to be so that the QPainter::CompositionMode_Multiply works
0370             // we could also put a
0371             // backImage = backImage.convertToFormat(QImage::Format_ARGB32_Premultiplied)
0372             // that would be almost a noop, but we'll leave the assert for now
0373             Q_ASSERT(backImage.format() == QImage::Format_ARGB32_Premultiplied);
0374             // precalc constants for normalizing [0,1] page coordinates into normalized [0,1] limit rect coordinates
0375             double pageScale = (double)croppedWidth / page->width();
0376             double xOffset = (double)limits.left() / (double)scaledWidth + crop.left, xScale = (double)scaledWidth / (double)limits.width(), yOffset = (double)limits.top() / (double)scaledHeight + crop.top,
0377                    yScale = (double)scaledHeight / (double)limits.height();
0378 
0379             // paint all buffered annotations in the page
0380             QList<Okular::Annotation *>::const_iterator aIt = bufferedAnnotations->constBegin(), aEnd = bufferedAnnotations->constEnd();
0381             for (; aIt != aEnd; ++aIt) {
0382                 Okular::Annotation *a = *aIt;
0383                 Okular::Annotation::SubType type = a->subType();
0384                 QColor acolor = a->style().color();
0385                 if (!acolor.isValid()) {
0386                     acolor = Qt::yellow;
0387                 }
0388                 acolor.setAlphaF(a->style().opacity());
0389 
0390                 // draw LineAnnotation MISSING: caption, dash pattern, endings for multipoint lines
0391                 if (type == Okular::Annotation::ALine) {
0392                     LineAnnotPainter linepainter {(Okular::LineAnnotation *)a, {page->width(), page->height()}, pageScale, {xScale, 0., 0., yScale, -xOffset * xScale, -yOffset * yScale}};
0393                     linepainter.draw(backImage);
0394                 }
0395                 // draw HighlightAnnotation MISSING: under/strike width, feather, capping
0396                 else if (type == Okular::Annotation::AHighlight) {
0397                     // get the annotation
0398                     Okular::HighlightAnnotation *ha = (Okular::HighlightAnnotation *)a;
0399                     Okular::HighlightAnnotation::HighlightType type = ha->highlightType();
0400 
0401                     // draw each quad of the annotation
0402                     int quads = ha->highlightQuads().size();
0403                     for (int q = 0; q < quads; q++) {
0404                         NormalizedPath path;
0405                         const Okular::HighlightAnnotation::Quad &quad = ha->highlightQuads()[q];
0406                         // normalize page point to image
0407                         for (int i = 0; i < 4; i++) {
0408                             Okular::NormalizedPoint point;
0409                             point.x = (quad.transformedPoint(i).x - xOffset) * xScale;
0410                             point.y = (quad.transformedPoint(i).y - yOffset) * yScale;
0411                             path.append(point);
0412                         }
0413                         // draw the normalized path into image
0414                         switch (type) {
0415                         // highlight the whole rect
0416                         case Okular::HighlightAnnotation::Highlight:
0417                             drawShapeOnImage(backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply);
0418                             break;
0419                         // highlight the bottom part of the rect
0420                         case Okular::HighlightAnnotation::Squiggly:
0421                             path[3].x = (path[0].x + path[3].x) / 2.0;
0422                             path[3].y = (path[0].y + path[3].y) / 2.0;
0423                             path[2].x = (path[1].x + path[2].x) / 2.0;
0424                             path[2].y = (path[1].y + path[2].y) / 2.0;
0425                             drawShapeOnImage(backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply);
0426                             break;
0427                         // make a line at 3/4 of the height
0428                         case Okular::HighlightAnnotation::Underline:
0429                             path[0].x = (3 * path[0].x + path[3].x) / 4.0;
0430                             path[0].y = (3 * path[0].y + path[3].y) / 4.0;
0431                             path[1].x = (3 * path[1].x + path[2].x) / 4.0;
0432                             path[1].y = (3 * path[1].y + path[2].y) / 4.0;
0433                             path.pop_back();
0434                             path.pop_back();
0435                             drawShapeOnImage(backImage, path, false, QPen(acolor, 2), QBrush(), pageScale);
0436                             break;
0437                         // make a line at 1/2 of the height
0438                         case Okular::HighlightAnnotation::StrikeOut:
0439                             path[0].x = (path[0].x + path[3].x) / 2.0;
0440                             path[0].y = (path[0].y + path[3].y) / 2.0;
0441                             path[1].x = (path[1].x + path[2].x) / 2.0;
0442                             path[1].y = (path[1].y + path[2].y) / 2.0;
0443                             path.pop_back();
0444                             path.pop_back();
0445                             drawShapeOnImage(backImage, path, false, QPen(acolor, 2), QBrush(), pageScale);
0446                             break;
0447                         }
0448                     }
0449                 }
0450                 // draw InkAnnotation MISSING:invar width, PENTRACER
0451                 else if (type == Okular::Annotation::AInk) {
0452                     // get the annotation
0453                     Okular::InkAnnotation *ia = (Okular::InkAnnotation *)a;
0454 
0455                     // draw each ink path
0456                     const QList<QList<Okular::NormalizedPoint>> transformedInkPaths = ia->transformedInkPaths();
0457 
0458                     const QPen inkPen = buildPen(a, a->style().width(), acolor);
0459 
0460                     for (const QList<Okular::NormalizedPoint> &inkPath : transformedInkPaths) {
0461                         // normalize page point to image
0462                         NormalizedPath path;
0463                         for (const Okular::NormalizedPoint &inkPoint : inkPath) {
0464                             Okular::NormalizedPoint point;
0465                             point.x = (inkPoint.x - xOffset) * xScale;
0466                             point.y = (inkPoint.y - yOffset) * yScale;
0467                             path.append(point);
0468                         }
0469                         // draw the normalized path into image
0470                         drawShapeOnImage(backImage, path, false, inkPen, QBrush(), pageScale);
0471                     }
0472                 }
0473             } // end current annotation drawing
0474         }
0475         if (viewPortPoint) {
0476             QPainter painter(&backImage);
0477             painter.translate(-limits.left(), -limits.top());
0478             painter.setPen(QApplication::palette().color(QPalette::Active, QPalette::Highlight));
0479             painter.drawLine(0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1, viewPortPoint->y * scaledHeight + 1);
0480             // ROTATION CURRENTLY NOT IMPLEMENTED
0481             /*
0482                         if( page->rotation() == Okular::Rotation0)
0483                         {
0484 
0485                         }
0486                         else if(page->rotation() == Okular::Rotation270)
0487                         {
0488                             painter.drawLine( viewPortPoint->y * scaledHeight + 1, 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1);
0489                         }
0490                         else if(page->rotation() == Okular::Rotation180)
0491                         {
0492                             painter.drawLine( 0, (1.0 - viewPortPoint->y) * scaledHeight - 1, scaledWidth - 1, (1.0 - viewPortPoint->y) * scaledHeight - 1 );
0493                         }
0494                         else if(page->rotation() == Okular::Rotation90) // not right, rotation clock-wise
0495                         {
0496                             painter.drawLine( scaledWidth - (viewPortPoint->y * scaledHeight + 1), 0, scaledWidth - (viewPortPoint->y * scaledHeight + 1), scaledWidth - 1);
0497                         }
0498             */
0499         }
0500 
0501         // 4B.5. create the back pixmap converting from the local image
0502         backPixmap = new QPixmap(QPixmap::fromImage(backImage));
0503         backPixmap->setDevicePixelRatio(dpr);
0504 
0505         // 4B.6. create a painter over the pixmap and set it as the active one
0506         mixedPainter = new QPainter(backPixmap);
0507         mixedPainter->translate(-limits.left(), -limits.top());
0508     }
0509 
0510     /** 5 -- MIXED FLOW. Draw ANNOTATIONS [OPAQUE ONES] on ACTIVE PAINTER  **/
0511     if (unbufferedAnnotations) {
0512         // iterate over annotations and paint AText, AGeom, AStamp
0513         QList<Okular::Annotation *>::const_iterator aIt = unbufferedAnnotations->constBegin(), aEnd = unbufferedAnnotations->constEnd();
0514         for (; aIt != aEnd; ++aIt) {
0515             Okular::Annotation *a = *aIt;
0516 
0517             // honor opacity settings on supported types
0518             unsigned int opacity = (unsigned int)(a->style().color().alpha() * a->style().opacity());
0519             // skip the annotation drawing if all the annotation is fully
0520             // transparent, but not with text annotations
0521             if (opacity <= 0 && a->subType() != Okular::Annotation::AText) {
0522                 continue;
0523             }
0524 
0525             QColor acolor = a->style().color();
0526             if (!acolor.isValid()) {
0527                 acolor = Qt::yellow;
0528             }
0529             acolor.setAlpha(opacity);
0530 
0531             // Annotation boundary in destPainter coordinates:
0532             QRect annotBoundary = a->transformedBoundingRectangle().geometry(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft());
0533             QRect annotRect = annotBoundary.intersected(limits);
0534             // Visible portion of the annotation at annotBoundary size:
0535             QRect innerRect = annotRect.translated(-annotBoundary.topLeft());
0536             QRectF dInnerRect(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr);
0537 
0538             Okular::Annotation::SubType type = a->subType();
0539 
0540             // draw TextAnnotation
0541             if (type == Okular::Annotation::AText) {
0542                 Okular::TextAnnotation *text = (Okular::TextAnnotation *)a;
0543                 if (text->textType() == Okular::TextAnnotation::InPlace) {
0544                     QImage image(annotBoundary.size(), QImage::Format_ARGB32);
0545                     image.fill(acolor.rgba());
0546                     QPainter painter(&image);
0547                     painter.setFont(text->textFont());
0548                     painter.setPen(text->textColor());
0549                     Qt::AlignmentFlag halign = (text->inplaceAlignment() == 1 ? Qt::AlignHCenter : (text->inplaceAlignment() == 2 ? Qt::AlignRight : Qt::AlignLeft));
0550                     const double invXScale = (double)page->width() / scaledWidth;
0551                     const double invYScale = (double)page->height() / scaledHeight;
0552                     const double borderWidth = text->style().width();
0553                     painter.scale(1 / invXScale, 1 / invYScale);
0554                     painter.drawText(
0555                         borderWidth * invXScale, borderWidth * invYScale, (image.width() - 2 * borderWidth) * invXScale, (image.height() - 2 * borderWidth) * invYScale, Qt::AlignTop | halign | Qt::TextWordWrap, text->contents());
0556                     painter.resetTransform();
0557                     // Required as asking for a zero width pen results
0558                     // in a default width pen (1.0) being created
0559                     if (borderWidth != 0) {
0560                         QPen pen(Qt::black, borderWidth);
0561                         painter.setPen(pen);
0562                         painter.drawRect(0, 0, image.width() - 1, image.height() - 1);
0563                     }
0564                     painter.end();
0565 
0566                     mixedPainter->drawImage(annotBoundary.topLeft(), image);
0567                 } else if (text->textType() == Okular::TextAnnotation::Linked) {
0568                     // get pixmap, colorize and alpha-blend it
0569                     QPixmap pixmap = QIcon::fromTheme(text->textIcon().toLower()).pixmap(32);
0570 
0571                     QPixmap scaledCroppedPixmap = pixmap.scaled(TEXTANNOTATION_ICONSIZE * dpr, TEXTANNOTATION_ICONSIZE * dpr).copy(dInnerRect.toAlignedRect());
0572                     scaledCroppedPixmap.setDevicePixelRatio(dpr);
0573                     QImage scaledCroppedImage = scaledCroppedPixmap.toImage();
0574 
0575                     // if the annotation color is valid (ie it was set), then
0576                     // use it to colorize the icon, otherwise the icon will be
0577                     // "gray"
0578                     if (a->style().color().isValid()) {
0579                         GuiUtils::colorizeImage(scaledCroppedImage, a->style().color(), opacity);
0580                     }
0581                     pixmap = QPixmap::fromImage(scaledCroppedImage);
0582 
0583                     // draw the mangled image to painter
0584                     mixedPainter->drawPixmap(annotRect.topLeft(), pixmap);
0585                 }
0586 
0587             }
0588             // draw StampAnnotation
0589             else if (type == Okular::Annotation::AStamp) {
0590                 Okular::StampAnnotation *stamp = (Okular::StampAnnotation *)a;
0591 
0592                 // get pixmap and alpha blend it if needed
0593                 QPixmap pixmap = Okular::AnnotationUtils::loadStamp(stamp->stampIconName(), qMax(annotBoundary.width(), annotBoundary.height()) * dpr);
0594                 if (!pixmap.isNull()) // should never happen but can happen on huge sizes
0595                 {
0596                     // Draw pixmap with opacity:
0597                     mixedPainter->save();
0598                     mixedPainter->setOpacity(mixedPainter->opacity() * opacity / 255.0);
0599 
0600                     mixedPainter->drawPixmap(annotRect.topLeft(), pixmap.scaled(annotBoundary.width() * dpr, annotBoundary.height() * dpr), dInnerRect.toAlignedRect());
0601 
0602                     mixedPainter->restore();
0603                 }
0604             }
0605             // draw GeomAnnotation
0606             else if (type == Okular::Annotation::AGeom) {
0607                 Okular::GeomAnnotation *geom = (Okular::GeomAnnotation *)a;
0608                 // check whether there's anything to draw
0609                 if (geom->style().width() || geom->geometricalInnerColor().isValid()) {
0610                     mixedPainter->save();
0611                     const double width = geom->style().width() * Okular::Utils::realDpi(nullptr).width() / (72.0 * 2.0) * scaledWidth / page->width();
0612                     QRectF r(.0, .0, annotBoundary.width(), annotBoundary.height());
0613                     r.adjust(width, width, -width, -width);
0614                     r.translate(annotBoundary.topLeft());
0615                     if (geom->geometricalInnerColor().isValid()) {
0616                         r.adjust(width, width, -width, -width);
0617                         const QColor color = geom->geometricalInnerColor();
0618                         mixedPainter->setPen(Qt::NoPen);
0619                         mixedPainter->setBrush(QColor(color.red(), color.green(), color.blue(), opacity));
0620                         if (geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare) {
0621                             mixedPainter->drawRect(r);
0622                         } else {
0623                             mixedPainter->drawEllipse(r);
0624                         }
0625                         r.adjust(-width, -width, width, width);
0626                     }
0627                     if (geom->style().width()) // need to check the original size here..
0628                     {
0629                         mixedPainter->setPen(buildPen(a, width * 2, acolor));
0630                         mixedPainter->setBrush(Qt::NoBrush);
0631                         if (geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare) {
0632                             mixedPainter->drawRect(r);
0633                         } else {
0634                             mixedPainter->drawEllipse(r);
0635                         }
0636                     }
0637                     mixedPainter->restore();
0638                 }
0639             }
0640 
0641             // draw extents rectangle
0642             if (Okular::Settings::debugDrawAnnotationRect()) {
0643                 mixedPainter->setPen(a->style().color());
0644                 mixedPainter->drawRect(annotBoundary);
0645             }
0646         }
0647     }
0648 
0649     if (boundingRectOnlyAnn) {
0650         QRect annotBoundary = boundingRectOnlyAnn->transformedBoundingRectangle().geometry(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft());
0651         mixedPainter->setPen(Qt::DashLine);
0652         mixedPainter->drawRect(annotBoundary);
0653     }
0654 
0655     /** 6 -- MIXED FLOW. Draw LINKS+IMAGES BORDER on ACTIVE PAINTER  **/
0656     if (enhanceLinks || enhanceImages) {
0657         mixedPainter->save();
0658         mixedPainter->scale(scaledWidth, scaledHeight);
0659         mixedPainter->translate(-crop.left, -crop.top);
0660 
0661         QColor normalColor = QApplication::palette().color(QPalette::Active, QPalette::Highlight);
0662         // enlarging limits for intersection is like growing the 'rectGeometry' below
0663         QRect limitsEnlarged = limits;
0664         limitsEnlarged.adjust(-2, -2, 2, 2);
0665         // draw rects that are inside the 'limits' paint region as opaque rects
0666         for (Okular::ObjectRect *rect : page->m_rects) {
0667             if ((enhanceLinks && rect->objectType() == Okular::ObjectRect::Action) || (enhanceImages && rect->objectType() == Okular::ObjectRect::Image)) {
0668                 if (limitsEnlarged.intersects(rect->boundingRect(scaledWidth, scaledHeight).translated(-scaledCrop.topLeft()))) {
0669                     mixedPainter->strokePath(rect->region(), QPen(normalColor, 0));
0670                 }
0671             }
0672         }
0673         mixedPainter->restore();
0674     }
0675 
0676     /** 7 -- BUFFERED FLOW. Copy BACKPIXMAP on DESTINATION PAINTER **/
0677     if (useBackBuffer) {
0678         delete mixedPainter;
0679         destPainter->drawPixmap(limits.left(), limits.top(), *backPixmap);
0680         delete backPixmap;
0681     }
0682 
0683     // delete object containers
0684     delete bufferedHighlights;
0685     delete bufferedAnnotations;
0686     delete unbufferedAnnotations;
0687 }
0688 
0689 void PagePainter::recolor(QImage *image, const QColor &foreground, const QColor &background)
0690 {
0691     if (image->format() != QImage::Format_ARGB32_Premultiplied) {
0692         qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
0693         *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
0694     }
0695 
0696     Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
0697 
0698     const float scaleRed = background.redF() - foreground.redF();
0699     const float scaleGreen = background.greenF() - foreground.greenF();
0700     const float scaleBlue = background.blueF() - foreground.blueF();
0701 
0702     const int foreground_red = foreground.red();
0703     const int foreground_green = foreground.green();
0704     const int foreground_blue = foreground.blue();
0705 
0706     QRgb *data = reinterpret_cast<QRgb *>(image->bits());
0707     const int pixels = image->width() * image->height();
0708 
0709     for (int i = 0; i < pixels; ++i) {
0710         const int lightness = qGray(data[i]);
0711 
0712         const float r = scaleRed * lightness + foreground_red;
0713         const float g = scaleGreen * lightness + foreground_green;
0714         const float b = scaleBlue * lightness + foreground_blue;
0715 
0716         const unsigned a = qAlpha(data[i]);
0717         data[i] = qRgba(r, g, b, a);
0718     }
0719 }
0720 
0721 void PagePainter::blackWhite(QImage *image, int contrast, int threshold)
0722 {
0723     unsigned int *data = reinterpret_cast<unsigned int *>(image->bits());
0724     int con = contrast;
0725     int thr = 255 - threshold;
0726 
0727     int pixels = image->width() * image->height();
0728     for (int i = 0; i < pixels; ++i) {
0729         // Piecewise linear function of val, through (0, 0), (thr, 128), (255, 255)
0730         int val = qGray(data[i]);
0731         if (val > thr) {
0732             val = 128 + (127 * (val - thr)) / (255 - thr);
0733         } else if (val < thr) {
0734             val = (128 * val) / thr;
0735         }
0736 
0737         // Linear contrast stretching through (thr, thr)
0738         if (con > 2) {
0739             val = thr + (val - thr) * con / 2;
0740             val = qBound(0, val, 255);
0741         }
0742 
0743         const unsigned a = qAlpha(data[i]);
0744         data[i] = qRgba(val, val, val, a);
0745     }
0746 }
0747 
0748 void PagePainter::invertLightness(QImage *image)
0749 {
0750     if (image->format() != QImage::Format_ARGB32_Premultiplied) {
0751         qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
0752         *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
0753     }
0754 
0755     Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
0756 
0757     QRgb *data = reinterpret_cast<QRgb *>(image->bits());
0758     int pixels = image->width() * image->height();
0759     for (int i = 0; i < pixels; ++i) {
0760         // Invert lightness of the pixel using the cylindric HSL color model.
0761         // Algorithm is based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB (2019-03-17).
0762         // Important simplifications are that inverting lightness does not change chroma and hue.
0763         // This means the sector (of the chroma/hue plane) is not changed,
0764         // so we can use a linear calculation after determining the sector using qMin() and qMax().
0765         uchar R = qRed(data[i]);
0766         uchar G = qGreen(data[i]);
0767         uchar B = qBlue(data[i]);
0768 
0769         // Get only the needed HSL components. These are chroma C and the common component m.
0770         // Get common component m
0771         uchar m = qMin(R, qMin(G, B));
0772         // Remove m from color components
0773         R -= m;
0774         G -= m;
0775         B -= m;
0776         // Get chroma C
0777         uchar C = qMax(R, qMax(G, B));
0778 
0779         // Get common component m' after inverting lightness L.
0780         // Hint: Lightness L = m + C / 2; L' = 255 - L = 255 - (m + C / 2) => m' = 255 - C - m
0781         uchar m_ = 255 - C - m;
0782 
0783         // Add m' to color compontents
0784         R += m_;
0785         G += m_;
0786         B += m_;
0787 
0788         // Save new color
0789         const unsigned A = qAlpha(data[i]);
0790         data[i] = qRgba(R, G, B, A);
0791     }
0792 }
0793 
0794 void PagePainter::invertLuma(QImage *image, float Y_R, float Y_G, float Y_B)
0795 {
0796     if (image->format() != QImage::Format_ARGB32_Premultiplied) {
0797         qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
0798         *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
0799     }
0800 
0801     Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
0802 
0803     QRgb *data = reinterpret_cast<QRgb *>(image->bits());
0804     int pixels = image->width() * image->height();
0805     for (int i = 0; i < pixels; ++i) {
0806         uchar R = qRed(data[i]);
0807         uchar G = qGreen(data[i]);
0808         uchar B = qBlue(data[i]);
0809 
0810         invertLumaPixel(R, G, B, Y_R, Y_G, Y_B);
0811 
0812         // Save new color
0813         const unsigned A = qAlpha(data[i]);
0814         data[i] = qRgba(R, G, B, A);
0815     }
0816 }
0817 
0818 void PagePainter::invertLumaPixel(uchar &R, uchar &G, uchar &B, float Y_R, float Y_G, float Y_B)
0819 {
0820     // Invert luma of the pixel using the bicone HCY color model, stretched to cylindric HSY.
0821     // Algorithm is based on https://en.wikipedia.org/wiki/HSL_and_HSV#Luma,_chroma_and_hue_to_RGB (2019-03-19).
0822     // For an illustration see https://experilous.com/1/product/make-it-colorful/ (2019-03-19).
0823 
0824     // Special case: The algorithm does not work when hue is undefined.
0825     if (R == G && G == B) {
0826         R = 255 - R;
0827         G = 255 - G;
0828         B = 255 - B;
0829         return;
0830     }
0831 
0832     // Get input and output luma Y, Y_inv in range 0..255
0833     float Y = R * Y_R + G * Y_G + B * Y_B;
0834     float Y_inv = 255 - Y;
0835 
0836     // Get common component m and remove from color components.
0837     // This moves us to the bottom faces of the HCY bicone, i. e. we get C and X in R, G, B.
0838     uint_fast8_t m = qMin(R, qMin(G, B));
0839     R -= m;
0840     G -= m;
0841     B -= m;
0842 
0843     // We operate in a hue plane of the luma/chroma/hue bicone.
0844     // The hue plane is a triangle.
0845     // This bicone is distorted, so we can not simply mirror the triangle.
0846     // We need to stretch it to a luma/saturation rectangle, so we need to stretch chroma C and the proportional X.
0847 
0848     // First, we need to calculate luma Y_full_C for the outer corner of the triangle.
0849     // Then we can interpolate the max chroma C_max, C_inv_max for our luma Y, Y_inv.
0850     // Then we calculate C_inv and X_inv by scaling them by the ratio of C_max and C_inv_max.
0851 
0852     // Calculate luma Y_full_C (in range equivalent to gray 0..255) for chroma = 1 at this hue.
0853     // Piecewise linear, with the corners of the bicone at the sum of one or two luma coefficients.
0854     float Y_full_C;
0855     if (R >= B && B >= G) {
0856         Y_full_C = 255 * Y_R + 255 * Y_B * B / R;
0857     } else if (R >= G && G >= B) {
0858         Y_full_C = 255 * Y_R + 255 * Y_G * G / R;
0859     } else if (G >= R && R >= B) {
0860         Y_full_C = 255 * Y_G + 255 * Y_R * R / G;
0861     } else if (G >= B && B >= R) {
0862         Y_full_C = 255 * Y_G + 255 * Y_B * B / G;
0863     } else if (B >= G && G >= R) {
0864         Y_full_C = 255 * Y_B + 255 * Y_G * G / B;
0865     } else {
0866         Y_full_C = 255 * Y_B + 255 * Y_R * R / B;
0867     }
0868 
0869     // Calculate C_max, C_inv_max, to scale C and X.
0870     float C_max, C_inv_max;
0871     if (Y >= Y_full_C) {
0872         C_max = Y_inv / (255 - Y_full_C);
0873     } else {
0874         C_max = Y / Y_full_C;
0875     }
0876     if (Y_inv >= Y_full_C) {
0877         C_inv_max = Y / (255 - Y_full_C);
0878     } else {
0879         C_inv_max = Y_inv / Y_full_C;
0880     }
0881 
0882     // Scale C and X. C and X already lie in R, G, B.
0883     float C_scale = C_inv_max / C_max;
0884     float R_ = R * C_scale;
0885     float G_ = G * C_scale;
0886     float B_ = B * C_scale;
0887 
0888     // Calculate missing luma (in range 0..255), to get common component m_inv
0889     float m_inv = Y_inv - (Y_R * R_ + Y_G * G_ + Y_B * B_);
0890 
0891     // Add m_inv to color compontents
0892     R_ += m_inv;
0893     G_ += m_inv;
0894     B_ += m_inv;
0895 
0896     // Return colors rounded
0897     R = R_ + 0.5;
0898     G = G_ + 0.5;
0899     B = B_ + 0.5;
0900 }
0901 
0902 void PagePainter::hueShiftPositive(QImage *image)
0903 {
0904     if (image->format() != QImage::Format_ARGB32_Premultiplied) {
0905         qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
0906         *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
0907     }
0908 
0909     Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
0910 
0911     QRgb *data = reinterpret_cast<QRgb *>(image->bits());
0912     int pixels = image->width() * image->height();
0913     for (int i = 0; i < pixels; ++i) {
0914         uchar R = qRed(data[i]);
0915         uchar G = qGreen(data[i]);
0916         uchar B = qBlue(data[i]);
0917 
0918         // Save new color
0919         const unsigned A = qAlpha(data[i]);
0920         data[i] = qRgba(B, R, G, A);
0921     }
0922 }
0923 
0924 void PagePainter::hueShiftNegative(QImage *image)
0925 {
0926     if (image->format() != QImage::Format_ARGB32_Premultiplied) {
0927         qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
0928         *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
0929     }
0930 
0931     Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
0932 
0933     QRgb *data = reinterpret_cast<QRgb *>(image->bits());
0934     int pixels = image->width() * image->height();
0935     for (int i = 0; i < pixels; ++i) {
0936         uchar R = qRed(data[i]);
0937         uchar G = qGreen(data[i]);
0938         uchar B = qBlue(data[i]);
0939 
0940         // Save new color
0941         const unsigned A = qAlpha(data[i]);
0942         data[i] = qRgba(G, B, R, A);
0943     }
0944 }
0945 
0946 void PagePainter::drawShapeOnImage(QImage &image, const NormalizedPath &normPath, bool closeShape, const QPen &pen, const QBrush &brush, double penWidthMultiplier, RasterOperation op
0947                                    // float antiAliasRadius
0948 )
0949 {
0950     // safety checks
0951     int pointsNumber = normPath.size();
0952     if (pointsNumber < 2) {
0953         return;
0954     }
0955 
0956     const double dpr = image.devicePixelRatio();
0957     const double fImageWidth = image.width() / dpr;
0958     const double fImageHeight = image.height() / dpr;
0959 
0960     // stroke outline
0961     double penWidth = (double)pen.width() * penWidthMultiplier;
0962     QPainter painter(&image);
0963     painter.setRenderHint(QPainter::Antialiasing);
0964     QPen pen2 = pen;
0965     pen2.setWidthF(penWidth);
0966     painter.setPen(pen2);
0967     painter.setBrush(brush);
0968 
0969     if (op == Multiply) {
0970         painter.setCompositionMode(QPainter::CompositionMode_Multiply);
0971     }
0972 
0973     if (brush.style() == Qt::NoBrush) {
0974         // create a polygon
0975         QPolygonF poly(closeShape ? pointsNumber + 1 : pointsNumber);
0976         for (int i = 0; i < pointsNumber; ++i) {
0977             poly[i] = QPointF(normPath[i].x * fImageWidth, normPath[i].y * fImageHeight);
0978         }
0979         if (closeShape) {
0980             poly[pointsNumber] = poly[0];
0981         }
0982 
0983         painter.drawPolyline(poly);
0984     } else {
0985         // create a 'path'
0986         QPainterPath path;
0987         path.setFillRule(Qt::WindingFill);
0988 
0989         path.moveTo(normPath[0].x * fImageWidth, normPath[0].y * fImageHeight);
0990         for (int i = 1; i < pointsNumber; i++) {
0991             path.lineTo(normPath[i].x * fImageWidth, normPath[i].y * fImageHeight);
0992         }
0993         if (closeShape) {
0994             path.closeSubpath();
0995         }
0996 
0997         painter.drawPath(path);
0998     }
0999 }
1000 
1001 void PagePainter::drawEllipseOnImage(QImage &image, const NormalizedPath &rect, const QPen &pen, const QBrush &brush, double penWidthMultiplier, RasterOperation op)
1002 {
1003     const double dpr = image.devicePixelRatio();
1004     const double fImageWidth = image.width() / dpr;
1005     const double fImageHeight = image.height() / dpr;
1006 
1007     // stroke outline
1008     const double penWidth = (double)pen.width() * penWidthMultiplier;
1009     QPainter painter(&image);
1010     painter.setRenderHint(QPainter::Antialiasing);
1011     QPen pen2 = pen;
1012     pen2.setWidthF(penWidth);
1013     painter.setPen(pen2);
1014     painter.setBrush(brush);
1015 
1016     if (op == Multiply) {
1017         painter.setCompositionMode(QPainter::CompositionMode_Multiply);
1018     }
1019 
1020     const QPointF &topLeft {rect[0].x * fImageWidth, rect[0].y * fImageHeight};
1021     const QSizeF &size {(rect[1].x - rect[0].x) * fImageWidth, (rect[1].y - rect[0].y) * fImageHeight};
1022     const QRectF imgRect {topLeft, size};
1023     if (brush.style() == Qt::NoBrush) {
1024         painter.drawArc(imgRect, 0, 16 * 360);
1025     } else {
1026         painter.drawEllipse(imgRect);
1027     }
1028 }
1029 
1030 LineAnnotPainter::LineAnnotPainter(const Okular::LineAnnotation *a, QSizeF pageSize, double pageScale, const QTransform &toNormalizedImage)
1031     : la {a}
1032     , pageSize {pageSize}
1033     , pageScale {pageScale}
1034     , toNormalizedImage {toNormalizedImage}
1035     , aspectRatio {pageSize.height() / pageSize.width()}
1036     , linePen {buildPen(a, a->style().width(), a->style().color())}
1037 {
1038     if ((la->lineClosed() || la->transformedLinePoints().count() == 2) && la->lineInnerColor().isValid()) {
1039         fillBrush = QBrush(la->lineInnerColor());
1040     }
1041 }
1042 
1043 void LineAnnotPainter::draw(QImage &image) const
1044 {
1045     const QList<Okular::NormalizedPoint> transformedLinePoints = la->transformedLinePoints();
1046     if (transformedLinePoints.count() == 2) {
1047         const Okular::NormalizedPoint delta {transformedLinePoints.last().x - transformedLinePoints.first().x, transformedLinePoints.first().y - transformedLinePoints.last().y};
1048         const double angle {atan2(delta.y * aspectRatio, delta.x)};
1049         const double cosA {cos(-angle)};
1050         const double sinA {sin(-angle)};
1051         const QTransform tmpMatrix = QTransform {cosA, sinA / aspectRatio, -sinA, cosA / aspectRatio, transformedLinePoints.first().x, transformedLinePoints.first().y};
1052         const double deaspectedY {delta.y * aspectRatio};
1053         const double mainSegmentLength {sqrt(delta.x * delta.x + deaspectedY * deaspectedY)};
1054         const double lineendSize {std::min(6. * la->style().width() / pageSize.width(), mainSegmentLength / 2.)};
1055 
1056         drawShortenedLine(mainSegmentLength, lineendSize, image, tmpMatrix);
1057         drawLineEnds(mainSegmentLength, lineendSize, image, tmpMatrix);
1058         drawLeaderLine(0., image, tmpMatrix);
1059         drawLeaderLine(mainSegmentLength, image, tmpMatrix);
1060     } else if (transformedLinePoints.count() > 2) {
1061         drawMainLine(image);
1062     }
1063 }
1064 
1065 void LineAnnotPainter::drawMainLine(QImage &image) const
1066 {
1067     // draw the line as normalized path into image
1068     PagePainter::drawShapeOnImage(image, transformPath(la->transformedLinePoints(), toNormalizedImage), la->lineClosed(), linePen, fillBrush, pageScale);
1069 }
1070 
1071 void LineAnnotPainter::drawShortenedLine(double mainSegmentLength, double size, QImage &image, const QTransform &toNormalizedPage) const
1072 {
1073     const QTransform combinedTransform {toNormalizedPage * toNormalizedImage};
1074     const QList<Okular::NormalizedPoint> path {{shortenForArrow(size, la->lineStartStyle()), 0}, {mainSegmentLength - shortenForArrow(size, la->lineEndStyle()), 0}};
1075     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), la->lineClosed(), linePen, fillBrush, pageScale);
1076 }
1077 
1078 void LineAnnotPainter::drawLineEnds(double mainSegmentLength, double size, QImage &image, const QTransform &transform) const
1079 {
1080     switch (la->lineStartStyle()) {
1081     case Okular::LineAnnotation::Square:
1082         drawLineEndSquare(0, -size, transform, image);
1083         break;
1084     case Okular::LineAnnotation::Circle:
1085         drawLineEndCircle(0, -size, transform, image);
1086         break;
1087     case Okular::LineAnnotation::Diamond:
1088         drawLineEndDiamond(0, -size, transform, image);
1089         break;
1090     case Okular::LineAnnotation::OpenArrow:
1091         drawLineEndArrow(0, -size, 1., false, transform, image);
1092         break;
1093     case Okular::LineAnnotation::ClosedArrow:
1094         drawLineEndArrow(0, -size, 1., true, transform, image);
1095         break;
1096     case Okular::LineAnnotation::None:
1097         break;
1098     case Okular::LineAnnotation::Butt:
1099         drawLineEndButt(0, size, transform, image);
1100         break;
1101     case Okular::LineAnnotation::ROpenArrow:
1102         drawLineEndArrow(0, size, 1., false, transform, image);
1103         break;
1104     case Okular::LineAnnotation::RClosedArrow:
1105         drawLineEndArrow(0, size, 1., true, transform, image);
1106         break;
1107     case Okular::LineAnnotation::Slash:
1108         drawLineEndSlash(0, -size, transform, image);
1109         break;
1110     }
1111     switch (la->lineEndStyle()) {
1112     case Okular::LineAnnotation::Square:
1113         drawLineEndSquare(mainSegmentLength, size, transform, image);
1114         break;
1115     case Okular::LineAnnotation::Circle:
1116         drawLineEndCircle(mainSegmentLength, size, transform, image);
1117         break;
1118     case Okular::LineAnnotation::Diamond:
1119         drawLineEndDiamond(mainSegmentLength, size, transform, image);
1120         break;
1121     case Okular::LineAnnotation::OpenArrow:
1122         drawLineEndArrow(mainSegmentLength, size, 1., false, transform, image);
1123         break;
1124     case Okular::LineAnnotation::ClosedArrow:
1125         drawLineEndArrow(mainSegmentLength, size, 1., true, transform, image);
1126         break;
1127     case Okular::LineAnnotation::None:
1128         break;
1129     case Okular::LineAnnotation::Butt:
1130         drawLineEndButt(mainSegmentLength, size, transform, image);
1131         break;
1132     case Okular::LineAnnotation::ROpenArrow:
1133         drawLineEndArrow(mainSegmentLength, size, -1., false, transform, image);
1134         break;
1135     case Okular::LineAnnotation::RClosedArrow:
1136         drawLineEndArrow(mainSegmentLength, size, -1., true, transform, image);
1137         break;
1138     case Okular::LineAnnotation::Slash:
1139         drawLineEndSlash(mainSegmentLength, size, transform, image);
1140         break;
1141     }
1142 }
1143 
1144 void LineAnnotPainter::drawLineEndArrow(double xEndPos, double size, double flipX, bool close, const QTransform &toNormalizedPage, QImage &image) const
1145 {
1146     const QTransform combinedTransform {toNormalizedPage * toNormalizedImage};
1147     const QList<Okular::NormalizedPoint> path {
1148         {xEndPos - size * flipX, size / 2.},
1149         {xEndPos, 0},
1150         {xEndPos - size * flipX, -size / 2.},
1151     };
1152     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), close, linePen, fillBrush, pageScale);
1153 }
1154 
1155 void LineAnnotPainter::drawLineEndButt(double xEndPos, double size, const QTransform &toNormalizedPage, QImage &image) const
1156 {
1157     const QTransform combinedTransform {toNormalizedPage * toNormalizedImage};
1158     const double halfSize {size / 2.};
1159     const QList<Okular::NormalizedPoint> path {
1160         {xEndPos, halfSize},
1161         {xEndPos, -halfSize},
1162     };
1163     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale);
1164 }
1165 
1166 void LineAnnotPainter::drawLineEndCircle(double xEndPos, double size, const QTransform &toNormalizedPage, QImage &image) const
1167 {
1168     /* transform the circle midpoint to intermediate normalized coordinates
1169      * where it's easy to construct the bounding rect of the circle */
1170     Okular::NormalizedPoint center;
1171     toNormalizedPage.map(xEndPos - size / 2., 0, &center.x, &center.y);
1172     const double halfSize {size / 2.};
1173     const QList<Okular::NormalizedPoint> path {
1174         {center.x - halfSize, center.y - halfSize / aspectRatio},
1175         {center.x + halfSize, center.y + halfSize / aspectRatio},
1176     };
1177 
1178     /* then transform bounding rect with toNormalizedImage */
1179     PagePainter::drawEllipseOnImage(image, transformPath(path, toNormalizedImage), linePen, fillBrush, pageScale);
1180 }
1181 
1182 void LineAnnotPainter::drawLineEndSquare(double xEndPos, double size, const QTransform &toNormalizedPage, QImage &image) const
1183 {
1184     const QTransform combinedTransform {toNormalizedPage * toNormalizedImage};
1185     const QList<Okular::NormalizedPoint> path {{xEndPos, size / 2.}, {xEndPos - size, size / 2.}, {xEndPos - size, -size / 2.}, {xEndPos, -size / 2.}};
1186     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale);
1187 }
1188 
1189 void LineAnnotPainter::drawLineEndDiamond(double xEndPos, double size, const QTransform &toNormalizedPage, QImage &image) const
1190 {
1191     const QTransform combinedTransform {toNormalizedPage * toNormalizedImage};
1192     const QList<Okular::NormalizedPoint> path {{xEndPos, 0}, {xEndPos - size / 2., size / 2.}, {xEndPos - size, 0}, {xEndPos - size / 2., -size / 2.}};
1193     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale);
1194 }
1195 
1196 void LineAnnotPainter::drawLineEndSlash(double xEndPos, double size, const QTransform &toNormalizedPage, QImage &image) const
1197 {
1198     const QTransform combinedTransform {toNormalizedPage * toNormalizedImage};
1199     const double halfSize {size / 2.};
1200     const double xOffset {cos(M_PI / 3.) * halfSize};
1201     const QList<Okular::NormalizedPoint> path {
1202         {xEndPos - xOffset, halfSize},
1203         {xEndPos + xOffset, -halfSize},
1204     };
1205     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale);
1206 }
1207 
1208 void LineAnnotPainter::drawLeaderLine(double xEndPos, QImage &image, const QTransform &toNormalizedPage) const
1209 {
1210     const QTransform combinedTransform = toNormalizedPage * toNormalizedImage;
1211     const double ll = aspectRatio * la->lineLeadingForwardPoint() / pageSize.height();
1212     const double lle = aspectRatio * la->lineLeadingBackwardPoint() / pageSize.height();
1213     const int sign {ll > 0 ? -1 : 1};
1214     QList<Okular::NormalizedPoint> path;
1215 
1216     if (fabs(ll) > 0) {
1217         path.append({xEndPos, ll});
1218         // do we have the extension on the "back"?
1219         if (fabs(lle) > 0) {
1220             path.append({xEndPos, sign * lle});
1221         } else {
1222             path.append({xEndPos, 0});
1223         }
1224     }
1225     PagePainter::drawShapeOnImage(image, transformPath(path, combinedTransform), false, linePen, fillBrush, pageScale);
1226 }
1227 
1228 double LineAnnotPainter::shortenForArrow(double size, Okular::LineAnnotation::TermStyle endStyle)
1229 {
1230     double shortenBy {0};
1231 
1232     if (endStyle == Okular::LineAnnotation::Square || endStyle == Okular::LineAnnotation::Circle || endStyle == Okular::LineAnnotation::Diamond || endStyle == Okular::LineAnnotation::ClosedArrow) {
1233         shortenBy = size;
1234     }
1235 
1236     return shortenBy;
1237 }
1238 
1239 /* kate: replace-tabs on; indent-width 4; */