AddTextToImage.mjs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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.<br><br>Note: This may cause a degradation in image quality, especially when using font sizes larger than 72.";
  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. const fontImages = [
  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. await Promise.all(fontImages);
  150. const font = fontsMap[fontFace];
  151. // LoadFont needs an absolute url, so append the font name to self.docURL
  152. const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default);
  153. jimpFont.pages.forEach(function(page) {
  154. if (page.bitmap) {
  155. // Adjust the RGB values of the image pages to change the font colour.
  156. const pageWidth = page.bitmap.width;
  157. const pageHeight = page.bitmap.height;
  158. for (let ix = 0; ix < pageWidth; ix++) {
  159. for (let iy = 0; iy < pageHeight; iy++) {
  160. const idx = (iy * pageWidth + ix) << 2;
  161. const newRed = page.bitmap.data[idx] - (255 - red);
  162. const newGreen = page.bitmap.data[idx + 1] - (255 - green);
  163. const newBlue = page.bitmap.data[idx + 2] - (255 - blue);
  164. const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha);
  165. // Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
  166. page.bitmap.data[idx] = (newRed > 0) ? newRed : 0;
  167. page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0;
  168. page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0;
  169. page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0;
  170. }
  171. }
  172. }
  173. });
  174. // Scale the image to a factor of 72, so we can print the text at any size
  175. const scaleFactor = 72 / size;
  176. if (size !== 72) {
  177. // Use bicubic for decreasing size
  178. if (size > 72) {
  179. image.scale(scaleFactor, jimp.RESIZE_BICUBIC);
  180. } else {
  181. image.scale(scaleFactor, jimp.RESIZE_BILINEAR);
  182. }
  183. }
  184. // If using the alignment options, calculate the pixel values AFTER the image has been scaled
  185. switch (hAlign) {
  186. case "Left":
  187. xPos = 0;
  188. break;
  189. case "Center":
  190. xPos = (image.getWidth() / 2) - (jimp.measureText(jimpFont, text) / 2);
  191. break;
  192. case "Right":
  193. xPos = image.getWidth() - jimp.measureText(jimpFont, text);
  194. break;
  195. default:
  196. // Adjust x position for the scaled image
  197. xPos = xPos * scaleFactor;
  198. }
  199. switch (vAlign) {
  200. case "Top":
  201. yPos = 0;
  202. break;
  203. case "Middle":
  204. yPos = (image.getHeight() / 2) - (jimp.measureTextHeight(jimpFont, text) / 2);
  205. break;
  206. case "Bottom":
  207. yPos = image.getHeight() - jimp.measureTextHeight(jimpFont, text);
  208. break;
  209. default:
  210. // Adjust y position for the scaled image
  211. yPos = yPos * scaleFactor;
  212. }
  213. image.print(jimpFont, xPos, yPos, text);
  214. if (size !== 72) {
  215. if (size > 72) {
  216. image.scale(1 / scaleFactor, jimp.RESIZE_BILINEAR);
  217. } else {
  218. image.scale(1 / scaleFactor, jimp.RESIZE_BICUBIC);
  219. }
  220. }
  221. let imageBuffer;
  222. if (image.getMIME() === "image/gif") {
  223. imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
  224. } else {
  225. imageBuffer = await image.getBufferAsync(jimp.AUTO);
  226. }
  227. return imageBuffer.buffer;
  228. } catch (err) {
  229. throw new OperationError(`Error adding text to image. (${err})`);
  230. }
  231. }
  232. /**
  233. * Displays the blurred image using HTML for web apps
  234. *
  235. * @param {ArrayBuffer} data
  236. * @returns {html}
  237. */
  238. present(data) {
  239. if (!data.byteLength) return "";
  240. const dataArray = new Uint8Array(data);
  241. const type = isImage(dataArray);
  242. if (!type) {
  243. throw new OperationError("Invalid file type.");
  244. }
  245. return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
  246. }
  247. }
  248. export default AddTextToImage;