How to draw an image in Juce while preserving the edges and corners

Hi,

I am trying to draw an image while preserving the edges and corner insets in juce. There is a method exists for iOS

resizableImage(withCapInsets:)

https://developer.apple.com/documentation/uikit/uiimage/1624102-resizableimage

https://developer.apple.com/documentation/uikit/uiimage#1658362

How to achieve this in Juce ?

These are traditionally (non-Apple) called “9-patch images”, in that they consist of 9 image patches which get transformed in an aesthetically pleasing way. As far as I can tell, there is no support for 9-patch in JUCE, but you can probably do what you need with the Grid class, easily enough. Build your own 9Patch Class, extending Grid/GridItem, is what I would suggest …

1 Like

I probably wouldn’t reach for the Grid class myself I would probably write something like this…

void draw9PatchImage (juce::Graphics& g,
                      const juce::Image& image,
                      const juce::Rectangle<int>& dstRect,
                      const juce::BorderSize<int>& border)
{
    const auto srcRect = image.getBounds();
    const auto srcMiddleRect = border.subtractedFrom (srcRect);
    const auto dstMiddleRect = border.subtractedFrom (dstRect);

    // top left corner
    g.drawImage (image,
                 dstRect.getX(), dstRect.getY(), border.getLeft(), border.getTop(),
                 srcRect.getX(), srcRect.getY(), border.getLeft(), border.getTop());

    // top right corner
    g.drawImage (image,
                 dstMiddleRect.getRight(), dstRect.getY(), border.getRight(), border.getTop(),
                 srcMiddleRect.getRight(), srcRect.getY(), border.getRight(), border.getTop());

    // bottom left corner
    g.drawImage (image,
                 dstRect.getX(), dstMiddleRect.getBottom(), border.getLeft(), border.getBottom(),
                 srcRect.getX(), srcMiddleRect.getBottom(), border.getLeft(), border.getBottom());

    // bottom right corner
    g.drawImage (image,
                 dstMiddleRect.getRight(), dstMiddleRect.getBottom(), border.getRight(), border.getBottom(),
                 srcMiddleRect.getRight(), srcMiddleRect.getBottom(), border.getRight(), border.getBottom());

    // top bar
    g.drawImage (image,
                 dstMiddleRect.getX(), dstRect.getY(), dstMiddleRect.getWidth(), border.getTop(),
                 srcMiddleRect.getX(), srcRect.getY(), srcMiddleRect.getWidth(), border.getTop());

    // bottom bar
    g.drawImage (image,
                 dstMiddleRect.getX(), dstMiddleRect.getBottom(), dstMiddleRect.getWidth(), border.getBottom(),
                 srcMiddleRect.getX(), srcMiddleRect.getBottom(), srcMiddleRect.getWidth(), border.getBottom());

    // left bar
    g.drawImage (image,
                 dstRect.getX(), dstMiddleRect.getY(), border.getLeft(), dstMiddleRect.getHeight(),
                 srcRect.getX(), srcMiddleRect.getY(), border.getLeft(), srcMiddleRect.getHeight());

    // right bar
    g.drawImage (image,
                 dstMiddleRect.getRight(), dstMiddleRect.getY(), border.getRight(), dstMiddleRect.getHeight(),
                 srcMiddleRect.getRight(), srcMiddleRect.getY(), border.getRight(), srcMiddleRect.getHeight());

    // middle
    g.drawImage (image,
                 dstMiddleRect.getX(), dstMiddleRect.getY(), dstMiddleRect.getWidth(), dstMiddleRect.getHeight(),
                 srcMiddleRect.getX(), srcMiddleRect.getY(), srcMiddleRect.getWidth(), srcMiddleRect.getHeight());
}
1 Like

An alternative implementation with Grid:

#include <juce_gui_basics/juce_gui_basics.h>

class NinePatch : public juce::Component {
public:
    NinePatch(const juce::Image& texture = juce::Image())
        : _texture(texture) {

        if (!_texture.isNull()) {
            createImageComponents();
        }

        // Initialize the Grid
        grid.autoColumns = juce::Grid::TrackInfo(juce::Grid::Fr(1.0f));
        grid.autoRows = juce::Grid::TrackInfo(juce::Grid::Fr(1.0f));
        grid.columnGap = juce::Grid::Px(0);
        grid.rowGap = juce::Grid::Px(0);

        // Add components to grid
        for (auto* component : imageComponents) {
            grid.items.add(juce::GridItem(component).withArea(juce::GridItem::Span(1), juce::GridItem::Span(1)));
        }

        // Add child components to this component
        for (auto* component : imageComponents) {
            addAndMakeVisible(component);
        }
    }

    void setSize(int width, int height) override {
        Component::setSize(width, height);
        resized(); // Trigger layout update
    }

    void setStretchColumns(float col1, float col2, float col3) {
        grid.templateColumns.clear();
        grid.templateColumns.add(juce::Grid::TrackInfo(juce::Grid::Fr(col1)));
        grid.templateColumns.add(juce::Grid::TrackInfo(juce::Grid::Fr(col2)));
        grid.templateColumns.add(juce::Grid::TrackInfo(juce::Grid::Fr(col3)));
        resized(); // Trigger layout update
    }

    void setStretchRows(float row1, float row2, float row3) {
        grid.templateRows.clear();
        grid.templateRows.add(juce::Grid::TrackInfo(juce::Grid::Fr(row1)));
        grid.templateRows.add(juce::Grid::TrackInfo(juce::Grid::Fr(row2)));
        grid.templateRows.add(juce::Grid::TrackInfo(juce::Grid::Fr(row3)));
        resized(); // Trigger layout update
    }

protected:
    void resized() override {
        // Apply the grid layout
        grid.performLayout(getLocalBounds());
    }

private:
    juce::Image _texture;
    juce::OwnedArray<juce::ImageComponent> imageComponents;
    juce::Grid grid;

    void createImageComponents() {
        int tw = _texture.getWidth();
        int th = _texture.getHeight();

        // Define 3x3 grid sections
        juce::Rectangle<int> topLeft(0, 0, tw / 3, th / 3);
        juce::Rectangle<int> topCenter(tw / 3, 0, tw / 3, th / 3);
        juce::Rectangle<int> topRight(2 * tw / 3, 0, tw / 3, th / 3);

        juce::Rectangle<int> middleLeft(0, th / 3, tw / 3, th / 3);
        juce::Rectangle<int> middleCenter(tw / 3, th / 3, tw / 3, th / 3);
        juce::Rectangle<int> middleRight(2 * tw / 3, th / 3, tw / 3, th / 3);

        juce::Rectangle<int> bottomLeft(0, 2 * th / 3, tw / 3, th / 3);
        juce::Rectangle<int> bottomCenter(tw / 3, 2 * th / 3, tw / 3, th / 3);
        juce::Rectangle<int> bottomRight(2 * tw / 3, 2 * th / 3, tw / 3, th / 3);

        // Create ImageComponents for each section
        imageComponents.add(createImageComponent(topLeft));
        imageComponents.add(createImageComponent(topCenter));
        imageComponents.add(createImageComponent(topRight));

        imageComponents.add(createImageComponent(middleLeft));
        imageComponents.add(createImageComponent(middleCenter));
        imageComponents.add(createImageComponent(middleRight));

        imageComponents.add(createImageComponent(bottomLeft));
        imageComponents.add(createImageComponent(bottomCenter));
        imageComponents.add(createImageComponent(bottomRight));
    }

    juce::ImageComponent* createImageComponent(const juce::Rectangle<int>& area) {
        juce::ImageComponent* component = new juce::ImageComponent();
        component->setImage(_texture.getClippedImage(area), juce::RectanglePlacement::stretchToFit);
        return component;
    }
};

1 Like

I’m not critiquing, but I’m curious. After Anthony posted his solution, what motivated you to create yours?

I also have a use-case for 9patch, and I think using Grid is more flexible. Note, my code is also non-working code, but regardless, I don’t think anthony’s code works per the 9patch technique, and also isn’t as useful as a class … I’ll probably refine my class a bit more over the next few days.