I did this a few months ago using a java action and ImageIO.
Java action parameters:
ImageBackground: System.Image, i.e. your QR image
ImageOnTop: System.Image, smaller logo image
ImageResult: System.Image in which resulting QR with logo is stored
ImageTopMaxSize: Integer for max width/height of your logo
BorderWidth: Integer width in px of surrounding white border
BorderSquare: Boolean, when false, ImageOnTop is supposed to be a circle or oval with transparency around
BorderBlack: Boolean to set border black instead of white
WhiteBgToTransparent: Boolean, whether or not to convert white bg to transparency when BorderSquare = false
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import javax.imageio.ImageIO;
// BEGIN USER CODE
// read input file contents
InputStream ImageBgStream = Core.getImage(this.context(), this.ImageBackground.getMendixObject(), false);
InputStream ImageTopStream = Core.getImage(this.context(), this.ImageOnTop.getMendixObject(), false);
BufferedImage source = ImageIO.read(ImageBgStream);
BufferedImage logo = ImageIO.read(ImageTopStream);
// turn white background (edges only) transparent
if (!this.BorderSquare && this.WhiteBgToTransparent) {
// log.debug("executing makeBackgroundTransparent");
BufferedImage target = new BufferedImage(logo.getWidth(), logo.getHeight(), BufferedImage.TYPE_INT_ARGB);
makeBackgroundTransparent(logo, target);
logo = target;
}
int borderWidth = this.BorderWidth.intValue();
int logoSize = this.ImageTopMaxSize.intValue();
// calculate logo size with border substracted and scale logo
int innerLogoSize = logoSize - borderWidth - borderWidth;
// check which of height/width is greater and scale that to size, keeping aspect ratio by setting the other negative
boolean WidthOverHeight = (logo.getWidth() >= logo.getHeight());
int innerLogoWidth = (WidthOverHeight) ? innerLogoSize : -1;
int innerLogoHeight = (WidthOverHeight) ? -1 : innerLogoSize;
Image scaledLogo = logo.getScaledInstance(innerLogoWidth, innerLogoHeight, Image.SCALE_SMOOTH);
int sourceHeight = source.getHeight();
int sourceWidth = source.getWidth();
int scaledLogoWidth = WidthOverHeight ? innerLogoSize : (logo.getWidth() * innerLogoSize / logo.getHeight());
int scaledLogoHeight = WidthOverHeight ? (logo.getHeight() * innerLogoSize / logo.getWidth()) : innerLogoSize;
int logoHeight = scaledLogoHeight + (borderWidth * 2);
int logoWidth = scaledLogoWidth + (borderWidth * 2);
// calculate x and y coordinates for border
int xParam = (int) Math.ceil((sourceWidth - logoWidth) / 2);
int yParam = (int) Math.ceil((sourceHeight - logoHeight) / 2);
// now calculate x and y coordinates for inner logo
int xParamInner = xParam + borderWidth;
int yParamInner = yParam + borderWidth;
// create new image with ARGB (in case source QR is binary black and white)
// and draw source + (white) border + logo
BufferedImage result = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_INT_ARGB);
Graphics g = result.createGraphics();
g.drawImage(source, 0, 0, null);
Color borderColor = this.BorderBlack ? Color.BLACK : Color.WHITE;
g.setColor(borderColor);
if (this.BorderSquare) {
g.fillRect(xParam, yParam, logoWidth, logoHeight);
} else {
g.fillOval(xParam, yParam, logoWidth, logoHeight);
}
g.drawImage(scaledLogo, xParamInner, yParamInner, null);
// write source to new outStream
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
ImageIO.write(result, "png", outStream);
// write outStream to result file with thumbnail 100x100
InputStream is = new ByteArrayInputStream(outStream.toByteArray());
Core.storeImageDocumentContent(this.context(), this.ImageResult.getMendixObject(), is, 200, 200);
// tidy up
g.dispose();
ImageBgStream.close();
ImageTopStream.close();
outStream.close();
is.close();
return null;
// END USER CODE
// BEGIN EXTRA CODE
private static final ILogNode log = Core.getLogger("TestQR");
private static final int tolerance = 1000000; // value chosen through several attempts
private static final int ColorWhite = Color.WHITE.getRGB();
// https://stackoverflow.com/questions/3101446/how-to-convert-an-image-into-a-transparent-image-in-java
/**
* Given an image with no transparency, it makes the white background
* transparent, provided that the entire image outline has a different color
* from the background; the internal pixels of the image, even if they have
* the same color as the background, are not changed.
*
* @param source image with a white background; the image must have an
* outline of a different color from background.
* @param target new image with a transparent background
*/
private static java.lang.Void makeBackgroundTransparent(BufferedImage source, BufferedImage target) {
/*
* Algorithm
*
* Pixels must be iterated in the four possible directions: (1) left to
* right, for each row (top to bottom); (2) from right to left, for each
* row (from top to bottom); (3) from top to bottom, for each column
* (from left to right); (4) from bottom to top, for each column (from
* left to right).
*
* In each iteration, each white pixel is replaced with a transparent
* one. Each iteration ends when a pixel of color other than white (or
* a transparent pixel) is encountered.
*/
if (source == null) {
throw new IllegalArgumentException("ImageUtilities.makeBackgroundTransparent -> null source image");
}
int width = source.getWidth();
int height = source.getHeight();
int[] pixels = source.getRGB(0, 0, width, height, null, 0, width); // array instance containing the ARGB data within this image
// check if the first pixel is transparent
if ((pixels[0] >> 24) == 0x00) {
return null; // nothing to do, the image already has a transparent background
}
// 1. Left to right, for each row (top to bottom)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (!pixelConverted(pixels, y * width + x)) {
break;
}
}
}
// 2. Right to left, for each row (top to bottom)
for (int y = 0; y < height; y++) {
for (int x = width - 1; x >= 0; x--) {
if (!pixelConverted(pixels, y * width + x)) {
break;
}
}
}
// 3. Top to bottom, for each column (from left to right)
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (!pixelConverted(pixels, y * width + x)) {
break;
}
}
}
// 4. Bottom to top, for each column (from left to right)
for (int x = 0; x < width; x++) {
for (int y = height - 1; y >= 0; y--) {
if (!pixelConverted(pixels, y * width + x)) {
break;
}
}
}
target.setRGB(0, 0, width, height, pixels, 0, width);
return null;
}
private static boolean pixelConverted(int[] pixels, int index) {
int color = pixels[index];
if ((color >> 24) != 0x00 && color >= ColorWhite - tolerance && color <= ColorWhite + tolerance) { // means white with tolerance and no transparency
pixels[index] = 0x00; // means full transparency
return true;
} else {
return false;
}
}
// END EXTRA CODE