AddTextToImage.mjs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /**
  2. * @author j433866 [j433866@gmail.com]
  3. * @copyright Crown Copyright 2019
  4. * @license Apache-2.0
  5. */
  6. import Operation from "../Operation";
  7. import OperationError from "../errors/OperationError";
  8. import { isImage } from "../lib/FileType";
  9. import { toBase64 } from "../lib/Base64";
  10. import jimp from "jimp";
  11. /**
  12. * Add Text To Image operation
  13. */
  14. class AddTextToImage extends Operation {
  15. /**
  16. * AddTextToImage constructor
  17. */
  18. constructor() {
  19. super();
  20. this.name = "Add Text To Image";
  21. this.module = "Image";
  22. 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.";
  23. this.infoURL = "";
  24. this.inputType = "ArrayBuffer";
  25. this.outputType = "ArrayBuffer";
  26. this.presentType = "html";
  27. this.args = [
  28. {
  29. name: "Text",
  30. type: "string",
  31. value: ""
  32. },
  33. {
  34. name: "Horizontal align",
  35. type: "option",
  36. value: ["None", "Left", "Center", "Right"]
  37. },
  38. {
  39. name: "Vertical align",
  40. type: "option",
  41. value: ["None", "Top", "Middle", "Bottom"]
  42. },
  43. {
  44. name: "X position",
  45. type: "number",
  46. value: 0
  47. },
  48. {
  49. name: "Y position",
  50. type: "number",
  51. value: 0
  52. },
  53. {
  54. name: "Size",
  55. type: "number",
  56. value: 32,
  57. min: 8
  58. },
  59. {
  60. name: "Font face",
  61. type: "option",
  62. value: [
  63. "Roboto",
  64. "Roboto Black",
  65. "Roboto Mono",
  66. "Roboto Slab"
  67. ]
  68. },
  69. {
  70. name: "Red",
  71. type: "number",
  72. value: 255,
  73. min: 0,
  74. max: 255
  75. },
  76. {
  77. name: "Green",
  78. type: "number",
  79. value: 255,
  80. min: 0,
  81. max: 255
  82. },
  83. {
  84. name: "Blue",
  85. type: "number",
  86. value: 255,
  87. min: 0,
  88. max: 255
  89. },
  90. {
  91. name: "Alpha",
  92. type: "number",
  93. value: 255,
  94. min: 0,
  95. max: 255
  96. }
  97. ];
  98. }
  99. /**
  100. * @param {ArrayBuffer} input
  101. * @param {Object[]} args
  102. * @returns {byteArray}
  103. */
  104. async run(input, args) {
  105. const text = args[0],
  106. hAlign = args[1],
  107. vAlign = args[2],
  108. size = args[5],
  109. fontFace = args[6],
  110. red = args[7],
  111. green = args[8],
  112. blue = args[9],
  113. alpha = args[10];
  114. let xPos = args[3],
  115. yPos = args[4];
  116. if (!isImage(new Uint8Array(input))) {
  117. throw new OperationError("Invalid file type.");
  118. }
  119. let image;
  120. try {
  121. image = await jimp.read(input);
  122. } catch (err) {
  123. throw new OperationError(`Error loading image. (${err})`);
  124. }
  125. try {
  126. if (ENVIRONMENT_IS_WORKER())
  127. self.sendStatusMessage("Adding text to image...");
  128. const fontsMap = {};
  129. const fonts = [
  130. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
  131. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"),
  132. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"),
  133. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt")
  134. ];
  135. await Promise.all(fonts)
  136. .then(fonts => {
  137. fontsMap.Roboto = fonts[0];
  138. fontsMap["Roboto Black"] = fonts[1];
  139. fontsMap["Roboto Mono"] = fonts[2];
  140. fontsMap["Roboto Slab"] = fonts[3];
  141. });
  142. // Make Webpack load the png font images
  143. await Promise.all([
  144. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"),
  145. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png"),
  146. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png"),
  147. import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png")
  148. ]);
  149. const font = fontsMap[fontFace];
  150. // LoadFont needs an absolute url, so append the font name to self.docURL
  151. const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default);
  152. jimpFont.pages.forEach(function(page) {
  153. if (page.bitmap) {
  154. // Adjust the RGB values of the image pages to change the font colour.
  155. const pageWidth = page.bitmap.width;
  156. const pageHeight = page.bitmap.height;
  157. for (let ix = 0; ix < pageWidth; ix++) {
  158. for (let iy = 0; iy < pageHeight; iy++) {
  159. const idx = (iy * pageWidth + ix) << 2;
  160. const newRed = page.bitmap.data[idx] - (255 - red);
  161. const newGreen = page.bitmap.data[idx + 1] - (255 - green);
  162. const newBlue = page.bitmap.data[idx + 2] - (255 - blue);
  163. const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha);
  164. // Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
  165. page.bitmap.data[idx] = (newRed > 0) ? newRed : 0;
  166. page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0;
  167. page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0;
  168. page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0;
  169. }
  170. }
  171. }
  172. });
  173. // Create a temporary image to hold the rendered text
  174. const textImage = new jimp(jimp.measureText(jimpFont, text), jimp.measureTextHeight(jimpFont, text));
  175. textImage.print(jimpFont, 0, 0, text);
  176. // Scale the rendered text image to the correct size
  177. const scaleFactor = size / 72;
  178. if (size !== 1) {
  179. // Use bicubic for decreasing size
  180. if (size > 1) {
  181. textImage.scale(scaleFactor, jimp.RESIZE_BICUBIC);
  182. } else {
  183. textImage.scale(scaleFactor, jimp.RESIZE_BILINEAR);
  184. }
  185. }
  186. // If using the alignment options, calculate the pixel values AFTER the image has been scaled
  187. switch (hAlign) {
  188. case "Left":
  189. xPos = 0;
  190. break;
  191. case "Center":
  192. xPos = (image.getWidth() / 2) - (textImage.getWidth() / 2);
  193. break;
  194. case "Right":
  195. xPos = image.getWidth() - textImage.getWidth();
  196. break;
  197. }
  198. switch (vAlign) {
  199. case "Top":
  200. yPos = 0;
  201. break;
  202. case "Middle":
  203. yPos = (image.getHeight() / 2) - (textImage.getHeight() / 2);
  204. break;
  205. case "Bottom":
  206. yPos = image.getHeight() - textImage.getHeight();
  207. break;
  208. }
  209. // Blit the rendered text image onto the original source image
  210. image.blit(textImage, xPos, yPos);
  211. let imageBuffer;
  212. if (image.getMIME() === "image/gif") {
  213. imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
  214. } else {
  215. imageBuffer = await image.getBufferAsync(jimp.AUTO);
  216. }
  217. return imageBuffer.buffer;
  218. } catch (err) {
  219. throw new OperationError(`Error adding text to image. (${err})`);
  220. }
  221. }
  222. /**
  223. * Displays the blurred image using HTML for web apps
  224. *
  225. * @param {ArrayBuffer} data
  226. * @returns {html}
  227. */
  228. present(data) {
  229. if (!data.byteLength) return "";
  230. const dataArray = new Uint8Array(data);
  231. const type = isImage(dataArray);
  232. if (!type) {
  233. throw new OperationError("Invalid file type.");
  234. }
  235. return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
  236. }
  237. }
  238. export default AddTextToImage;