diff --git a/Pdf4QtLib/sources/pdftransparencyrenderer.cpp b/Pdf4QtLib/sources/pdftransparencyrenderer.cpp index a23bb10..8086ca7 100644 --- a/Pdf4QtLib/sources/pdftransparencyrenderer.cpp +++ b/Pdf4QtLib/sources/pdftransparencyrenderer.cpp @@ -742,7 +742,7 @@ QImage PDFTransparencyRenderer::toImage(bool use16Bit, bool usePaper, PDFRGB pap PDFFloatBitmap softMask(paperImage.getWidth(), paperImage.getHeight(), PDFPixelFormat::createOpacityMask()); softMask.makeOpaque(); - QRect blendRegion(0, 0, floatImage.getWidth(), floatImage.getHeight()); + QRect blendRegion(0, 0, int(floatImage.getWidth()), int(floatImage.getHeight())); PDFFloatBitmapWithColorSpace::blend(floatImage, paperImage, paperImage, paperImage, softMask, false, 1.0f, BlendMode::Normal, false, 0xFFFF, PDFFloatBitmap::OverprintMode::NoOveprint, blendRegion); return toImageImpl(paperImage, use16Bit); @@ -755,9 +755,6 @@ void PDFTransparencyRenderer::performPathPainting(const QPainterPath& path, bool { Q_UNUSED(fillRule); - PDFPainterPathSampler clipSampler(m_painterStateStack.top().clipPath, m_settings.samplesCount, 1.0f, - m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); - QMatrix worldMatrix = getCurrentWorldMatrix(); const PDFReal shapeStroking = getShapeStroking(); @@ -783,7 +780,8 @@ void PDFTransparencyRenderer::performPathPainting(const QPainterPath& path, bool // and world matrix. Path can be translated outside of the paint area. if (fillRect.isValid()) { - PDFPainterPathSampler pathSampler(worldPath, m_settings.samplesCount, 0.0f, m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); + PDFPainterPathSampler clipSampler(m_painterStateStack.top().clipPath, m_settings.samplesCount, 1.0f, fillRect, m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); + PDFPainterPathSampler pathSampler(worldPath, m_settings.samplesCount, 0.0f, fillRect, m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); const PDFMappedColor& fillColor = getMappedFillColor(); for (int x = fillRect.left(); x < fillRect.right(); ++x) @@ -841,7 +839,8 @@ void PDFTransparencyRenderer::performPathPainting(const QPainterPath& path, bool // and world matrix. Path can be translated outside of the paint area. if (strokeRect.isValid()) { - PDFPainterPathSampler pathSampler(worldPath, m_settings.samplesCount, 0.0f, m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); + PDFPainterPathSampler clipSampler(m_painterStateStack.top().clipPath, m_settings.samplesCount, 1.0f, strokeRect, m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); + PDFPainterPathSampler pathSampler(worldPath, m_settings.samplesCount, 0.0f, strokeRect, m_settings.flags.testFlag(PDFTransparencyRendererSettings::PrecisePathSampler)); const PDFMappedColor& strokeColor = getMappedStrokeColor(); for (int x = strokeRect.left(); x < strokeRect.right(); ++x) @@ -918,6 +917,14 @@ void PDFTransparencyRenderer::performUpdateGraphicsState(const PDFPageContentPro m_mappedFillColor.dirty(); } + if (stateFlags.testFlag(PDFPageContentProcessorState::StateSoftMask)) + { + if (getGraphicState()->getSoftMask()) + { + reportRenderErrorOnce(RenderErrorType::NotImplemented, PDFTranslationContext::tr("Soft mask not implemented.")); + } + } + BaseClass::performUpdateGraphicsState(state); } @@ -1045,6 +1052,20 @@ void PDFTransparencyRenderer::performTextEnd(ProcessOrder order) } } +void PDFTransparencyRenderer::performImagePainting(const QImage& image) +{ + Q_UNUSED(image); + + reportRenderErrorOnce(RenderErrorType::NotImplemented, PDFTranslationContext::tr("Image painting not implemented.")); +} + +void PDFTransparencyRenderer::performMeshPainting(const PDFMesh& mesh) +{ + Q_UNUSED(mesh); + + reportRenderErrorOnce(RenderErrorType::NotImplemented, PDFTranslationContext::tr("Mesh painting not implemented.")); +} + PDFReal PDFTransparencyRenderer::getShapeStroking() const { return getGraphicState()->getAlphaIsShape() ? getGraphicState()->getAlphaStroking() : 1.0; @@ -1532,25 +1553,32 @@ PDFInkMapping PDFInkMapper::createMapping(const PDFAbstractColorSpace* sourceCol return mapping; } -PDFPainterPathSampler::PDFPainterPathSampler(QPainterPath path, int samplesCount, PDFColorComponent defaultShape, bool precise) : +PDFPainterPathSampler::PDFPainterPathSampler(QPainterPath path, int samplesCount, PDFColorComponent defaultShape, QRect fillRect, bool precise) : m_defaultShape(defaultShape), - m_samplesCount(samplesCount), - m_precise(precise), - m_path(qMove(path)) + m_samplesCount(qMax(samplesCount, 1)), + m_path(qMove(path)), + m_fillRect(fillRect), + m_precise(precise) { if (!precise) { m_fillPolygon = m_path.toFillPolygon(); + prepareScanLines(); } } PDFColorComponent PDFPainterPathSampler::sample(QPoint point) const { - if (m_path.isEmpty()) + if (m_path.isEmpty() || !m_fillRect.contains(point)) { return m_defaultShape; } + if (!m_scanLineInfo.empty()) + { + return sampleByScanLine(point); + } + const qreal coordX1 = point.x(); const qreal coordX2 = coordX1 + 1.0; const qreal coordY1 = point.y(); @@ -1639,6 +1667,181 @@ PDFColorComponent PDFPainterPathSampler::sample(QPoint point) const return sampleValue; } +PDFColorComponent PDFPainterPathSampler::sampleByScanLine(QPoint point) const +{ + int scanLinePosition = point.y() - m_fillRect.y(); + + size_t scanLineCountPerPixel = getScanLineCountPerPixel(); + size_t scanLineTopRow = scanLinePosition * scanLineCountPerPixel; + size_t scanLineBottomRow = scanLineTopRow + scanLineCountPerPixel - 1; + size_t scanLineGridRowTop = scanLineTopRow + 1; + + Qt::FillRule fillRule = m_path.fillRule(); + + auto performSampling = [&](size_t scanLineIndex, PDFReal firstOrdinate, int sampleCount, PDFReal step, PDFReal gain) + { + ScanLineInfo info = m_scanLineInfo[scanLineIndex]; + auto it = std::next(m_scanLineSamples.cbegin(), info.indexStart); + auto itEnd = std::next(m_scanLineSamples.cbegin(), info.indexEnd); + + PDFReal ordinate = firstOrdinate; + PDFColorComponent value = 0.0; + auto ordinateIt = std::lower_bound(it, itEnd, ordinate); + for (int i = 0; i < sampleCount; ++i) + { + int windingNumber = ordinateIt->windingNumber; + + const bool inside = (fillRule == Qt::WindingFill) ? windingNumber != 0 : windingNumber % 2 != 0; + if (inside) + { + value += gain; + } + + ordinate += step; + ordinateIt = std::lower_bound(ordinateIt, itEnd, ordinate); + } + + return value; + }; + + const qreal coordX1 = point.x(); + const PDFReal cornerValue = performSampling(scanLineTopRow, coordX1, 2, 1.0, 1.0) + performSampling(scanLineBottomRow, coordX1, 2, 1.0, 1.0); + + if (qFuzzyIsNull(4.0 - cornerValue)) + { + // Whole inside + return 1.0; + } + + if (qFuzzyIsNull(cornerValue)) + { + // Whole outside + return 0.0; + } + + const qreal offset = 1.0f / PDFColorComponent(m_samplesCount + 1); + PDFColorComponent sampleValue = 0.0f; + const PDFColorComponent sampleGain = 1.0f / PDFColorComponent(m_samplesCount * m_samplesCount); + + for (size_t i = 0; i < m_samplesCount; ++i) + { + sampleValue += performSampling(scanLineGridRowTop++, coordX1 + offset, m_samplesCount, offset, sampleGain); + } + + return sampleValue; +} + +size_t PDFPainterPathSampler::getScanLineCountPerPixel() const +{ + return m_samplesCount + 2; +} + +void PDFPainterPathSampler::prepareScanLines() +{ + if (m_fillPolygon.isEmpty()) + { + return; + } + + for (int yOffset = m_fillRect.top(); yOffset < m_fillRect.bottom(); ++yOffset) + { + const qreal coordY1 = yOffset; + const qreal coordY2 = coordY1 + 1.0; + + // Top pixel line + if (m_scanLineInfo.empty()) + { + m_scanLineInfo.emplace_back(createScanLine(coordY1)); + } + else + { + m_scanLineInfo.emplace_back(m_scanLineInfo.back()); + } + + // Sample grid + const qreal offset = 1.0f / PDFColorComponent(m_samplesCount + 1); + for (int iy = 0; iy < m_samplesCount; ++iy) + { + const qreal y = offset * (iy + 1) + coordY1; + m_scanLineInfo.emplace_back(createScanLine(y)); + } + + // Bottom pixel line + m_scanLineInfo.emplace_back(createScanLine(coordY2)); + } +} + +PDFPainterPathSampler::ScanLineInfo PDFPainterPathSampler::createScanLine(qreal y) +{ + ScanLineInfo result; + result.indexStart = m_scanLineSamples.size(); + + // Add start item + m_scanLineSamples.emplace_back(-std::numeric_limits::infinity(), 0); + + // Traverse polygon, add sample for each polygon line, we must + // also implicitly close last edge (if polygon is not closed) + for (int i = 1; i < m_fillPolygon.size(); ++i) + { + createScanLineSample(m_fillPolygon[i - 1], m_fillPolygon[i], y); + } + + if (m_fillPolygon.front() != m_fillPolygon.back()) + { + createScanLineSample(m_fillPolygon.back(), m_fillPolygon.front(), y); + } + + // Add end item + m_scanLineSamples.emplace_back(+std::numeric_limits::infinity(), 0); + + result.indexEnd = m_scanLineSamples.size(); + + auto it = std::next(m_scanLineSamples.begin(), result.indexStart); + auto itEnd = std::next(m_scanLineSamples.begin(), result.indexEnd); + + // Jakub Melka: now, sort the line samples and compute properly the winding number + std::sort(it, itEnd); + + int currentWindingNumber = 0; + for (; it != itEnd; ++it) + { + currentWindingNumber += it->windingNumber; + it->windingNumber = currentWindingNumber; + } + + return result; +} + +void PDFPainterPathSampler::createScanLineSample(const QPointF& p1, const QPointF& p2, qreal y) +{ + PDFReal y1 = p1.y(); + PDFReal y2 = p2.y(); + + if (qFuzzyIsNull(y2 - y1)) + { + // Ignore horizontal lines + return; + } + + PDFReal x1 = p1.x(); + PDFReal x2 = p2.x(); + + int windingNumber = 1; + if (y2 < y1) + { + std::swap(y1, y2); + std::swap(x1, x2); + windingNumber = -1; + } + + // Do we have intercept? + if (y1 <= y && y < y2) + { + const PDFReal x = interpolate(y, y1, y2, x1, x2); + m_scanLineSamples.emplace_back(x, windingNumber); + } +} + void PDFDrawBuffer::markActiveColors(uint32_t activeColors) { m_activeColors |= activeColors; diff --git a/Pdf4QtLib/sources/pdftransparencyrenderer.h b/Pdf4QtLib/sources/pdftransparencyrenderer.h index 2440f7f..c173a06 100644 --- a/Pdf4QtLib/sources/pdftransparencyrenderer.h +++ b/Pdf4QtLib/sources/pdftransparencyrenderer.h @@ -329,21 +329,75 @@ class PDFPainterPathSampler public: /// Creates new painter path sampler, using given painter path, /// sample count (in one direction) and default shape used, when painter path is empty. + /// Fill rectangle is used to precompute winding numbers for samples. Points outside + /// of fill rectangle are considered as outside and defaultShape is returned. /// \param path Sampled path /// \param samplesCount Samples count in one direction /// \param defaultShape Default shape returned, if path is empty - /// \param precise Use precise sampling (using spline curves), or fill polygon (fast, but not too precise) - PDFPainterPathSampler(QPainterPath path, int samplesCount, PDFColorComponent defaultShape, bool precise); + /// \param fillRect Fill rectangle (sample point must be in this rectangle) + /// \param precise Use precise painter path computation + PDFPainterPathSampler(QPainterPath path, + int samplesCount, + PDFColorComponent defaultShape, + QRect fillRect, + bool precise); /// Return sample value for a given pixel PDFColorComponent sample(QPoint point) const; private: + struct ScanLineSample + { + inline constexpr ScanLineSample() = default; + inline constexpr ScanLineSample(PDFReal x, int windingNumber) : + x(x), + windingNumber(windingNumber) + { + + } + + bool operator<(const ScanLineSample& other) const { return x < other.x; } + bool operator<(PDFReal ordinate) const { return x < ordinate; } + + PDFReal x = 0.0; + int windingNumber = 0; + }; + + struct ScanLineInfo + { + size_t indexStart = 0; + size_t indexEnd = 0; + }; + + /// Compute sample by using scan lines + PDFColorComponent sampleByScanLine(QPoint point) const; + + /// Returns number of scan lines per pixel + size_t getScanLineCountPerPixel() const; + + /// Creates scan lines using fill rectangle + void prepareScanLines(); + + /// Creates scan line for given y coordinate and + /// returns info about this scan line + /// \param y Vertical coordinate of the sample line + ScanLineInfo createScanLine(qreal y); + + /// Creates scan line sample (if horizontal line of y coordinate + /// is intersecting boundary line segment p1-p2. + /// \param p1 First point of the oriented boundary line segment + /// \param p2 Second point of the oriented boundary line segment + /// \param y Vertical coordinate of the sample line + void createScanLineSample(const QPointF& p1, const QPointF& p2, qreal y); + PDFColorComponent m_defaultShape = 0.0; int m_samplesCount = 0; ///< Samples count in one direction - bool m_precise = false; QPainterPath m_path; QPolygonF m_fillPolygon; + QRect m_fillRect; + std::vector m_scanLineSamples; + std::vector m_scanLineInfo; + bool m_precise; }; /// Represents draw buffer, into which is current graphics drawn @@ -457,6 +511,8 @@ public: virtual void performBeginTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup) override; virtual void performEndTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup) override; virtual void performTextEnd(ProcessOrder order) override; + virtual void performImagePainting(const QImage& image) override; + virtual void performMeshPainting(const PDFMesh& mesh); private: