Skip to content
Snippets Groups Projects
qrCodeTemplateUtils.ts 38.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • import QRCode from "easyqrcodejs";
    
    import { isNode } from "./common";
    
    
    const DEFAULT_TEMPLATE =
    
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXUAAAF1CAYAAAAX/XrIAAAACXBIWXMAAAsTAAALEwEAmpwYAAAG0mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OTc3NywgMjAyMy8wNi8yNS0yMzo1NzoxNCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIzLTExLTI0VDEyOjU4OjUxKzAyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0xMS0yNFQxMzoxMDo1MyswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0xMS0yNFQxMzoxMDo1MyswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE5ZDUxMjEtOTMwOC00MWJhLTg3OTYtZWJhMjUzNTYwODUxIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OWQ2ZGE1NTEtYjQ1Yy1jYTRhLTg1YWUtYmE5MjJmMTZkMDk1IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MDA2OWIyM2MtNDVlOC00Yjg2LWI4OGEtOTZlODQzMjQzNWQxIj4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSJTY2FuIHRvIGFjY2VzcyIgcGhvdG9zaG9wOkxheWVyVGV4dD0iU2NhbiB0byBhY2Nlc3MiLz4gPC9yZGY6QmFnPiA8L3Bob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjAwNjliMjNjLTQ1ZTgtNGI4Ni1iODhhLTk2ZTg0MzI0MzVkMSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0yNFQxMjo1ODo1MSswMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKE1hY2ludG9zaCkiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MDE5ZDUxMjEtOTMwOC00MWJhLTg3OTYtZWJhMjUzNTYwODUxIiBzdEV2dDp3aGVuPSIyMDIzLTExLTI0VDEzOjEwOjUzKzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBl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FYX6y9jsELrhhhuOCO+5TA+Lyy67zA4M1N/3/v7+GR/2qZCZHS4bNmywGzZsmLH96UE7PdSnh/mxguZE9m/2tqaH02WXXWa3bt06Zxlmn6Dmcuutt85Zrv7+/uY+TA+7O++8c8ZxnwrYgYGBGfs8dWxPdvnpJ6vp5d+8eXNzn6f/fOpn1157bXMds9c9tc0bbrjBAnbNmjUzjtnUzwG7efPm4x4zOfUU6jEzu7Y+/WvNmjV248aNc344pwf39A+1tTMDfCrEpofr9PXl8/kZ25wKk9lXAdNDcvrvpp8IjuVo4Tw9iGfXomdv63i19bmCe/YxmV7eqeXn2oeBgYEj9v1kl9+wYcNRyzO9Vj51zI92opy+7NRJemrdGzZsmLHs9PfzRE6Ecuop1GOov79/Rg1rrq/pTRDTa4AnGqpbt25tfk03O9Snfj+7pj67vNNfcyJNI0cLmqmgnB1Oc23rRJp75rJx48bmSW3qeE3f7xO5Mnohyx+txjx1Yp5a39GuWGav19qZNfKjHZcTeV/k1Jsr1NVR+jK3atUqPvWpTzVHvEzvHJ1yyy238IEPfACAAwcONH9+1llnndA2zj//fM4//3yAZifpjTfeyIUXXnjc177qVa86orzTvZCbox566CEALrjggjl/P31bExMTx13fXXfddUQn5jXXXMPdd989Y7n+/v7mv9euXXvc9b6Q5S+99NIZ5Zn6muokf+655wD4whe+AMDdd9/N+vXrm52kmzZtanbCZrNZAN7//vezZs0aAK666qoZnaR33XXXjGXl5UehHiNXX301119/PdZa+vv72bBhQ/N33/zmN485quVYdu7cyeWXX8769eu55ppruOaaa/j0pz/NkiVLXqyin3I33ngjV111FbfccsuMn996661ce+21p6hUJ+7KK69k69at3HDDDc2f3XLLLVxzzTWsX7+ed73rXc0T6KpVq/jxj388owJw9913c80113DVVVexdu3a02JUkzw/CvWXqek1yrnutFy1ahVf/OIXmzUygH379pHL5ZrfDw8PH/G6qaFuU7W2QqHAW9/61mZtdePGjWzduhVrLT/84Q9fgj07cVNXCg8//PCcv58eTK2trUddz86dO5s3cc0e73/99dcfcbWxcOHC5r+ffvrp45bzZJc/44wzmv/evHlzsyxzfX3kIx9pLnv++ec3r9ry+Twb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";
    
    const DEFAULT_LOGO =
    
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAYAAAA4TnrqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGOmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OTc3NywgMjAyMy8wNi8yNS0yMzo1NzoxNCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIzLTExLTI0VDEyOjU4OjUxKzAyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0xMS0yNFQxMzoxMzo1MSswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0xMS0yNFQxMzoxMzo1MSswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjUxY2VlODgtNDlhNi00MDNkLWE4MzAtMWUwNzZkZTdhY2M4IiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NzZjZjhmYjAtODU3Zi1mZTQzLTk2MjMtZjg1NjBjY2IxZjJhIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjQzNDNkMWItNjEyZC00OGY0LTk1NjctZTNiOWI1MjgwMGQ0Ij4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyNDM0M2QxYi02MTJkLTQ4ZjQtOTU2Ny1lM2I5YjUyODAwZDQiIHN0RXZ0OndoZW49IjIwMjMtMTEtMjRUMTI6NTg6NTErMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChNYWNpbnRvc2gpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjb252ZXJ0ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmY1MWNlZTg4LTQ5YTYtNDAzZC1hODMwLTFlMDc2ZGU3YWNjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0yNFQxMzoxMzo1MSswMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKE1hY2ludG9zaCkiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+wWASQQAACMZJREFUeJzlnH+MHVUVxz933u5CrMmTVAFD9lkigv1BFehY0NBUVBLqgIr2D5OSUA0wYmvcBhEMaflRwLRqolZ92vBDBakSG0yWP9DsEoMl1UWI/QEFCoWHLTRS5IG0pu7c4x9z5/3a92buzPux3dfvZjJvZs69c+53zrlzzp17V4kIxwv8UvkE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";
    
    interface ChromakeyBoundaries {
      fromL: number;
      fromT: number;
      toL: number;
      toT: number;
    }
    
    const findChromakeyBoundaries = (
      width: number,
      height: number,
      imageData: ImageData,
      chromakeyWidth: number,
      chromakeyHeight: number
    ): ChromakeyBoundaries => {
    
      const maskSizeX = chromakeyWidth;
      const maskSizeY = chromakeyHeight;
    
      const totalSum = maskSizeX * maskSizeY * 255;
    
    
      const matrixR = [];
      const matrixG = [];
      const matrixB = [];
    
      for (let i = 0; i < width; i++) {
        matrixR[i] = [];
        matrixG[i] = [];
        matrixB[i] = [];
        for (let j = 0; j < height; j++) {
          matrixR[i][j] = 0;
          matrixG[i][j] = 0;
          matrixB[i][j] = 0;
        }
      }
    
    
      const maxDistance = Math.sqrt(3 * totalSum * totalSum);
      const suitableSimilarity = 0.6;
    
      let bestSimilarity = 0;
      let bestX = -1;
      let bestY = -1;
    
      for (let x = 0; x < width; x++) {
        for (let y = 0; y < height; y++) {
          const index = (y * width + x) * 4;
          const r = imageData.data[index];
          const g = imageData.data[index + 1];
          const b = imageData.data[index + 2];
    
          matrixR[x][y] += r;
          matrixG[x][y] += g;
          matrixB[x][y] += b;
    
          if (x - 1 >= 0) {
            matrixR[x][y] += matrixR[x - 1][y];
            matrixG[x][y] += matrixG[x - 1][y];
            matrixB[x][y] += matrixB[x - 1][y];
          }
    
          if (y - 1 >= 0) {
            matrixR[x][y] += matrixR[x][y - 1];
            matrixG[x][y] += matrixG[x][y - 1];
            matrixB[x][y] += matrixB[x][y - 1];
          }
    
          if (x - 1 >= 0 && y - 1 >= 0) {
            matrixR[x][y] -= matrixR[x - 1][y - 1];
            matrixG[x][y] -= matrixG[x - 1][y - 1];
            matrixB[x][y] -= matrixB[x - 1][y - 1];
          }
    
          if (x >= maskSizeX && y >= maskSizeY) {
    
            const tempSumR =
              matrixR[x][y] +
    
              matrixR[x - maskSizeX][y - maskSizeY] -
              (matrixR[x - maskSizeX][y] + matrixR[x][y - maskSizeY]);
    
            const tempSumG =
              matrixG[x][y] +
    
              matrixG[x - maskSizeX][y - maskSizeY] -
              (matrixG[x - maskSizeX][y] + matrixG[x][y - maskSizeY]);
    
            const tempSumB =
              matrixB[x][y] +
    
              matrixB[x - maskSizeX][y - maskSizeY] -
              (matrixB[x - maskSizeX][y] + matrixB[x][y - maskSizeY]);
    
            const distance = Math.sqrt(
              Math.pow(0 - tempSumR, 2) +
                Math.pow(totalSum - tempSumG, 2) +
                Math.pow(0 - tempSumB, 2)
            );
            const similarity = 1 - distance / maxDistance;
    
            if (similarity > bestSimilarity) {
              bestX = x;
              bestY = y;
              bestSimilarity = similarity;
    
      if (bestX >= 0 && bestY >= 0 && bestSimilarity > suitableSimilarity) {
        return {
          fromL: bestX - maskSizeX,
          fromT: bestY - maskSizeY,
          toL: bestX - maskSizeX + chromakeyWidth,
          toT: bestY - maskSizeY + chromakeyHeight,
        };
      }
    
    
      return { fromL: -1, fromT: -1, toL: -1, toT: -1 };
    };
    
    
    interface TemplateOptions {
      src: string;
      placeholderSize: number;
    }
    
    
    /**
     * https://www.qrcode.com/en/about/error_correction.html#:~:text=QR%20Code%20has%20error%20correction,of%20data%20QR%20Code%20size.
     */
    type CorrectionLevels = "L" | "M" | "Q" | "H";
    
    
    interface QrCodeOptions {
      width?: number;
      height?: number;
      logoSrc?: string;
      logoWidth?: number;
      logoHeight?: number;
    
      correctionLevel?: CorrectionLevels;
    
    const defaultOptions: QrCodeOptions = {
      width: 256,
      height: 256,
      logoSrc: DEFAULT_LOGO,
      logoWidth: 75,
    
      logoHeight: 75,
    
      // Use "L" level as we don't need a redundancy
      correctionLevel: "L",
    };
    
    const defaultTemplateOptions: TemplateOptions = {
      placeholderSize: 260,
      src: DEFAULT_TEMPLATE,
    };
    
    /**
     * Generates QR Code based of the context - Browser or NodeJs.
     * IMPORTANT: In case of NodeJS it uses the JSDON and Canvas libraries to mimic the DOM behaviour
     *
     * @param text
     * @param qrCodeOptions
     * @param templateOptions
     * @returns
     */
    const generateQrCode = (
    
      text: string,
      qrCodeOptions?: QrCodeOptions,
      templateOptions?: TemplateOptions
    ): Promise<string> => {
      qrCodeOptions = qrCodeOptions
        ? Object.assign(defaultOptions, qrCodeOptions)
        : defaultOptions;
    
      templateOptions = templateOptions
        ? Object.assign(defaultTemplateOptions, templateOptions)
        : defaultTemplateOptions;
    
      const options = {
        text,
        width: qrCodeOptions.width,
        height: qrCodeOptions.height,
        colorDark: "#000000",
        colorLight: "#ffffff",
        dotScale: 1,
      };
    
      if (qrCodeOptions.logoSrc) {
        Object.assign(options, {
          logo: qrCodeOptions.logoSrc,
          logoBackgroundTransparent: true,
          logoWidth: qrCodeOptions.logoWidth,
          logoHeight: qrCodeOptions.logoHeight,
        });
      }
    
    
      if (isNode) {
        return generateNodeJSQrCode(options, qrCodeOptions, templateOptions);
      }
      options["correctLevel"] = QRCode.CorrectLevel[qrCodeOptions.correctionLevel]; // L, M, Q, H
      return generateBrowserQrCode(options, templateOptions);
    };
    
    /**
     * Nodejs impl for generation of qr code. Uses JSDOM and Canvas libs to mimic the DOM.
     *
     * @param defaultOptions
     * @param qrCodeOptions
     * @param templateOptions
     * @returns
     */
    const generateNodeJSQrCode = async (
      defaultOptions: QrCodeOptions,
      qrCodeOptions: QrCodeOptions,
      templateOptions: TemplateOptions
    ): Promise<string> => {
    
    Zdravko Iliev's avatar
    Zdravko Iliev committed
      const QRCodeNodeJS = await import("easyqrcodejs-nodejs/index.js");
    
    
      defaultOptions["correctLevel"] =
        QRCodeNodeJS.CorrectLevel[qrCodeOptions.correctionLevel]; // L, M, Q, H
    
      const qrcode = new QRCodeNodeJS.default(defaultOptions);
      const dataUrl = await qrcode.toDataURL();
    
      await new Promise((resolve) => setTimeout(resolve, 10));
    
      return await putQrCodeOnChromakeyTemplateNodeJS(
        dataUrl,
        templateOptions.src,
        templateOptions.placeholderSize,
        templateOptions.placeholderSize
      );
    };
    
    const generateBrowserQrCode = async (
      qrCodeOptions: QrCodeOptions,
      templateOptions: TemplateOptions
    ): Promise<string> => {
      const container = document.createElement("div");
      new QRCode(container, qrCodeOptions);
    
      const canvas = container.getElementsByTagName("canvas")[0];
    
      // Add short async action because of the bug with logo not appearing in the resulting image.
      await new Promise((resolve) => setTimeout(resolve, 10));
    
      return await putQrCodeOnChromakeyTemplate(
        canvas.toDataURL(),
        templateOptions.src,
        templateOptions.placeholderSize,
        templateOptions.placeholderSize
      );
    };
    
    
    /**
     * Nodejs impl for template generation
     *
     * @param qrCodeImageBase64
     * @param templateImageBase64
     * @param placeholderWidth
     * @param placeholderHeight
     * @param scale
     * @returns
     */
    const putQrCodeOnChromakeyTemplateNodeJS = async (
      qrCodeImageBase64: string,
      templateImageBase64: string,
      placeholderWidth: number,
      placeholderHeight: number,
      scale = 1
    ): Promise<string> => {
      const jsdom = await import("jsdom");
    
      const { JSDOM } = jsdom;
      const dom = new JSDOM("", { resources: "usable" });
      const document = dom.window.document;
      let qrCodeImage;
    
      try {
        qrCodeImage = await loadImageNode(qrCodeImageBase64, document);
      } catch (e) {
        throw new Error("NodeJS cannot load qr code image");
      }
      const templateImage = await loadImageNode(templateImageBase64, document);
    
      if (
        templateImage.width < placeholderWidth ||
        templateImage.height < placeholderHeight
      ) {
        throw new Error("Placeholder is bigger than image");
      }
    
      const templateCanvas = document.createElement("canvas");
      templateCanvas.width = templateImage.width;
      templateCanvas.height = templateImage.height;
    
      const templateCtx = templateCanvas.getContext("2d");
      templateCtx.drawImage(
        templateImage,
        0,
        0,
        templateImage.width,
        templateImage.height
      );
    
      const templateImgData = templateCtx.getImageData(
        0,
        0,
        templateCanvas.width,
        templateCanvas.height
      );
    
      const placeholderCoordinates = findChromakeyBoundaries(
        templateImage.width,
        templateImage.height,
        templateImgData,
        placeholderWidth,
        placeholderHeight
      );
      // -2 is for QR to slightly cover borders. To avoid green mask bulging out
      const scaleX = ((qrCodeImage.width - 2) / placeholderWidth) * scale;
      const scaleY = ((qrCodeImage.height - 2) / placeholderHeight) * scale;
      qrCodeImage.width *= scale;
      qrCodeImage.height *= scale;
    
      const bannerCanvas = document.createElement("canvas");
      const scaledTemplateW = Math.floor(templateImage.width * scaleX);
      const scaledTemplateH = Math.floor(templateImage.height * scaleY);
    
      bannerCanvas.width = scaledTemplateW;
      bannerCanvas.height = scaledTemplateH;
    
      const bannerCtx = bannerCanvas.getContext("2d");
      // bannerCtx
    
      bannerCtx.drawImage(
        templateImage,
        0,
        0,
        templateImage.width,
        templateImage.height,
        0,
        0,
        scaledTemplateW,
        scaledTemplateH
      );
      // +1 hides green border
      bannerCtx.drawImage(
        qrCodeImage,
        placeholderCoordinates.fromL * scaleX,
        placeholderCoordinates.fromT * scaleY,
        qrCodeImage.width,
        qrCodeImage.height
      );
      return bannerCanvas.toDataURL();
    };
    
    
    Igor Markin's avatar
    Igor Markin committed
    const putQrCodeOnChromakeyTemplate = async (
      qrCodeImageBase64: string,
      templateImageBase64: string,
    
      placeholderWidth: number,
    
    Igor Markin's avatar
    Igor Markin committed
      placeholderHeight: number,
      scale = 1
    
    Igor Markin's avatar
    Igor Markin committed
    ): Promise<string> => {
      const qrCodeImage = await loadImage(qrCodeImageBase64);
      const templateImage = await loadImage(templateImageBase64);
    
    
      if (
        templateImage.width < placeholderWidth ||
        templateImage.height < placeholderHeight
      ) {
        throw new Error("Placeholder is bigger than image");
      }
    
    
    Igor Markin's avatar
    Igor Markin committed
      const templateCanvas = document.createElement("canvas");
      templateCanvas.width = templateImage.width;
      templateCanvas.height = templateImage.height;
    
    Igor Markin's avatar
    Igor Markin committed
      const templateCtx = templateCanvas.getContext("2d");
      templateCtx.drawImage(
    
        templateImage,
        0,
        0,
        templateImage.width,
    
    Igor Markin's avatar
    Igor Markin committed
        templateImage.height
      );
    
      const templateImgData = templateCtx.getImageData(
    
    Igor Markin's avatar
    Igor Markin committed
        templateCanvas.width,
        templateCanvas.height
    
      );
    
      const placeholderCoordinates = findChromakeyBoundaries(
        templateImage.width,
        templateImage.height,
    
    Igor Markin's avatar
    Igor Markin committed
        templateImgData,
        placeholderWidth,
        placeholderHeight
    
    Igor Markin's avatar
    Igor Markin committed
      // -2 is for QR to slightly cover borders. To avoid green mask bulging out
      const scaleX = ((qrCodeImage.width - 2) / placeholderWidth) * scale;
      const scaleY = ((qrCodeImage.height - 2) / placeholderHeight) * scale;
      qrCodeImage.width *= scale;
      qrCodeImage.height *= scale;
    
      const bannerCanvas = document.createElement("canvas");
      const scaledTemplateW = Math.floor(templateImage.width * scaleX);
      const scaledTemplateH = Math.floor(templateImage.height * scaleY);
    
      bannerCanvas.width = scaledTemplateW;
      bannerCanvas.height = scaledTemplateH;
    
      const bannerCtx = bannerCanvas.getContext("2d");
      // bannerCtx
    
      bannerCtx.drawImage(
        templateImage,
        0,
        0,
        templateImage.width,
        templateImage.height,
        0,
        0,
        scaledTemplateW,
        scaledTemplateH
      );
    
      // +1 hides green border
      bannerCtx.drawImage(
    
        qrCodeImage,
    
    Igor Markin's avatar
    Igor Markin committed
        placeholderCoordinates.fromL * scaleX,
        placeholderCoordinates.fromT * scaleY,
    
        qrCodeImage.width,
        qrCodeImage.height
      );
    
    Igor Markin's avatar
    Igor Markin committed
      return bannerCanvas.toDataURL();
    
    Igor Markin's avatar
    Igor Markin committed
    const loadImage = (imageSrc: string): Promise<HTMLImageElement> => {
      return new Promise((resolve, reject) => {
        const templateImg = document.createElement("img");
        templateImg.src = imageSrc;
        templateImg.onload = () => resolve(templateImg);
    
        templateImg.onerror = (error) => reject(error);
      });
    };
    
    /**
     * Duplicates the load img for browser
     *
     * @param imageSrc
     * @returns
     */
    const loadImageNode = (
      imageSrc: string,
      document
    ): Promise<HTMLImageElement> => {
      return new Promise((resolve, reject) => {
        const templateImg = document.createElement("img");
        templateImg.src = imageSrc;
        templateImg.onload = () => resolve(templateImg);
        templateImg.onerror = (error) => reject(error);
    
    export default {
      putQrCodeOnChromakeyTemplate,
    
      generateQrCode,