teamdigitale/italia-app

View on GitHub
ts/features/fci/utils/signatureFields.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { pipe } from "fp-ts/lib/function";
import * as RA from "fp-ts/lib/ReadonlyArray";
import * as O from "fp-ts/lib/Option";
import * as S from "fp-ts/lib/string";
import * as N from "fp-ts/number";
import { contramap } from "fp-ts/lib/Ord";
import { PDFDocument, rgb } from "pdf-lib";
import ReactNativeBlobUtil from "react-native-blob-util";
import { SignatureField } from "../../../../definitions/fci/SignatureField";
import I18n from "../../../i18n";
import { TypeEnum as ClauseTypeEnum } from "../../../../definitions/fci/Clause";
import { TranslationKeys } from "../../../../locales/locales";
import { DocumentToSign } from "../../../../definitions/fci/DocumentToSign";
import { DocumentDetailView } from "../../../../definitions/fci/DocumentDetailView";
import { SignatureFieldToBeCreatedAttrs } from "../../../../definitions/fci/SignatureFieldToBeCreatedAttrs";
import { SignatureFieldAttrType } from "../components/DocumentWithSignature";
import { ExistingSignatureFieldAttrs } from "../../../../definitions/fci/ExistingSignatureFieldAttrs";
import { savePath } from "../saga/networking/handleDownloadDocument";

const clausesEnumValues = {
  [ClauseTypeEnum.REQUIRED]: "features.fci.signatureFields.required",
  [ClauseTypeEnum.UNFAIR]: "features.fci.signatureFields.unfair",
  [ClauseTypeEnum.OPTIONAL]: "features.fci.signatureFields.optional"
};

export const getClauseLabel = (clauseType: ClauseTypeEnum) =>
  I18n.t(`${clausesEnumValues[clauseType] as TranslationKeys}`);

export type LIST_DATA_TYPE = {
  title: string;
  data: ReadonlyArray<SignatureField>;
};

/*
 * Get the list of required signature fields
 */
export const getRequiredSignatureFields = (
  signatureFields: ReadonlyArray<SignatureField>
) =>
  clausesByType(signatureFields, [
    ClauseTypeEnum.REQUIRED,
    ClauseTypeEnum.UNFAIR
  ]);

/**
 * Get the list of optional signature fields
 */
export const getOptionalSignatureFields = (
  signatureFields: ReadonlyArray<SignatureField>
) => clausesByType(signatureFields, [ClauseTypeEnum.OPTIONAL]);

/**
 * Get the list of clauses by type
 */
export const clausesByType = (
  signatureFields: ReadonlyArray<SignatureField>,
  clauseType: ReadonlyArray<string>
) =>
  pipe(
    signatureFields,
    RA.filterMap(signatureField =>
      clauseType.includes(signatureField.clause.type)
        ? O.fromNullable(signatureField)
        : O.none
    )
  );

/**
 * Get the list of all types for the signature fields
 * of the current document
 */
export const getAllTypes = (signatureFields: ReadonlyArray<SignatureField>) =>
  pipe(
    signatureFields,
    RA.filterMap(signatureField => O.fromNullable(signatureField.clause.type)),
    RA.uniq(S.Eq)
  );

/**
 * Giving a list of signature fields, it returns the DATA
 * to rendering the SectionList
 */
export const getSectionListData = (
  signatureFields: ReadonlyArray<SignatureField>
): ReadonlyArray<LIST_DATA_TYPE> =>
  pipe(
    getAllTypes(signatureFields),
    RA.map(type => ({
      title: type,
      data: clausesByType(signatureFields, [type])
    }))
  );

/**
 * Defines a total ordering for the signature field type: UNFAIR -> REQURED -> EVERYTHING ELSE (OPTIONAL)
 */
const byClausesType = pipe(
  N.Ord,
  contramap((signatureField: SignatureField) => {
    switch (signatureField.clause.type) {
      case ClauseTypeEnum.UNFAIR:
        return 0;
      case ClauseTypeEnum.REQUIRED:
        return 1;
      case ClauseTypeEnum.OPTIONAL:
        return 2;
      default:
        return 3;
    }
  })
);

/**
 * Defines a read only array sorting by using the total ordering byClausesType
 */
const sortByType = RA.sortBy([byClausesType]);

/**
 * Orders the signatureFields array with the given order: UNFAIR -> REQURED -> EVERYTHING ELSE (OPTIONAL)
 * @param signatureFields an array of signature fields
 * @returns the new ordered array
 */
export const orderSignatureFields = (
  signatureFields: ReadonlyArray<SignatureField>
): ReadonlyArray<SignatureField> => pipe(signatureFields, sortByType);

/**
 * Given a list of documents to sign and an array of Clauses types
 * it returns the number of clauses.
 * @param documentsToSign the list of documents to sign
 * @returns the number of OPTIONAL clauses
 */
export const getClausesCountByTypes = (
  documentsToSign: ReadonlyArray<DocumentToSign>,
  clausesType: ReadonlyArray<string>
): number =>
  pipe(
    documentsToSign,
    RA.chain(d => d.signature_fields),
    RA.filterMap(f =>
      clausesType.includes(f.clause.type) ? O.some(f) : O.none
    ),
    RA.size
  );

/**
 * Get the number of signature fields.
 * @param doc the document detail view
 * @returns the number of signature fields
 */
export const getSignatureFieldsLength = (doc: DocumentDetailView) =>
  pipe(
    doc,
    O.fromNullable,
    O.map(_ => _.metadata.signature_fields),
    O.map(_ => _.length),
    O.getOrElse(() => 0)
  );

/**
 * Adds a base 64 PDF uri scheme to a base64 string representation of a PDF.
 * @param r the base64 string representation of a PDF
 * @returns r prexied by the uri scheme
 */
const addBase64PdfUriScheme = (r: string) => `data:application/pdf;base64,${r}`;

/**
 * Parses a PDF from filesystem as a base64 string with the prefixed uri scheme.
 * @param uri the uri of the PDF
 * @returns a base64 string representation of the PDF at uri
 */
export const parsePdfAsBase64 = async (uri: string) => {
  const parsed = await ReactNativeBlobUtil.fs.readFile(
    `${savePath(uri)}`,
    "base64"
  );
  return addBase64PdfUriScheme(parsed);
};

/**
 * Converts a PDFDocument instance to a base64 string representation with the prefixed URI scheme.
 * @param parsedPdf the PDFDocument instance
 * @returns a base64 string repreesntation with the prefixed URI scheme
 */
const savePdfDocumentoAsBase64 = async (parsedPdf: PDFDocument) => {
  const res = await parsedPdf.saveAsBase64();
  return addBase64PdfUriScheme(res);
};

/**
 * Get the pdf url from documents, download it as base64 string and load the pdf as pdf-lib object to draw a rect over the signature field.
 * @param uniqueName the of the signature field
 * @param bytes the pdf representation
 * @returns a promise of an output document with the drawn box and the field page
 */
const drawRectangleOverSignatureFieldById = async (
  bytes: string,
  uniqueName: string
) => {
  const parsedPdf = await PDFDocument.load(addBase64PdfUriScheme(bytes));
  const pageRef = parsedPdf.findPageForAnnotationRef(
    parsedPdf.getForm().getSignature(uniqueName).ref
  );
  if (pageRef) {
    const page = parsedPdf.getPages().indexOf(pageRef);
    // The signature field is extracted by its unique_name.
    // Using low-level acrofield (acrobat field) it is possible
    // to obtain the elements of the signature field such as the
    // box that contains it. Once the box is obtained, its
    // coordinates are used to draw a rectangle on the related page.
    const signature = parsedPdf.getForm().getSignature(uniqueName);
    const [widget] = signature.acroField.getWidgets();
    const rect = widget.getRectangle();
    parsedPdf.getPage(page).drawRectangle({
      x: rect.x,
      y: rect.y,
      width: rect.width,
      height: rect.height,
      color: rgb(0, 0.77, 0.79),
      opacity: 0.5,
      borderOpacity: 0.75
    });
    const base64 = await savePdfDocumentoAsBase64(parsedPdf);
    return {
      drawnBase64: base64,
      signaturePage: page
    };
  } else {
    throw new Error(); // TO:DO refactor with fp-ts https://pagopa.atlassian.net/browse/SFEQS-1601
  }
};

/**
 * Get the pdf url from documents,
 * download it as base64 string and
 * load the pdf as pdf-lib object
 * to draw a rect over the signature field
 * giving a set of coordinates
 * @param attrs the signature field attrs containing the coords
 * @param bytes the pdf representation
 * @returns a promise of an output document with the drawn box and the field page
 */
const drawRectangleOverSignatureFieldByCoordinates = async (
  bytes: string,
  attrs: SignatureFieldToBeCreatedAttrs
) => {
  const parsedPdf = await PDFDocument.load(addBase64PdfUriScheme(bytes));
  const page = attrs.page;
  parsedPdf.getPage(page).drawRectangle({
    x: attrs.bottom_left.x ?? 0,
    y: attrs.bottom_left.y ?? 0,
    height: Math.abs((attrs.top_right.y ?? 0) - (attrs.bottom_left.y ?? 0)),
    width: Math.abs((attrs.top_right.x ?? 0) - (attrs.bottom_left.x ?? 0)),
    color: rgb(0, 0.77, 0.79),
    opacity: 0.5,
    borderOpacity: 0.75
  });
  const base64 = await savePdfDocumentoAsBase64(parsedPdf);
  return {
    drawnBase64: base64,
    signaturePage: page
  };
};

/**
 * Draws a box on a signature field.
 * @param bytes the pdf bytes representation
 * @param attrs the signature field attributes
 * @returns a promise of an output document with the drawn box and the field page
 */
export const drawSignatureField = async (
  bytes: string,
  attrs: ExistingSignatureFieldAttrs | SignatureFieldToBeCreatedAttrs
) => {
  if (hasUniqueName(attrs)) {
    return await drawRectangleOverSignatureFieldById(bytes, attrs.unique_name);
  } else {
    return await drawRectangleOverSignatureFieldByCoordinates(bytes, attrs);
  }
};

/**
 * Checks if the signature field attribute has a unique name or not (coords)
 * @param f the signature field attributes
 * @returns true if the signature field has a unique name, false otherwise
 */
export const hasUniqueName = (
  f: SignatureFieldAttrType
): f is ExistingSignatureFieldAttrs =>
  (f as ExistingSignatureFieldAttrs).unique_name !== undefined;