Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more image operations #515

Merged
merged 22 commits into from
Jul 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@
"Cover Image",
"Image Hue/Saturation/Lightness",
"Sharpen Image",
"Convert image format"
"Convert Image Format",
"Add Text To Image"
]
},
{
Expand Down
257 changes: 257 additions & 0 deletions src/core/operations/AddTextToImage.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/

import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";

/**
* Add Text To Image operation
*/
class AddTextToImage extends Operation {

/**
* AddTextToImage constructor
*/
constructor() {
super();

this.name = "Add Text To Image";
this.module = "Image";
this.description = "Adds text onto an image.<br><br>Text can be horizontally or vertically aligned, or the position can be manually specified.<br>Variants of the Roboto font face are available in any size or colour.<br><br>Note: This may cause a degradation in image quality, especially when using font sizes larger than 72.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Text",
type: "string",
value: ""
},
{
name: "Horizontal align",
type: "option",
value: ["None", "Left", "Center", "Right"]
},
{
name: "Vertical align",
type: "option",
value: ["None", "Top", "Middle", "Bottom"]
},
{
name: "X position",
type: "number",
value: 0
},
{
name: "Y position",
type: "number",
value: 0
},
{
name: "Size",
type: "number",
value: 32,
min: 8
},
{
name: "Font face",
type: "option",
value: [
"Roboto",
"Roboto Black",
"Roboto Mono",
"Roboto Slab"
]
},
{
name: "Red",
type: "number",
value: 255,
min: 0,
max: 255
},
{
name: "Green",
type: "number",
value: 255,
min: 0,
max: 255
},
{
name: "Blue",
type: "number",
value: 255,
min: 0,
max: 255
},
{
name: "Alpha",
type: "number",
value: 255,
min: 0,
max: 255
}
];
}

/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const text = args[0],
hAlign = args[1],
vAlign = args[2],
size = args[5],
fontFace = args[6],
red = args[7],
green = args[8],
blue = args[9],
alpha = args[10];

let xPos = args[3],
yPos = args[4];

if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}

let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Adding text to image...");

const fontsMap = {
"Roboto": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use Promise.all here so we're not awaiting for each of these to load individually? Not sure whether webpack will be friends with that though...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like this be better? Webpack seems to be happy with it.

const fontsMap = {};
const fonts = [
    import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
    ...
];

await Promise.all(fonts)
    .then(fonts => {
        fontsMap.Roboto = fonts[0];
        ...
});

const fontImages = [
    import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"),
    ...
];
await Promise.all(fontImages);

"Roboto Black": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"),
"Roboto Mono": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"),
"Roboto Slab": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt")
};

// Make Webpack load the png font images
await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png");
await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png");
await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png");
await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png");

const font = fontsMap[fontFace];

// LoadFont needs an absolute url, so append the font name to self.docURL
const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default);

jimpFont.pages.forEach(function(page) {
if (page.bitmap) {
// Adjust the RGB values of the image pages to change the font colour.
const pageWidth = page.bitmap.width;
const pageHeight = page.bitmap.height;
for (let ix = 0; ix < pageWidth; ix++) {
for (let iy = 0; iy < pageHeight; iy++) {
const idx = (iy * pageWidth + ix) << 2;

const newRed = page.bitmap.data[idx] - (255 - red);
const newGreen = page.bitmap.data[idx + 1] - (255 - green);
const newBlue = page.bitmap.data[idx + 2] - (255 - blue);
const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha);

// Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
page.bitmap.data[idx] = (newRed > 0) ? newRed : 0;
page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0;
page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0;
page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0;
}
}
}
});

// Scale the image to a factor of 72, so we can print the text at any size
const scaleFactor = 72 / size;
if (size !== 72) {
// Use bicubic for decreasing size
if (size > 72) {
image.scale(scaleFactor, jimp.RESIZE_BICUBIC);
} else {
image.scale(scaleFactor, jimp.RESIZE_BILINEAR);
}
}

// If using the alignment options, calculate the pixel values AFTER the image has been scaled
switch (hAlign) {
case "Left":
xPos = 0;
break;
case "Center":
xPos = (image.getWidth() / 2) - (jimp.measureText(jimpFont, text) / 2);
break;
case "Right":
xPos = image.getWidth() - jimp.measureText(jimpFont, text);
break;
default:
// Adjust x position for the scaled image
xPos = xPos * scaleFactor;
}

switch (vAlign) {
case "Top":
yPos = 0;
break;
case "Middle":
yPos = (image.getHeight() / 2) - (jimp.measureTextHeight(jimpFont, text) / 2);
break;
case "Bottom":
yPos = image.getHeight() - jimp.measureTextHeight(jimpFont, text);
break;
default:
// Adjust y position for the scaled image
yPos = yPos * scaleFactor;
}

image.print(jimpFont, xPos, yPos, text);

if (size !== 72) {
if (size > 72) {
image.scale(1 / scaleFactor, jimp.RESIZE_BILINEAR);
} else {
image.scale(1 / scaleFactor, jimp.RESIZE_BICUBIC);
}
}

const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error adding text to image. (${err})`);
}
}

/**
* Displays the blurred image using HTML for web apps
*
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";

const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}

return `<img src="data:${type};base64,${toBase64(data)}">`;
}

}

export default AddTextToImage;
Loading