AddTextToImage.mjs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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 = "byteArray";
  25. this.outputType = "byteArray";
  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 {byteArray} 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(input)) {
  117. throw new OperationError("Invalid file type.");
  118. }
  119. let image;
  120. try {
  121. image = await jimp.read(Buffer.from(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. "Roboto": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
  130. "Roboto Black": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"),
  131. "Roboto Mono": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"),
  132. "Roboto Slab": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt")
  133. };
  134. // Make Webpack load the png font images
  135. await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png");
  136. await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png");
  137. await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png");
  138. await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png");
  139. const font = fontsMap[fontFace];
  140. // LoadFont needs an absolute url, so append the font name to self.docURL
  141. const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default);
  142. jimpFont.pages.forEach(function(page) {
  143. if (page.bitmap) {
  144. // Adjust the RGB values of the image pages to change the font colour.
  145. const pageWidth = page.bitmap.width;
  146. const pageHeight = page.bitmap.height;
  147. for (let ix = 0; ix < pageWidth; ix++) {
  148. for (let iy = 0; iy < pageHeight; iy++) {
  149. const idx = (iy * pageWidth + ix) << 2;
  150. const newRed = page.bitmap.data[idx] - (255 - red);
  151. const newGreen = page.bitmap.data[idx + 1] - (255 - green);
  152. const newBlue = page.bitmap.data[idx + 2] - (255 - blue);
  153. const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha);
  154. // Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
  155. page.bitmap.data[idx] = (newRed > 0) ? newRed : 0;
  156. page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0;
  157. page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0;
  158. page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0;
  159. }
  160. }
  161. }
  162. });
  163. // Scale the image to a factor of 72, so we can print the text at any size
  164. const scaleFactor = 72 / size;
  165. if (size !== 72) {
  166. // Use bicubic for decreasing size
  167. if (size > 72) {
  168. image.scale(scaleFactor, jimp.RESIZE_BICUBIC);
  169. } else {
  170. image.scale(scaleFactor, jimp.RESIZE_BILINEAR);
  171. }
  172. }
  173. // If using the alignment options, calculate the pixel values AFTER the image has been scaled
  174. switch (hAlign) {
  175. case "Left":
  176. xPos = 0;
  177. break;
  178. case "Center":
  179. xPos = (image.getWidth() / 2) - (jimp.measureText(jimpFont, text) / 2);
  180. break;
  181. case "Right":
  182. xPos = image.getWidth() - jimp.measureText(jimpFont, text);
  183. break;
  184. default:
  185. // Adjust x position for the scaled image
  186. xPos = xPos * scaleFactor;
  187. }
  188. switch (vAlign) {
  189. case "Top":
  190. yPos = 0;
  191. break;
  192. case "Middle":
  193. yPos = (image.getHeight() / 2) - (jimp.measureTextHeight(jimpFont, text) / 2);
  194. break;
  195. case "Bottom":
  196. yPos = image.getHeight() - jimp.measureTextHeight(jimpFont, text);
  197. break;
  198. default:
  199. // Adjust y position for the scaled image
  200. yPos = yPos * scaleFactor;
  201. }
  202. image.print(jimpFont, xPos, yPos, text);
  203. if (size !== 72) {
  204. if (size > 72) {
  205. image.scale(1 / scaleFactor, jimp.RESIZE_BILINEAR);
  206. } else {
  207. image.scale(1 / scaleFactor, jimp.RESIZE_BICUBIC);
  208. }
  209. }
  210. let imageBuffer;
  211. if (image.getMIME() === "image/gif") {
  212. imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
  213. } else {
  214. imageBuffer = await image.getBufferAsync(jimp.AUTO);
  215. }
  216. return [...imageBuffer];
  217. } catch (err) {
  218. throw new OperationError(`Error adding text to image. (${err})`);
  219. }
  220. }
  221. /**
  222. * Displays the blurred image using HTML for web apps
  223. *
  224. * @param {byteArray} data
  225. * @returns {html}
  226. */
  227. present(data) {
  228. if (!data.length) return "";
  229. const type = isImage(data);
  230. if (!type) {
  231. throw new OperationError("Invalid file type.");
  232. }
  233. return `<img src="data:${type};base64,${toBase64(data)}">`;
  234. }
  235. }
  236. export default AddTextToImage;