PHP WebShell

Текущая директория: /opt/BitGoJS/modules/key-card/src

Просмотр файла: drawKeycard.ts

import type { jsPDF } from 'jspdf';
import * as QRCode from 'qrcode';
import { IDrawKeyCard } from './types';
import { splitKeys } from './utils';
type jsPDFModule = typeof import('jspdf');

async function loadJSPDF(): Promise<jsPDFModule> {
  let jsPDF: jsPDFModule;

  if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
    // We are in the browser
    jsPDF = await import('jspdf');
  } else {
    // We are in Node.js
    jsPDF = require('jspdf');
  }
  return jsPDF;
}

enum KeyCurveName {
  ed25519 = 'EDDSA',
  secp256k1 = 'ECDSA',
  bls = 'BLS',
}

// Max for Binary/Byte Data https://github.com/soldair/node-qrcode#qr-code-capacity
// the largest theoretically possible value is actually 2953 but the QR codes get so dense that scanning them with a
// phone (off of a printed page) doesn't work anymore
// this limitation was chosen by trial and error
export const QRBinaryMaxLength = 1500;

const font = {
  header: 24,
  subheader: 15,
  body: 12,
};

const color = {
  black: '#000000',
  darkgray: '#4c4c4c',
  gray: '#9b9b9b',
  red: '#e21e1e',
};

const margin = 30;

// Helpers for data formatting / positioning on the paper
function left(x: number): number {
  return margin + x;
}
function moveDown(y: number, ydelta: number): number {
  return y + ydelta;
}

function drawOnePageOfQrCodes(
  qrImages: HTMLCanvasElement[],
  doc: jsPDF,
  y: number,
  qrSize: number,
  startIndex
): number {
  doc.setFont('helvetica');
  let qrIndex: number = startIndex;
  for (; qrIndex < qrImages.length; qrIndex++) {
    const image = qrImages[qrIndex];
    const textBuffer = 15;
    if (y + qrSize + textBuffer >= doc.internal.pageSize.getHeight()) {
      return qrIndex;
    }

    doc.addImage(image, left(0), y, qrSize, qrSize);

    if (qrImages.length === 1) {
      return qrIndex + 1;
    }

    y = moveDown(y, qrSize + textBuffer);
    doc.setFontSize(font.body).setTextColor(color.black);
    doc.text('Part ' + (qrIndex + 1).toString(), left(0), y);
    y = moveDown(y, 20);
  }
  return qrIndex + 1;
}

function computeKeyCardImageDimensions(keyCardImage: HTMLImageElement) {
  // Max dimensions stablished by fixed available PDF space
  const KEY_CARD_IMAGE_MAX_DIMENSIONS = {
    width: 303,
    height: 40,
  };

  const { width: imgWidth, height: imgHeight } = keyCardImage;
  const { width: maxWidth, height: maxHeight } = KEY_CARD_IMAGE_MAX_DIMENSIONS;

  // Try scaling ratio based on width
  const wRatio = imgWidth / maxWidth;
  let finalRatio = wRatio;

  // If resized height exceeds the available height space, base ratio also on height
  if (imgHeight / finalRatio > maxHeight) {
    finalRatio = imgHeight / maxHeight;
  }
  return [imgWidth / finalRatio, imgHeight / finalRatio];
}

export async function drawKeycard({
  activationCode,
  questions,
  keyCardImage,
  qrData,
  walletLabel,
  curve,
}: IDrawKeyCard): Promise<jsPDF> {
  const jsPDFModule = await loadJSPDF();

  // document details
  const width = 8.5 * 72;
  let y = 0;

  // Create the PDF instance
  const doc = new jsPDFModule.jsPDF('portrait', 'pt', 'letter'); // jshint ignore:line
  doc.setFont('helvetica');

  // PDF Header Area - includes the logo and company name
  // This is data for the BitGo logo in the top left of the PDF
  y = moveDown(y, 30);

  if (keyCardImage) {
    const [imgWidth, imgHeight] = computeKeyCardImageDimensions(keyCardImage);
    doc.addImage(keyCardImage, left(0), y, imgWidth, imgHeight);
  }

  // Activation Code
  if (activationCode) {
    y = moveDown(y, 8);
    doc.setFontSize(font.body).setTextColor(color.gray);
    doc.text('Activation Code', left(460), y);
  }
  doc.setFontSize(font.header).setTextColor(color.black);
  y = moveDown(y, 25);
  doc.text('KeyCard', left(curve && !activationCode ? 460 : 325), y - 1);
  if (activationCode) {
    doc.setFontSize(font.header).setTextColor(color.gray);
    doc.text(activationCode, left(460), y);
  }

  // Subheader
  // titles
  const date = new Date().toDateString();
  y = moveDown(y, margin);
  doc.setFontSize(font.body).setTextColor(color.gray);
  const title = curve ? KeyCurveName[curve] + ' key:' : 'wallet named:';
  doc.text('Created on ' + date + ' for ' + title, left(0), y);
  // copy
  y = moveDown(y, 25);
  doc.setFontSize(font.subheader).setTextColor(color.black);
  doc.text(walletLabel, left(0), y);
  if (!curve) {
    // Red Bar
    y = moveDown(y, 20);
    doc.setFillColor(255, 230, 230);
    doc.rect(left(0), y, width - 2 * margin, 32, 'F');

    // warning message
    y = moveDown(y, 20);
    doc.setFontSize(font.body).setTextColor(color.red);
    doc.text('Print this document, or keep it securely offline. See below for FAQ.', left(75), y);
  }
  // Generate the first page's data for the backup PDF
  y = moveDown(y, 35);
  const qrSize = 130;
  const qrKeys = ['user', 'userMasterPublicKey', 'backup', 'backupMasterPublicKey', 'bitgo', 'passcode'].filter(
    (key) => !!qrData[key]
  );
  for (let index = 0; index < qrKeys.length; index++) {
    const name = qrKeys[index];
    if (index === 2) {
      // Add 2nd Page
      doc.addPage();

      // 2nd page title
      y = 30;
    }

    const qr = qrData[name];
    let topY = y;
    const textLeft = left(qrSize + 15);
    let textHeight = 0;

    const qrImages: HTMLCanvasElement[] = [];
    const keys = splitKeys(qr.data, QRBinaryMaxLength);
    for (const key of keys) {
      qrImages.push(await QRCode.toCanvas(key, { errorCorrectionLevel: 'L' }));
    }

    let nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, 0);

    doc.setFontSize(font.subheader).setTextColor(color.black);
    y = moveDown(y, 10);
    textHeight += 10;
    doc.text(qr.title, textLeft, y);
    textHeight += doc.getLineHeight();
    y = moveDown(y, 15);
    textHeight += 15;
    doc.setFontSize(font.body).setTextColor(color.darkgray);
    doc.text(qr.description, textLeft, y);
    textHeight += doc.getLineHeight();
    doc.setFontSize(font.body - 2);
    if (qr?.data?.length > QRBinaryMaxLength) {
      y = moveDown(y, 30);
      textHeight += 30;
      doc.text('Note: you will need to put all Parts together for the full key', textLeft, y);
    }
    y = moveDown(y, 30);
    textHeight += 30;
    doc.text('Data:', textLeft, y);
    textHeight += doc.getLineHeight();
    y = moveDown(y, 15);
    textHeight += 15;
    const width = 72 * 8.5 - textLeft - 30;
    doc.setFont('courier').setFontSize(9).setTextColor(color.black);
    const lines = doc.splitTextToSize(qr.data, width);
    const buffer = 10;
    for (let line = 0; line < lines.length; line++) {
      // add new page if data does not fit on one page
      if (y + buffer >= doc.internal.pageSize.getHeight()) {
        doc.addPage();
        textHeight = 0;
        y = 30;
        topY = y;
        nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, nextQrIndex);
        doc.setFont('courier').setFontSize(9).setTextColor(color.black);
      }
      doc.text(lines[line], textLeft, y);
      if (line !== lines.length - 1) {
        y = moveDown(y, buffer);
        textHeight += buffer;
      }
    }

    // Add public key if exists
    if (qr.publicMasterKey) {
      const text = 'Key Id: ' + qr.publicMasterKey;

      // Gray bar
      y = moveDown(y, 20);
      textHeight += 20;
      doc.setFillColor(247, 249, 249); // Gray background
      doc.setDrawColor(0, 0, 0); // Border

      // Leave a bit of space for the side of the rectangle.
      const splitKeyId = doc.splitTextToSize(text, width - 10);

      // The height of the box must be at least 15 px (for single line case), or
      // a multiple of 13 for each line.  This allows for proper padding.
      doc.rect(textLeft, y, width, Math.max(12 * splitKeyId.length, 15), 'FD');
      textHeight += splitKeyId.length * doc.getLineHeight();
      doc.text(splitKeyId, textLeft + 5, y + 10);
    }

    doc.setFont('helvetica');
    // Move down the size of the QR code minus accumulated height on the right side, plus margin
    // if we have a key that spans multiple pages, then exclude QR code size
    const rowHeight = Math.max(qr.data.length > QRBinaryMaxLength ? qrSize + 20 : qrSize, textHeight);
    const marginBottom = 15;
    y = moveDown(y, rowHeight - (y - topY) + marginBottom);
  }

  // Add next Page
  doc.addPage();

  // next pages title
  y = 0;
  y = moveDown(y, 55);
  doc.setFontSize(font.header).setTextColor(color.black);
  doc.text('BitGo KeyCard FAQ', left(0), y);

  // Generate the second page's data for the backup PDF
  y = moveDown(y, 30);
  questions.forEach(function (q) {
    doc.setFontSize(font.subheader).setTextColor(color.black);
    doc.text(q.question, left(0), y);
    y = moveDown(y, 20);
    doc.setFontSize(font.body).setTextColor(color.darkgray);
    q.answer.forEach(function (line) {
      doc.text(line, left(0), y);
      y = moveDown(y, font.body + 3);
    });
    y = moveDown(y, 22);
  });

  return doc;
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!