Add border and colour to rounded corners bitmap?

I have this:

	public static Bitmap cornerBitmap(Bitmap source, float round) {
		int width = source.getWidth();
		int height = source.getHeight();
		Bitmap bmOut = Bitmap.createBitmap(width, height, source.getConfig());
		android.graphics.Canvas canvas = new Canvas(bmOut);
		canvas.drawARGB(0, 0, 0, 0);
		Paint paint = new Paint();
		paint.setAntiAlias(true);
		paint.setColor(-16777216);
		Rect rect = new Rect(0, 0, width, height);
		RectF rectF = new RectF(rect);
		canvas.drawRoundRect(rectF, round, round, paint);
		paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
		canvas.drawBitmap(source, rect, rect, paint);
		return bmOut;
	}

which works just fine, and returns the provided bitmap as a new bitmap with rounded corners.

I would like to be able to add a border with provided width and colour, and from other examples I have seen I believe I need to add another "Paint" to draw these, then draw the rounded bitmap "on top", but I cannot find the correct syntax or order of things to make it happen, I either just get the rounded bitmap, a square bitmap, or nothing!

	public static Bitmap cornerBitmap(Bitmap source, float round,int borderWidth, int borderColour) {
		int width = source.getWidth();
		int height = source.getHeight();
		Bitmap bmOut = Bitmap.createBitmap(width, height, source.getConfig());
		android.graphics.Canvas canvas = new Canvas(bmOut);
		canvas.drawARGB(0, 0, 0, 0);
		Paint paint = new Paint();
		paint.setAntiAlias(true);
		paint.setColor(-16777216);
		Rect rect = new Rect(0, 0, width, height);
		RectF rectF = new RectF(rect);
		canvas.drawRoundRect(rectF, round, round, paint);
		paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

// ## what I believe is needed, or something like it...##

//      if (borderWidth > 0) {
//		Paint pnt = new Paint;
//		pnt.setAntiAlias(true);
//		pnt.setStyle(Paint.Style.STROKE);
//		pnt.setColor(borderColour);
//		pnt.setStrokeWidth((float)borderWidth);
//		canvas.drawBitmap(source, rect, rect, pnt); 
                 }

		canvas.drawBitmap(source, rect, rect, paint);

		return bmOut;
	}

Can anyone see where I am going wrong, and what is needed to make this work ?

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;




public static Bitmap cornerBitmap(
        Bitmap source,
        float round,
        int borderWidth,
        int borderColour) {

    int width = source.getWidth();
    int height = source.getHeight();

    Bitmap bmOut = Bitmap.createBitmap(
            width,
            height,
            Bitmap.Config.ARGB_8888);

    Canvas canvas = new Canvas(bmOut);

    // Transparent background
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

    float halfBorder = borderWidth / 2.0f;

    // Keep the complete border inside the bitmap bounds
    float left = halfBorder;
    float top = halfBorder;
    float right = width - halfBorder;
    float bottom = height - halfBorder;

    float radius = Math.max(0, round - halfBorder);

    Rect srcRect = new Rect(0, 0, width, height);

    RectF dstRect = new RectF(
            left,
            top,
            right,
            bottom);

    // -----------------------------
    // 1. Clip bitmap to rounded shape
    // -----------------------------

    Path path = new Path();

    path.addRoundRect(
            dstRect,
            radius,
            radius,
            Path.Direction.CW);

    canvas.save();

    canvas.clipPath(path);

    Paint bitmapPaint = new Paint(
            Paint.ANTI_ALIAS_FLAG |
            Paint.FILTER_BITMAP_FLAG);

    canvas.drawBitmap(
            source,
            srcRect,
            dstRect,
            bitmapPaint);

    canvas.restore();

    // -----------------------------
    // 2. Draw rounded border
    // -----------------------------

    if (borderWidth > 0) {

        Paint borderPaint = new Paint(
                Paint.ANTI_ALIAS_FLAG);

        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(borderWidth);
        borderPaint.setColor(borderColour);

        canvas.drawRoundRect(
                dstRect,
                radius,
                radius,
                borderPaint);
    }

    return bmOut;
}
2 Likes

Thanks, @eAcademy, I will give it a go !

2 Likes

Works just fine! Many thanks! Will give credits of course.

1 Like

You are always welcome :folded_hands:

@eAcademy

Your function works well to round corners and add a border, but seems to resize the image by quite a bit - @ 60%. For example: take a 100x100 pixel image, do the rounded corners(8) and border(4), and I get back a 38x38 pixel image. Same for a 200x200, returns 76x76. Doesn't seem to make any difference where the image comes from, local storage, url etc.

Before I start fiddling with it, could you take a look ?

That's exactly a density scaling issue. The new Bitmap is created with Bitmap.createBitmap(...), but we aren't preserving the density of the original bitmap.

Android uses bitmap density when drawing bitmaps onto a density-aware Canvas/View, so the returned bitmap can appear smaller even though its actual pixel are still 100Γ—100 or 200Γ—200.

We can fix this simply by copying the source density to the output bitmap immediately after creating it:

Bitmap bmOut = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888);

bmOut.setDensity(source.getDensity());

1 Like

I'll try it, see what happens :smiley:

1 Like

No change :frowning:

public static Bitmap newCornerBitmap(Bitmap source,float round,int borderWidth,int borderColour) {

		int width = source.getWidth();
		int height = source.getHeight();
		Bitmap bmOut = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
>>>		bmOut.setDensity(source.getDensity());
		Canvas canvas = new Canvas(bmOut);
		// Transparent background
		canvas.drawColor(Color.TRANS

Hmm...It happens always with us :frowning: please try one diagnostic before changing the drawing code.

Immediately before return bmOut;,
add:
Log.d("CornerBitmap",
"source = " + source.getWidth() + "x" + source.getHeight()
+ " density=" + source.getDensity()
+ " | output = " + bmOut.getWidth() + "x" + bmOut.getHeight()
+ " density=" + bmOut.getDensity());

If that reports output = 100x100, then we know conclusively that the resize is happening after this function returns.

If you don't want to use Logcat, another quick test is to temporarily return:

return Bitmap.createBitmap(source);

with no rounding/border code at all. If that also appears as ~38% size, then the issue is definitely outside the rounded-corner/border drawing code.

Once we know that, I can adjust the function specifically for the way App Inventor is handling the returned Bitmap rather than guessing at the density.

OR simply give it a try-

public static Bitmap cornerBitmap(Bitmap source, float round, int borderWidth, int borderColour) {
    int width = source.getWidth();
    int height = source.getHeight();
    Bitmap bmOut = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bmOut);
    
    // Transparent background
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

    Rect srcRect = new Rect(0, 0, width, height);
    RectF fullRect = new RectF(0, 0, width, height);

    // 1. Clip and draw the source bitmap at its original full size
    Path clipPath = new Path();
    clipPath.addRoundRect(fullRect, round, round, Path.Direction.CW);
    
    canvas.save();
    canvas.clipPath(clipPath);
    Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    canvas.drawBitmap(source, srcRect, fullRect, bitmapPaint);
    canvas.restore();

    // 2. Draw the border precisely inside the outer edges without scaling the image
    if (borderWidth > 0) {
        float halfBorder = borderWidth / 2.0f;
        RectF borderRect = new RectF(
            halfBorder, 
            halfBorder, 
            width - halfBorder, 
            height - halfBorder
        );
        float borderRectRadius = Math.max(0, round - halfBorder);

        Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(borderWidth);
        borderPaint.setColor(borderColour);
        
        canvas.drawRoundRect(borderRect, borderRectRadius, borderRectRadius, borderPaint);
    }

    return bmOut;
}

@TIMAI2 If you're getting the 38Γ—38 value from an App Inventor Image/Canvas component's Width/Height, that may be the dp value rather than the bitmap's actual pixel dimensions.

The easiest way to confirm would be to log the dimensions immediately before returning:

Log.d("CornerBitmap",
"SOURCE: " + source.getWidth() + "x" + source.getHeight() +
" OUTPUT: " + bmOut.getWidth() + "x" + bmOut.getHeight());

If that says 100x100 β†’ 100x100 (and similarly 200x200 β†’ 200x200), then the bitmap itself is not being reduced β€” we're just seeing a px/dp conversion somewhere after the function returns.

The screen density on my device is 2.625, so 38*2.625 = 99.75 (close enough to prove a point...)

I have a function to convert dp to pixels which i use elsewhere:

// function to convert dp to pixels
	public int setPixels(int element) {
		return Math.round((float)element * this.form.deviceDensity());
	}

Perhaps this can be used in some of the sizings in the roundCornersBitmap function ?

Got it working, probably not pretty or the correct way, but seems to be working

	public Bitmap newCornerBitmap(Bitmap source, float round, int borderWidth, int borderColour) {

		//get the device Density
		float density = this.form.deviceDensity();
		int iDensity = (int) density;
        //set the new bitmap's width,height and round to pixels from dp (<>)
		int width = setPixels(source.getWidth());
		int height = setPixels(source.getHeight());
		float newRound = round*density;
		//resize the source bitmap using device density
		source = Bitmap.createScaledBitmap(source, width*iDensity/2, height*iDensity/2, false);
		Bitmap bmOut = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
1 Like

I slightly modified the method you marked as the solution.

public Bitmap cornerBitmap(
        Bitmap source,
        float round,
        int borderWidth,
        int borderColour) {

    if (source == null) return null;

    float density = this.form.deviceDensity();

    int width = source.getWidth();
    int height = source.getHeight();

    Bitmap bmOut = Bitmap.createBitmap(
            width,
            height,
            Bitmap.Config.ARGB_8888);

    bmOut.setDensity(source.getDensity());

    Canvas canvas = new Canvas(bmOut);

    // Transparent background
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

    float borderWidthPx = borderWidth * density;
    float roundPx = round * density;

    float halfBorder = borderWidthPx / 2.0f;

    // Keep the complete border inside the bitmap bounds
    float left = halfBorder;
    float top = halfBorder;
    float right = width - halfBorder;
    float bottom = height - halfBorder;

    float radius = Math.max(0, roundPx - halfBorder);

    Rect srcRect = new Rect(0, 0, width, height);

    RectF dstRect = new RectF(
            left,
            top,
            right,
            bottom);

    // -----------------------------
    // 1. Clip bitmap to rounded shape
    // -----------------------------

    Path path = new Path();

    path.addRoundRect(
            dstRect,
            radius,
            radius,
            Path.Direction.CW);

    canvas.save();

    canvas.clipPath(path);

    Paint bitmapPaint = new Paint(
            Paint.ANTI_ALIAS_FLAG |
            Paint.FILTER_BITMAP_FLAG);

    canvas.drawBitmap(
            source,
            srcRect,
            dstRect,
            bitmapPaint);

    canvas.restore();

    // -----------------------------
    // 2. Draw rounded border
    // -----------------------------

    if (borderWidthPx > 0) {

        Paint borderPaint = new Paint(
                Paint.ANTI_ALIAS_FLAG);

        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(borderWidthPx);
        borderPaint.setColor(borderColour);

        canvas.drawRoundRect(
                dstRect,
                radius,
                radius,
                borderPaint);
    }

    return bmOut;
}

I'm afraid your fix only works correctly on devices with density = 2.0.

The density on my device (Google Pixel 8a) is 2.625...

Tested on another device (Google Pixel 4a) with Density of 2.75, no issues.

Using bmOut.setDensity(source.getDensity()); made no difference in my tests.

Try it on devices with a density other than 2.625. Your fix will only work for that density.

As above, I did.

Both bitmaps are being sized according to the device density. The /2 handles the width and height discrepancy.

Try it on a larger screen with a density of at least 3.0. This truncates the decimal values ​​from the density. Whether you have 2.625 or 2.75, your iDensity is 2. Then the rest of the math works... But if the density is, for example, 3.0, then your iDensity will be 3, and the image will be larger...

I don't have a suitable device other than the two already mentioned.

If we go with what you are saying then there appears to be no point in doing this:

//resize the source bitmap using device density
source = Bitmap.createScaledBitmap(source, width*iDensity/2, height*iDensity/2, false);

because width*iDensity/2 == width

but if I don't scale the bitmap, then it fills only the top quarter of the other bitmap.

Happy to do some more testing :slight_smile:

My initial testing was to have the image component set to automatic for height and width, then load the image with the image.picture block and compare it with the size of the extension provided image, loaded into the same image component.