SVGPaintServerGradientCg.cpp   [plain text]


/*
    Copyright (C) 2006, 2007, 2008 Nikolas Zimmermann <zimmermann@kde.org>

    This file is part of the KDE project

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public License
    aint with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.
*/

#include "config.h"

#if ENABLE(SVG)
#include "SVGPaintServerGradient.h"

#include "CgSupport.h"
#include "FloatConversion.h"
#include "GraphicsContext.h"
#include "ImageBuffer.h"
#include "RenderObject.h"
#include "SVGGradientElement.h"
#include "SVGPaintServerLinearGradient.h"
#include "SVGPaintServerRadialGradient.h"
#include "SVGRenderSupport.h"

#include <wtf/MathExtras.h>

using namespace std;

namespace WebCore {

static void releaseCachedStops(void* info)
{
    static_cast<SVGPaintServerGradient::SharedStopCache*>(info)->deref();
}

static void cgGradientCallback(void* info, const CGFloat* inValues, CGFloat* outColor)
{
    SVGPaintServerGradient::SharedStopCache* stopsCache = static_cast<SVGPaintServerGradient::SharedStopCache*>(info);
    
    SVGPaintServerGradient::QuartzGradientStop* stops = stopsCache->m_stops.data();
        
    int stopsCount = stopsCache->m_stops.size();

    CGFloat inValue = inValues[0];

    if (!stopsCount) {
        outColor[0] = 0;
        outColor[1] = 0;
        outColor[2] = 0;
        outColor[3] = 0;
        return;
    } else if (stopsCount == 1) {
        memcpy(outColor, stops[0].colorArray, 4 * sizeof(CGFloat));
        return;
    }

    if (!(inValue > stops[0].offset))
        memcpy(outColor, stops[0].colorArray, 4 * sizeof(CGFloat));
    else if (!(inValue < stops[stopsCount - 1].offset))
        memcpy(outColor, stops[stopsCount - 1].colorArray, 4 * sizeof(CGFloat));
    else {
        int nextStopIndex = 0;
        while ((nextStopIndex < stopsCount) && (stops[nextStopIndex].offset < inValue))
            nextStopIndex++;

        CGFloat* nextColorArray = stops[nextStopIndex].colorArray;
        CGFloat* previousColorArray = stops[nextStopIndex - 1].colorArray;
        CGFloat diffFromPrevious = inValue - stops[nextStopIndex - 1].offset;
        CGFloat percent = diffFromPrevious * stops[nextStopIndex].previousDeltaInverse;

        outColor[0] = ((1.0f - percent) * previousColorArray[0] + percent * nextColorArray[0]);
        outColor[1] = ((1.0f - percent) * previousColorArray[1] + percent * nextColorArray[1]);
        outColor[2] = ((1.0f - percent) * previousColorArray[2] + percent * nextColorArray[2]);
        outColor[3] = ((1.0f - percent) * previousColorArray[3] + percent * nextColorArray[3]);
    }
    // FIXME: have to handle the spreadMethod()s here SPREADMETHOD_REPEAT, etc.
}

static CGShadingRef CGShadingRefForLinearGradient(const SVGPaintServerLinearGradient* server)
{
    CGPoint start = CGPoint(server->gradientStart());
    CGPoint end = CGPoint(server->gradientEnd());

    CGFunctionCallbacks callbacks = {0, cgGradientCallback, releaseCachedStops};
    CGFloat domainLimits[2] = {0, 1};
    CGFloat rangeLimits[8] = {0, 1, 0, 1, 0, 1, 0, 1};
    server->m_stopsCache->ref();
    CGFunctionRef shadingFunction = CGFunctionCreate(server->m_stopsCache.get(), 1, domainLimits, 4, rangeLimits, &callbacks);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGShadingRef shading = CGShadingCreateAxial(colorSpace, start, end, shadingFunction, true, true);
    CGColorSpaceRelease(colorSpace);
    CGFunctionRelease(shadingFunction);
    return shading;
}

static CGShadingRef CGShadingRefForRadialGradient(const SVGPaintServerRadialGradient* server)
{
    CGPoint center = CGPoint(server->gradientCenter());
    CGPoint focus = CGPoint(server->gradientFocal());
    double radius = server->gradientRadius();

    double fdx = focus.x - center.x;
    double fdy = focus.y - center.y;

    // Spec: If (fx, fy) lies outside the circle defined by (cx, cy) and r, set (fx, fy)
    // to the point of intersection of the line through (fx, fy) and the circle.
    if (sqrt(fdx * fdx + fdy * fdy) > radius) { 
        double angle = atan2(focus.y * 100.0, focus.x * 100.0);
        focus.x = narrowPrecisionToCGFloat(cos(angle) * radius);
        focus.y = narrowPrecisionToCGFloat(sin(angle) * radius);
    }

    CGFunctionCallbacks callbacks = {0, cgGradientCallback, releaseCachedStops};
    CGFloat domainLimits[2] = {0, 1};
    CGFloat rangeLimits[8] = {0, 1, 0, 1, 0, 1, 0, 1};
    server->m_stopsCache->ref();
    CGFunctionRef shadingFunction = CGFunctionCreate(server->m_stopsCache.get(), 1, domainLimits, 4, rangeLimits, &callbacks);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGShadingRef shading = CGShadingCreateRadial(colorSpace, focus, 0, center, narrowPrecisionToCGFloat(radius), shadingFunction, true, true);
    CGColorSpaceRelease(colorSpace);
    CGFunctionRelease(shadingFunction);
    return shading;
}

void SVGPaintServerGradient::updateQuartzGradientStopsCache(const Vector<SVGGradientStop>& stops)
{
    m_stopsCache = new SharedStopCache;
    Vector<QuartzGradientStop>& stopsCache = m_stopsCache->m_stops;
    stopsCache.resize(stops.size());
    CGFloat previousOffset = 0.0f;
    for (unsigned i = 0; i < stops.size(); ++i) {
        CGFloat currOffset = min(max(stops[i].first, previousOffset), static_cast<CGFloat>(1.0));
        stopsCache[i].offset = currOffset;
        stopsCache[i].previousDeltaInverse = 1.0f / (currOffset - previousOffset);
        previousOffset = currOffset;
        CGFloat* ca = stopsCache[i].colorArray;
        stops[i].second.getRGBA(ca[0], ca[1], ca[2], ca[3]);
    }
}

void SVGPaintServerGradient::updateQuartzGradientCache(const SVGPaintServerGradient* server)
{
    // cache our own copy of the stops for faster access.
    // this is legacy code, probably could be reworked.
    if (!m_stopsCache)
        updateQuartzGradientStopsCache(gradientStops());

    CGShadingRelease(m_shadingCache);

    if (type() == RadialGradientPaintServer) {
        const SVGPaintServerRadialGradient* radial = static_cast<const SVGPaintServerRadialGradient*>(server);
        m_shadingCache = CGShadingRefForRadialGradient(radial);
    } else if (type() == LinearGradientPaintServer) {
        const SVGPaintServerLinearGradient* linear = static_cast<const SVGPaintServerLinearGradient*>(server);
        m_shadingCache = CGShadingRefForLinearGradient(linear);
    }
}

// Helper function for text painting
static inline const RenderObject* findTextRootObject(const RenderObject* start)
{
    while (start && !start->isSVGText())
        start = start->parent();

    ASSERT(start);
    ASSERT(start->isSVGText());

    return start;
}

void SVGPaintServerGradient::teardown(GraphicsContext*& context, const RenderObject* object, SVGPaintTargetType type, bool isPaintingText) const
{
    CGShadingRef shading = m_shadingCache;
    CGContextRef contextRef = context->platformContext();
    ASSERT(contextRef);

    // As renderPath() is not used when painting text, special logic needed here.
    if (isPaintingText) {
        if (m_savedContext) {
            FloatRect maskBBox = const_cast<RenderObject*>(findTextRootObject(object))->relativeBBox(false);

            // Fixup transformations to be able to clip to mask
            AffineTransform transform = object->absoluteTransform();
            FloatRect textBoundary = transform.mapRect(maskBBox);

            IntSize maskSize(lroundf(textBoundary.width()), lroundf(textBoundary.height()));
            clampImageBufferSizeToViewport(object->document()->renderer(), maskSize);

            if (maskSize.width() < static_cast<int>(textBoundary.width()))
                textBoundary.setWidth(maskSize.width());

            if (maskSize.height() < static_cast<int>(textBoundary.height()))
                textBoundary.setHeight(maskSize.height());

            // Clip current context to mask image (gradient)
            m_savedContext->concatCTM(transform.inverse());
            CGContextClipToMask(m_savedContext->platformContext(), CGRect(textBoundary), m_imageBuffer->cgImage());
            m_savedContext->concatCTM(transform);

            handleBoundingBoxModeAndGradientTransformation(m_savedContext, maskBBox);

            // Restore on-screen drawing context, after we got the image of the gradient
            delete m_imageBuffer;

            context = m_savedContext;
            contextRef = context->platformContext();

            m_savedContext = 0;
            m_imageBuffer = 0;
        }
    }

    CGContextDrawShading(contextRef, shading);
    context->restore();
}

void SVGPaintServerGradient::renderPath(GraphicsContext*& context, const RenderObject* path, SVGPaintTargetType type) const
{
    RenderStyle* style = path->style();
    CGContextRef contextRef = context->platformContext();
    ASSERT(contextRef);
    
    bool isFilled = (type & ApplyToFillTargetType) && style->svgStyle()->hasFill();

    // Compute destination object bounding box
    FloatRect objectBBox;
    if (boundingBoxMode()) {
        FloatRect bbox = path->relativeBBox(false);
        if (bbox.width() > 0 && bbox.height() > 0)
            objectBBox = bbox;
    }

    if (isFilled)
        clipToFillPath(contextRef, path);
    else
        clipToStrokePath(contextRef, path);

    handleBoundingBoxModeAndGradientTransformation(context, objectBBox);
}

void SVGPaintServerGradient::handleBoundingBoxModeAndGradientTransformation(GraphicsContext* context, const FloatRect& targetRect) const
{
    CGContextRef contextRef = context->platformContext();

    if (boundingBoxMode()) {
        // Choose default gradient bounding box
        CGRect gradientBBox = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);

        // Generate a transform to map between both bounding boxes
        CGAffineTransform gradientIntoObjectBBox = CGAffineTransformMakeMapBetweenRects(gradientBBox, CGRect(targetRect));
        CGContextConcatCTM(contextRef, gradientIntoObjectBBox);
    }

    // Apply the gradient's own transform
    CGAffineTransform transform = gradientTransform();
    CGContextConcatCTM(contextRef, transform);
}

bool SVGPaintServerGradient::setup(GraphicsContext*& context, const RenderObject* object, SVGPaintTargetType type, bool isPaintingText) const
{
    m_ownerElement->buildGradient();

    // We need a hook to call this when the gradient gets updated, before drawn.
    if (!m_shadingCache)
        const_cast<SVGPaintServerGradient*>(this)->updateQuartzGradientCache(this);

    CGContextRef contextRef = context->platformContext();
    ASSERT(contextRef);

    RenderStyle* style = object->style();

    bool isFilled = (type & ApplyToFillTargetType) && style->svgStyle()->hasFill();
    bool isStroked = (type & ApplyToStrokeTargetType) && style->svgStyle()->hasStroke();

    ASSERT(isFilled && !isStroked || !isFilled && isStroked);

    context->save();
    CGContextSetAlpha(contextRef, isFilled ? style->svgStyle()->fillOpacity() : style->svgStyle()->strokeOpacity());

    if (isPaintingText) {
        FloatRect maskBBox = const_cast<RenderObject*>(findTextRootObject(object))->relativeBBox(false);
        IntRect maskRect = enclosingIntRect(object->absoluteTransform().mapRect(maskBBox));

        IntSize maskSize(maskRect.width(), maskRect.height());
        clampImageBufferSizeToViewport(object->document()->renderer(), maskSize);

        auto_ptr<ImageBuffer> maskImage = ImageBuffer::create(maskSize, false);

        if (!maskImage.get()) {
            context->restore();
            return false;
        }

        GraphicsContext* maskImageContext = maskImage->context();
        maskImageContext->save();

        maskImageContext->setTextDrawingMode(isFilled ? cTextFill : cTextStroke);
        maskImageContext->translate(-maskRect.x(), -maskRect.y());
        maskImageContext->concatCTM(object->absoluteTransform());

        m_imageBuffer = maskImage.release();
        m_savedContext = context;

        context = maskImageContext;
        contextRef = context->platformContext();
    }

    if (isStroked)
        applyStrokeStyleToContext(context, style, object);

    return true;
}

void SVGPaintServerGradient::invalidate()
{
    SVGPaintServer::invalidate();

    // Invalidate caches
    CGShadingRelease(m_shadingCache);

    m_stopsCache = 0;
    m_shadingCache = 0;
}

} // namespace WebCore

#endif