async _getAppearance()

in src/core/annotation.js [2270:2543]


  async _getAppearance(evaluator, task, intent, annotationStorage) {
    if (this.data.password) {
      return null;
    }
    const storageEntry = annotationStorage?.get(this.data.id);
    let value, rotation;
    if (storageEntry) {
      value = storageEntry.formattedValue || storageEntry.value;
      rotation = storageEntry.rotation;
    }

    if (
      rotation === undefined &&
      value === undefined &&
      !this._needAppearances
    ) {
      if (!this._hasValueFromXFA || this.appearance) {
        // The annotation hasn't been rendered so use the appearance.
        return null;
      }
    }

    // Empty or it has a trailing whitespace.
    const colors = this.getBorderAndBackgroundAppearances(annotationStorage);

    if (value === undefined) {
      // The annotation has its value in XFA datasets but not in the V field.
      value = this.data.fieldValue;
      if (!value) {
        return `/Tx BMC q ${colors}Q EMC`;
      }
    }

    if (Array.isArray(value) && value.length === 1) {
      value = value[0];
    }

    assert(typeof value === "string", "Expected `value` to be a string.");
    value = value.trimEnd();

    if (this.data.combo) {
      // The value can be one of the exportValue or any other values.
      const option = this.data.options.find(
        ({ exportValue }) => value === exportValue
      );
      value = option?.displayValue || value;
    }

    if (value === "") {
      // the field is empty: nothing to render
      return `/Tx BMC q ${colors}Q EMC`;
    }

    if (rotation === undefined) {
      rotation = this.rotation;
    }

    let lineCount = -1;
    let lines;

    // We could have a text containing for example some sequences of chars and
    // their diacritics (e.g. "é".normalize("NFKD") shows 1 char when it's 2).
    // Positioning diacritics is really something we don't want to do here.
    // So if a font has a glyph for a acute accent and one for "e" then we won't
    // get any encoding issues but we'll render "e" and then "´".
    // It's why we normalize the string. We use NFC to preserve the initial
    // string, (e.g. "²".normalize("NFC") === "²"
    // but "²".normalize("NFKC") === "2").
    //
    // TODO: it isn't a perfect solution, some chars like "ẹ́" will be
    // decomposed into two chars ("ẹ" and "´"), so we should detect such
    // situations and then use either FakeUnicodeFont or set the
    // /NeedAppearances flag.
    if (this.data.multiLine) {
      lines = value.split(/\r\n?|\n/).map(line => line.normalize("NFC"));
      lineCount = lines.length;
    } else {
      lines = [value.replace(/\r\n?|\n/, "").normalize("NFC")];
    }

    const defaultPadding = 1;
    const defaultHPadding = 2;
    let { width: totalWidth, height: totalHeight } = this;

    if (rotation === 90 || rotation === 270) {
      [totalWidth, totalHeight] = [totalHeight, totalWidth];
    }

    if (!this._defaultAppearance) {
      // The DA is required and must be a string.
      // If there is no font named Helvetica in the resource dictionary,
      // the evaluator will fall back to a default font.
      // Doing so prevents exceptions and allows saving/printing
      // the file as expected.
      this.data.defaultAppearanceData = parseDefaultAppearance(
        (this._defaultAppearance = "/Helvetica 0 Tf 0 g")
      );
    }

    let font = await WidgetAnnotation._getFontData(
      evaluator,
      task,
      this.data.defaultAppearanceData,
      this._fieldResources.mergedResources
    );

    let defaultAppearance, fontSize, lineHeight;
    const encodedLines = [];
    let encodingError = false;
    for (const line of lines) {
      const encodedString = font.encodeString(line);
      if (encodedString.length > 1) {
        encodingError = true;
      }
      encodedLines.push(encodedString.join(""));
    }

    if (encodingError && intent & RenderingIntentFlag.SAVE) {
      // We don't have a way to render the field, so we just rely on the
      // /NeedAppearances trick to let the different sofware correctly render
      // this pdf.
      return { needAppearances: true };
    }

    // We check that the font is able to encode the string.
    if (encodingError && this._isOffscreenCanvasSupported) {
      // If it can't then we fallback on fake unicode font (mapped to sans-serif
      // for the rendering).
      // It means that a printed form can be rendered differently (it depends on
      // the sans-serif font) but at least we've something to render.
      // In an ideal world the associated font should correctly handle the
      // possible chars but a user can add a smiley or whatever.
      // We could try to embed a font but it means that we must have access
      // to the raw font file.
      const fontFamily = this.data.comb ? "monospace" : "sans-serif";
      const fakeUnicodeFont = new FakeUnicodeFont(evaluator.xref, fontFamily);
      const resources = fakeUnicodeFont.createFontResources(lines.join(""));
      const newFont = resources.getRaw("Font");

      if (this._fieldResources.mergedResources.has("Font")) {
        const oldFont = this._fieldResources.mergedResources.get("Font");
        for (const key of newFont.getKeys()) {
          oldFont.set(key, newFont.getRaw(key));
        }
      } else {
        this._fieldResources.mergedResources.set("Font", newFont);
      }

      const fontName = fakeUnicodeFont.fontName.name;
      font = await WidgetAnnotation._getFontData(
        evaluator,
        task,
        { fontName, fontSize: 0 },
        resources
      );

      for (let i = 0, ii = encodedLines.length; i < ii; i++) {
        encodedLines[i] = stringToUTF16String(lines[i]);
      }

      const savedDefaultAppearance = Object.assign(
        Object.create(null),
        this.data.defaultAppearanceData
      );
      this.data.defaultAppearanceData.fontSize = 0;
      this.data.defaultAppearanceData.fontName = fontName;

      [defaultAppearance, fontSize, lineHeight] = this._computeFontSize(
        totalHeight - 2 * defaultPadding,
        totalWidth - 2 * defaultHPadding,
        value,
        font,
        lineCount
      );

      this.data.defaultAppearanceData = savedDefaultAppearance;
    } else {
      if (!this._isOffscreenCanvasSupported) {
        warn(
          "_getAppearance: OffscreenCanvas is not supported, annotation may not render correctly."
        );
      }

      [defaultAppearance, fontSize, lineHeight] = this._computeFontSize(
        totalHeight - 2 * defaultPadding,
        totalWidth - 2 * defaultHPadding,
        value,
        font,
        lineCount
      );
    }

    let descent = font.descent;
    if (isNaN(descent)) {
      descent = BASELINE_FACTOR * lineHeight;
    } else {
      descent = Math.max(
        BASELINE_FACTOR * lineHeight,
        Math.abs(descent) * fontSize
      );
    }

    // Take into account the space we have to compute the default vertical
    // padding.
    const defaultVPadding = Math.min(
      Math.floor((totalHeight - fontSize) / 2),
      defaultPadding
    );
    const alignment = this.data.textAlignment;

    if (this.data.multiLine) {
      return this._getMultilineAppearance(
        defaultAppearance,
        encodedLines,
        font,
        fontSize,
        totalWidth,
        totalHeight,
        alignment,
        defaultHPadding,
        defaultVPadding,
        descent,
        lineHeight,
        annotationStorage
      );
    }

    if (this.data.comb) {
      return this._getCombAppearance(
        defaultAppearance,
        font,
        encodedLines[0],
        fontSize,
        totalWidth,
        totalHeight,
        defaultHPadding,
        defaultVPadding,
        descent,
        lineHeight,
        annotationStorage
      );
    }

    const bottomPadding = defaultVPadding + descent;
    if (alignment === 0 || alignment > 2) {
      // Left alignment: nothing to do
      return (
        `/Tx BMC q ${colors}BT ` +
        defaultAppearance +
        ` 1 0 0 1 ${numberToString(defaultHPadding)} ${numberToString(
          bottomPadding
        )} Tm (${escapeString(encodedLines[0])}) Tj` +
        " ET Q EMC"
      );
    }

    const prevInfo = { shift: 0 };
    const renderedText = this._renderText(
      encodedLines[0],
      font,
      fontSize,
      totalWidth,
      alignment,
      prevInfo,
      defaultHPadding,
      bottomPadding
    );
    return (
      `/Tx BMC q ${colors}BT ` +
      defaultAppearance +
      ` 1 0 0 1 0 0 Tm ${renderedText}` +
      " ET Q EMC"
    );
  }