SVG gradient and shadow not rendered

Please see our roadmap update here.

In it, it says (emphasis mine)

It’s time to disentangle JUCE’s Drawable class (which wraps SVG) from JUCE Components. At the moment working with any SVG data requires that you pull in the juce_gui_basics module, which is a substantially heavier dependency than just juce_graphics , and this slows down the compilation of helper executables required to build via CMake. We will also use this opportunity to evaluate a third-party SVG parsing library to bring our SVG compliance up to date.

Others have since pointed out that the JUCE SVG functionality may actually be better than most other 3rd party libraries so when we get to that we’ll have to take a closer look.

6 Likes

Hi, just want to note that SVG gradient seems to be not working properly. You can see how it looks in the browser and how it renders on screen with a Juce (it just fills all the area with red color). Not sure if this is opacity issue or something else.

Code example:

<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="40.568 144.89 88.059 128.161" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="PSgrad_2" x1="0%" x2="46.947%" y1="88.295%" y2="0%">
      <stop offset="0" stop-color="rgb(0,0,0)" stop-opacity="0.42"/>
      <stop offset="1" stop-color="rgb(0,0,0)" stop-opacity="0"/>
    </linearGradient>
    <linearGradient id="gradient-1" x1="0%" x2="46.947%" y1="88.295%" y2="0%" gradientTransform="matrix(0.858, 0.14858, 0.310392, 0.409084, -0.274061, 0.52175)">
      <stop offset="0" stop-opacity="0.42" style="stop-color: rgb(255, 0, 0);"/>
      <stop offset="1" stop-opacity="0" style="stop-color: rgb(255, 0, 0);"/>
    </linearGradient>
  </defs>
  <path fill="url(#PSgrad_2)" d="M 105.221 188.881 L 105.221 208.166 L 63.598 208.166 L 63.598 188.881 C 63.598 186.662 65.407 184.863 67.639 184.863 L 101.18 184.863 C 103.412 184.863 105.221 186.662 105.221 188.881 Z"/>
  <path fill="url(#gradient-1)" d="M 105.223 188.918 L 105.223 208.203 L 63.6 208.203 L 63.6 188.918 C 63.6 186.699 65.409 184.9 67.641 184.9 L 101.182 184.9 C 103.414 184.9 105.223 186.699 105.223 188.918 Z" style="">
    <title>shadow</title>
  </path>
</svg>

The #PSgrad_2 works great. but not #gradient-1

Boxy SVG (app):

How it should be (Safari/Chrome):

Juce:
image

I know shadows not working as well, but if I’m wrong and there is solution in Juce8, let me know please.

Thanks.

From my tests a few years ago, SVG gradients don’t work on MacOS. I don’t know what’s been updated since, so I can’t help any further, sorry.

The issue was gradientTransform="matrix(0.858, 0.14858, 0.310392, 0.409084, -0.274061, 0.52175)"

Once I removed it manually and started using only <linearGradient x1="0%" x2="46.947%" y1="88.295%" y2="0%"> it seems to be working. But it works very strangely, sometimes the gradient depends on the size of the view port. So I have to imitate darkness by duplicating path and superimposed on each other.

It also seems that if you draw it in Adobe Illustrator and do Export As → Svg, it works. And it doesn’t work if you edit svg directly (Open File: my1.svg ->Save). That’s completely adobe issue, but just wanted to share my tricks.

Another issue that I see. Sometimes if your layer is “LOCKED” or has rotation. Just remove it, because you will see random hell :slight_smile:

Not sure if it would have solved this particular case, but I would always always recommend making sure you optimise your SVGs before trying to rendering them with JUCE.

E.g. always export text as curves, flattern transforms, use (rgba) hex colours, and whatever other export options your editor has that may remove any properties that JUCE doesn’t support.

You can then pass it through https://svgomg.net/ to simplify and minify it even further.

SVG is a huge specification, and not everything that’s supported by every editor/browser is standard, so it’s always best to use these tools to make sure you’re only using standard features where possible. At the very least I would always expect JUCE to support the SVG Tiny Specification reguardless of what dependencies they might use.

3 Likes

Is there any alternatives that people use to draw SVG with shadows and filters? I tried librsvg and it works great. I was so happy to see how the UI looks. But then I realized that this doesn’t work for iOS which is my main platform. And I lost two days with attemps to get it working without any luck.

I feel like I’m stuck. I’ve already redone 75 controls and it’s so stupid that I can’t run it on iOS with shadows now.

1 Like

(ONLY iOS/Mac OS X)

I found seems to be a beautiful way on how to get Image from SVG.

The main idea is to use Apple’s PDFKit framework and .AI (Adobe Illustrator) or .Pdf format.

1 Rule: If you are using .AI (Adobe Illustrator format) then during “Save As” Make sure you are checked “Create PDF Compatible File”.

2 Rule If you are using PDF, make sure to save your PDF as vector, i.e. not raster. For this during export select “Adobe PDF Preset”: Illustrator Default.

Into your C++ project add:

PDFHandler.h

#ifndef PDFHANDLER_H
#define PDFHANDLER_H

#include <juce_graphics/juce_graphics.h>

juce::Image pdfToJuceImage(const char* data, int dataSize, int width, int height);

#endif // PDFHANDLER_H

PDFHandler.mm

#import "PDFHandler.h"

#ifdef __OBJC__
#import <Foundation/Foundation.h>
#import <PDFKit/PDFKit.h>
#if TARGET_OS_IPHONE
    #import <UIKit/UIKit.h>
#elif TARGET_OS_MAC
    #import <AppKit/AppKit.h>
#endif
#endif

juce::Image pdfToJuceImage(const char* data, int dataSize, int width, int height)
{
    @autoreleasepool
    {
        // Create NSData from the provided data
        NSData* pdfData = [NSData dataWithBytes:data length:(NSUInteger)dataSize];

        // Create PDFDocument from NSData
        PDFDocument* pdfDocument = [[PDFDocument alloc] initWithData: pdfData];

        if (pdfDocument == nil || [pdfDocument pageCount] == 0) {
            return juce::Image();
        }

        // Always use the first page
        PDFPage* page = [pdfDocument pageAtIndex: 0];

        if (page == nil) {
            return juce::Image();
        }

        // Get the bounds of the PDF page
        CGRect pageRect = [page boundsForBox: kPDFDisplayBoxMediaBox];

#if TARGET_OS_IPHONE
        // iOS: Use UIGraphicsBeginImageContextWithOptions
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 0.0);
        CGContextRef context = UIGraphicsGetCurrentContext();

        // Set transparent background
        CGContextClearRect(context, CGRectMake(0, 0, width, height));

        // Scale the context to fit the bounds
        CGContextTranslateCTM(context, 0.0, height);
        CGContextScaleCTM(context, width / pageRect.size.width, -height / pageRect.size.height);

        // Set Quality
        CGContextSetAllowsAntialiasing(context, true);
        CGContextSetShouldAntialias(context, true);
        CGContextSetInterpolationQuality(context, kCGInterpolationHigh);

        // Draw the PDF page onto the context
        [page drawWithBox: kPDFDisplayBoxMediaBox toContext: context];

        // Get UIImage from the current image context
        UIImage* uiImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        if (uiImage == nil) {
            return juce::Image();
        }

        // Convert UIImage into juce::Image
        NSData* imageData = UIImagePNGRepresentation(uiImage);
        juce::MemoryInputStream stream(imageData.bytes, imageData.length, false);
        return juce::ImageFileFormat::loadFrom(stream);

#elif TARGET_OS_MAC
    // macOS: Use NSImage and NSGraphicsContext

    NSRect rect = NSMakeRect(0, 0, width, height);
    NSImage* nsImage = [[NSImage alloc] initWithSize:rect.size];

    [nsImage lockFocus];
    CGContextRef context = [NSGraphicsContext currentContext].CGContext;

    // Set transparent background
    CGContextClearRect(context, CGRectMake(0, 0, width, height));

    // Scale the context to fit the bounds
    CGContextScaleCTM(context, width / pageRect.size.width, height / pageRect.size.height);

    // Set Quality
    CGContextSetAllowsAntialiasing(context, true);
    CGContextSetShouldAntialias(context, true);
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);

    // Draw the PDF page onto the context
    [page drawWithBox: kPDFDisplayBoxMediaBox toContext: context];

    [nsImage unlockFocus];

    if (nsImage == nil) {
        return juce::Image();
    }

    // Convert NSImage to NSData
    CGImageRef cgImage = [nsImage CGImageForProposedRect:NULL context:nil hints:nil];
    NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
    NSData* imageData = [bitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];

    // Convert NSData into juce::Image
    juce::MemoryInputStream stream([imageData bytes], [imageData length], false);
    return juce::ImageFileFormat::loadFrom(stream);
#endif


    }
}

Projucer
Make sure you added PDFKit.framework (see Extra System Frameworks )

Cmake (optional)

# Locate the PDFKit framework
find_library(PDFKIT_FRAMEWORK PDFKit)
...
target_link_libraries(YourSuperPooperApp
        PRIVATE
        GuiAppData 
        juce::juce_gui_extra
        juce::juce_dsp
        juce::juce_animation
        juce::juce_audio_utils
        ${PDFKIT_FRAMEWORK} #### <------ ADD THIS
        PUBLIC
        juce::juce_recommended_config_flags
        juce::juce_recommended_lto_flags
        juce::juce_recommended_warning_flags)

Usage:
In your MainComponent.h for example add

#include "PDFHandler.h"

MainComponent() {
            yourJuceImage = pdfToJuceImage(BinaryData::default_fader_cap_ai, BinaryData::default_fader_cap_aiSize, width, height);
}

[NOTE]: Keep in mind that image will be returned 2X larger if you are using retina display. So if you don’t want that, set scale to UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0);

As you can see I’m using BinaryData to get the ai file from resources. The code is not optimized, but you get this beautiful and clean juce::Image, which support Everething! Shadows, Filters, Gradients and more, more :slight_smile:

I’ll be glad if someone helps optimize it, I haven’t had time for it yet, but I’ll share it with you. Thanks!

1 Like

I wonder if muPDF would provide similar functionality in terms of rendering PDF images cross-platform?

@adamwilson
I feel like with muPDF you have to configure/rebuild for iOS following dependencies as well: freetype, jbig2dec, jpeg, openjpeg, libpng, zlib, harfbuzz, openssl (optinal).

I honestly tried this for librsvg (they have a similar dependencies, but a lot more) but got tired already on the 3rd day. Maybe if only no plans for iOS, then it can be installed with brew install.

But I haven’t checked if their renderer draws everything well. Even with cairo and librsvg In most cases, layers are positioned incorrectly.

1 Like

There is a Huge memory leak in the code I’ve posted before and it can easily grow up to 2-3gb. For some reason @autoreleasepool{} is not releasing PDFPage* page (except if you close your app). So I it can be updated with a manual pool.

PDFHandler.h

#ifndef PDFHANDLER_H
#define PDFHANDLER_H

#include <juce_graphics/juce_graphics.h>

juce::Image pdfToJuceImage(const char* data, int dataSize, int width, int height);

#endif // PDFHANDLER_H

PDFHandler.mm (<- it should be exactly .mm not a .h, not a .hpp)

#import "PDFHandler.h"

#ifdef __OBJC__
#import <Foundation/Foundation.h>
#import <PDFKit/PDFKit.h>
#if TARGET_OS_IPHONE
    #import <UIKit/UIKit.h>
#elif TARGET_OS_MAC
    #import <AppKit/AppKit.h>
#endif
#endif

juce::Image pdfToJuceImage(const char* data, int dataSize, int width, int height)
{
    // ✅ Use NSAutoreleasePool to force memory cleanup after function execution
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // ✅ Create NSData from raw PDF data
    NSData* pdfData = [[NSData alloc] initWithBytes:data length:(NSUInteger)dataSize];
    if (!pdfData) {
        [pool release];
        return juce::Image();
    }

    // ✅ Create PDFDocument from NSData
    PDFDocument* pdfDocument = [[PDFDocument alloc] initWithData:pdfData];
    if (!pdfDocument || [pdfDocument pageCount] == 0) {
        [pdfData release];
        [pool release];
        return juce::Image();
    }

    // ✅ Get the first page of the PDF document
    PDFPage* page = [pdfDocument pageAtIndex:0];
    if (!page) {
        [pdfDocument release];
        [pdfData release];
        [pool release];
        return juce::Image();
    }

    // ✅ Get page bounds
    CGRect pageRect = [page boundsForBox:kPDFDisplayBoxMediaBox];

    // **iOS Rendering**
#if TARGET_OS_IPHONE
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextClearRect(context, CGRectMake(0, 0, width, height));
    CGContextTranslateCTM(context, 0.0, height);
    CGContextScaleCTM(context, width / pageRect.size.width, -height / pageRect.size.height);

    CGContextSetAllowsAntialiasing(context, true);
    CGContextSetShouldAntialias(context, true);
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);

    // ✅ Render the PDF page to context
    [page drawWithBox:kPDFDisplayBoxMediaBox toContext:context];

    UIImage* uiImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    if (!uiImage) {
        [pdfDocument release];
        [pdfData release];
        [pool release];
        return juce::Image();
    }

    // ✅ Convert UIImage to JUCE image
    NSData* imageData = UIImagePNGRepresentation(uiImage);
    juce::MemoryInputStream stream(imageData.bytes, imageData.length, true);
    juce::Image resultImage = juce::ImageFileFormat::loadFrom(stream);

    // **macOS Rendering**
#elif TARGET_OS_MAC
    NSRect rect = NSMakeRect(0, 0, width, height);
    NSImage* nsImage = [[[NSImage alloc] initWithSize:rect.size] autorelease];

    [nsImage lockFocus];
    CGContextRef context = [NSGraphicsContext currentContext].CGContext;

    CGContextClearRect(context, CGRectMake(0, 0, width, height));
    CGContextScaleCTM(context, width / pageRect.size.width, height / pageRect.size.height);

    CGContextSetAllowsAntialiasing(context, true);
    CGContextSetShouldAntialias(context, true);
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);

    // ✅ Render the PDF page to context
    [page drawWithBox:kPDFDisplayBoxMediaBox toContext:context];

    [nsImage unlockFocus];

    if (!nsImage) {
        [pdfDocument release];
        [pdfData release];
        [pool release];
        return juce::Image();
    }

    // ✅ Convert NSImage to JUCE image
    CGImageRef cgImage = [nsImage CGImageForProposedRect:NULL context:nil hints:nil];
    if (!cgImage) {
        [pdfDocument release];
        [pdfData release];
        [pool release];
        return juce::Image();
    }

    NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
    NSData* imageData = [bitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
    CGImageRelease(cgImage);

    juce::MemoryInputStream stream([imageData bytes], [imageData length], true);
    juce::Image resultImage = juce::ImageFileFormat::loadFrom(stream);
#endif

    // ✅ Manually release PDF objects before function exit
    [pdfDocument release];
    [pdfData release];

    // ✅ Release NSAutoreleasePool to ensure memory cleanup
    [pool release];

    return resultImage;
}

Usage (let’s say in your MainComponent.h):

#include "PDFHandler.h"

const Image image = pdfToJuceImage(
                BinaryData::knob_bg,
                BinaryData::knob_bg_Size,
                width,
                height
            );
1 Like