[edition] Add support for saving a newly added FreeText
This commit is contained in:
		
							parent
							
								
									1816b5e926
								
							
						
					
					
						commit
						7773b3f5be
					
				@ -16,6 +16,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  AnnotationActionEventType,
 | 
			
		||||
  AnnotationBorderStyleType,
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  AnnotationFieldFlag,
 | 
			
		||||
  AnnotationFlag,
 | 
			
		||||
  AnnotationReplyType,
 | 
			
		||||
@ -24,12 +25,14 @@ import {
 | 
			
		||||
  escapeString,
 | 
			
		||||
  getModificationDate,
 | 
			
		||||
  isAscii,
 | 
			
		||||
  LINE_DESCENT_FACTOR,
 | 
			
		||||
  LINE_FACTOR,
 | 
			
		||||
  OPS,
 | 
			
		||||
  RenderingIntentFlag,
 | 
			
		||||
  shadow,
 | 
			
		||||
  stringToPDFString,
 | 
			
		||||
  stringToUTF16BEString,
 | 
			
		||||
  stringToUTF8String,
 | 
			
		||||
  unreachable,
 | 
			
		||||
  Util,
 | 
			
		||||
  warn,
 | 
			
		||||
@ -45,6 +48,7 @@ import {
 | 
			
		||||
  parseDefaultAppearance,
 | 
			
		||||
} from "./default_appearance.js";
 | 
			
		||||
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
 | 
			
		||||
import { writeDict, writeObject } from "./writer.js";
 | 
			
		||||
import { BaseStream } from "./base_stream.js";
 | 
			
		||||
import { bidi } from "./bidi.js";
 | 
			
		||||
import { Catalog } from "./catalog.js";
 | 
			
		||||
@ -53,7 +57,6 @@ import { FileSpec } from "./file_spec.js";
 | 
			
		||||
import { ObjectLoader } from "./object_loader.js";
 | 
			
		||||
import { OperatorList } from "./operator_list.js";
 | 
			
		||||
import { StringStream } from "./stream.js";
 | 
			
		||||
import { writeDict } from "./writer.js";
 | 
			
		||||
import { XFAFactory } from "./xfa/factory.js";
 | 
			
		||||
 | 
			
		||||
class AnnotationFactory {
 | 
			
		||||
@ -237,6 +240,49 @@ class AnnotationFactory {
 | 
			
		||||
      return -1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async saveNewAnnotations(evaluator, task, annotations) {
 | 
			
		||||
    const xref = evaluator.xref;
 | 
			
		||||
    let baseFontRef;
 | 
			
		||||
    const results = [];
 | 
			
		||||
    const dependencies = [];
 | 
			
		||||
    const promises = [];
 | 
			
		||||
    for (const annotation of annotations) {
 | 
			
		||||
      switch (annotation.annotationType) {
 | 
			
		||||
        case AnnotationEditorType.FREETEXT:
 | 
			
		||||
          if (!baseFontRef) {
 | 
			
		||||
            const baseFont = new Dict(xref);
 | 
			
		||||
            baseFont.set("BaseFont", Name.get("Helvetica"));
 | 
			
		||||
            baseFont.set("Type", Name.get("Font"));
 | 
			
		||||
            baseFont.set("Subtype", Name.get("Type1"));
 | 
			
		||||
            baseFont.set("Encoding", Name.get("WinAnsiEncoding"));
 | 
			
		||||
            const buffer = [];
 | 
			
		||||
            baseFontRef = xref.getNewRef();
 | 
			
		||||
            writeObject(baseFontRef, baseFont, buffer, null);
 | 
			
		||||
            dependencies.push({ ref: baseFontRef, data: buffer.join("") });
 | 
			
		||||
          }
 | 
			
		||||
          promises.push(
 | 
			
		||||
            FreeTextAnnotation.createNewAnnotation(
 | 
			
		||||
              xref,
 | 
			
		||||
              evaluator,
 | 
			
		||||
              task,
 | 
			
		||||
              annotation,
 | 
			
		||||
              baseFontRef,
 | 
			
		||||
              results,
 | 
			
		||||
              dependencies
 | 
			
		||||
            )
 | 
			
		||||
          );
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      annotations: results,
 | 
			
		||||
      dependencies,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) {
 | 
			
		||||
@ -1617,7 +1663,12 @@ class WidgetAnnotation extends Annotation {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const font = await this._getFontData(evaluator, task);
 | 
			
		||||
    const font = await WidgetAnnotation._getFontData(
 | 
			
		||||
      evaluator,
 | 
			
		||||
      task,
 | 
			
		||||
      this.data.defaultAppearanceData,
 | 
			
		||||
      this._fieldResources.mergedResources
 | 
			
		||||
    );
 | 
			
		||||
    const [defaultAppearance, fontSize] = this._computeFontSize(
 | 
			
		||||
      totalHeight - defaultPadding,
 | 
			
		||||
      totalWidth - 2 * hPadding,
 | 
			
		||||
@ -1700,7 +1751,7 @@ class WidgetAnnotation extends Annotation {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async _getFontData(evaluator, task) {
 | 
			
		||||
  static async _getFontData(evaluator, task, appearanceData, resources) {
 | 
			
		||||
    const operatorList = new OperatorList();
 | 
			
		||||
    const initialState = {
 | 
			
		||||
      font: null,
 | 
			
		||||
@ -1709,9 +1760,9 @@ class WidgetAnnotation extends Annotation {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { fontName, fontSize } = this.data.defaultAppearanceData;
 | 
			
		||||
    const { fontName, fontSize } = appearanceData;
 | 
			
		||||
    await evaluator.handleSetFont(
 | 
			
		||||
      this._fieldResources.mergedResources,
 | 
			
		||||
      resources,
 | 
			
		||||
      [fontName && Name.get(fontName), fontSize],
 | 
			
		||||
      /* fontRef = */ null,
 | 
			
		||||
      operatorList,
 | 
			
		||||
@ -2640,7 +2691,12 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const font = await this._getFontData(evaluator, task);
 | 
			
		||||
    const font = await WidgetAnnotation._getFontData(
 | 
			
		||||
      evaluator,
 | 
			
		||||
      task,
 | 
			
		||||
      this.data.defaultAppearanceData,
 | 
			
		||||
      this._fieldResources.mergedResources
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let defaultAppearance;
 | 
			
		||||
    let { fontSize } = this.data.defaultAppearanceData;
 | 
			
		||||
@ -2871,6 +2927,129 @@ class FreeTextAnnotation extends MarkupAnnotation {
 | 
			
		||||
 | 
			
		||||
    this.data.annotationType = AnnotationType.FREETEXT;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async createNewAnnotation(
 | 
			
		||||
    xref,
 | 
			
		||||
    evaluator,
 | 
			
		||||
    task,
 | 
			
		||||
    annotation,
 | 
			
		||||
    baseFontRef,
 | 
			
		||||
    results,
 | 
			
		||||
    dependencies
 | 
			
		||||
  ) {
 | 
			
		||||
    const { color, fontSize, rect, user, value } = annotation;
 | 
			
		||||
    const freetextRef = xref.getNewRef();
 | 
			
		||||
    const freetext = new Dict(xref);
 | 
			
		||||
    freetext.set("Type", Name.get("Annot"));
 | 
			
		||||
    freetext.set("Subtype", Name.get("FreeText"));
 | 
			
		||||
    freetext.set("CreationDate", `D:${getModificationDate()}`);
 | 
			
		||||
    freetext.set("Rect", rect);
 | 
			
		||||
    const da = `/Helv ${fontSize} Tf ${getPdfColor(color)}`;
 | 
			
		||||
    freetext.set("DA", da);
 | 
			
		||||
    freetext.set("Contents", value);
 | 
			
		||||
    freetext.set("F", 4);
 | 
			
		||||
    freetext.set("Border", [0, 0, 0]);
 | 
			
		||||
    freetext.set("Rotate", 0);
 | 
			
		||||
 | 
			
		||||
    if (user) {
 | 
			
		||||
      freetext.set("T", stringToUTF8String(user));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const resources = new Dict(xref);
 | 
			
		||||
    const font = new Dict(xref);
 | 
			
		||||
    font.set("Helv", baseFontRef);
 | 
			
		||||
    resources.set("Font", font);
 | 
			
		||||
 | 
			
		||||
    const helv = await WidgetAnnotation._getFontData(
 | 
			
		||||
      evaluator,
 | 
			
		||||
      task,
 | 
			
		||||
      {
 | 
			
		||||
        fontName: "Helvetica",
 | 
			
		||||
        fontSize,
 | 
			
		||||
      },
 | 
			
		||||
      resources
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [x1, y1, x2, y2] = rect;
 | 
			
		||||
    const w = x2 - x1;
 | 
			
		||||
    const h = y2 - y1;
 | 
			
		||||
 | 
			
		||||
    const lines = value.split("\n");
 | 
			
		||||
    const scale = fontSize / 1000;
 | 
			
		||||
    let totalWidth = -Infinity;
 | 
			
		||||
    const encodedLines = [];
 | 
			
		||||
    for (let line of lines) {
 | 
			
		||||
      line = helv.encodeString(line).join("");
 | 
			
		||||
      encodedLines.push(line);
 | 
			
		||||
      let lineWidth = 0;
 | 
			
		||||
      const glyphs = helv.charsToGlyphs(line);
 | 
			
		||||
      for (const glyph of glyphs) {
 | 
			
		||||
        lineWidth += glyph.width * scale;
 | 
			
		||||
      }
 | 
			
		||||
      totalWidth = Math.max(totalWidth, lineWidth);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let hscale = 1;
 | 
			
		||||
    if (totalWidth > w) {
 | 
			
		||||
      hscale = w / totalWidth;
 | 
			
		||||
    }
 | 
			
		||||
    let vscale = 1;
 | 
			
		||||
    const lineHeight = LINE_FACTOR * fontSize;
 | 
			
		||||
    const lineDescent = LINE_DESCENT_FACTOR * fontSize;
 | 
			
		||||
    const totalHeight = lineHeight * lines.length;
 | 
			
		||||
    if (totalHeight > h) {
 | 
			
		||||
      vscale = h / totalHeight;
 | 
			
		||||
    }
 | 
			
		||||
    const fscale = Math.min(hscale, vscale);
 | 
			
		||||
    const newFontSize = fontSize * fscale;
 | 
			
		||||
    const buffer = [
 | 
			
		||||
      "q",
 | 
			
		||||
      `0 0 ${numberToString(w)} ${numberToString(h)} re W n`,
 | 
			
		||||
      `BT`,
 | 
			
		||||
      `1 0 0 1 0 ${numberToString(h + lineDescent)} Tm 0 Tc ${getPdfColor(
 | 
			
		||||
        color
 | 
			
		||||
      )}`,
 | 
			
		||||
      `/Helv ${numberToString(newFontSize)} Tf`,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const vShift = numberToString(lineHeight);
 | 
			
		||||
    for (const line of encodedLines) {
 | 
			
		||||
      buffer.push(`0 -${vShift} Td (${escapeString(line)}) Tj`);
 | 
			
		||||
    }
 | 
			
		||||
    buffer.push("ET", "Q");
 | 
			
		||||
    const appearance = buffer.join("\n");
 | 
			
		||||
 | 
			
		||||
    const appearanceStreamDict = new Dict(xref);
 | 
			
		||||
    appearanceStreamDict.set("FormType", 1);
 | 
			
		||||
    appearanceStreamDict.set("Subtype", Name.get("Form"));
 | 
			
		||||
    appearanceStreamDict.set("Type", Name.get("XObject"));
 | 
			
		||||
    appearanceStreamDict.set("BBox", [0, 0, w, h]);
 | 
			
		||||
    appearanceStreamDict.set("Length", appearance.length);
 | 
			
		||||
    appearanceStreamDict.set("Resources", resources);
 | 
			
		||||
 | 
			
		||||
    const ap = new StringStream(appearance);
 | 
			
		||||
    ap.dict = appearanceStreamDict;
 | 
			
		||||
 | 
			
		||||
    buffer.length = 0;
 | 
			
		||||
    const apRef = xref.getNewRef();
 | 
			
		||||
    let transform = xref.encrypt
 | 
			
		||||
      ? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
 | 
			
		||||
      : null;
 | 
			
		||||
    writeObject(apRef, ap, buffer, transform);
 | 
			
		||||
    dependencies.push({ ref: apRef, data: buffer.join("") });
 | 
			
		||||
 | 
			
		||||
    const n = new Dict(xref);
 | 
			
		||||
    n.set("N", apRef);
 | 
			
		||||
    freetext.set("AP", n);
 | 
			
		||||
 | 
			
		||||
    buffer.length = 0;
 | 
			
		||||
    transform = xref.encrypt
 | 
			
		||||
      ? xref.encrypt.createCipherTransform(freetextRef.num, freetextRef.gen)
 | 
			
		||||
      : null;
 | 
			
		||||
    writeObject(freetextRef, freetext, buffer, transform);
 | 
			
		||||
 | 
			
		||||
    results.push({ ref: freetextRef, data: buffer.join("") });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class LineAnnotation extends MarkupAnnotation {
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,7 @@ import { OperatorList } from "./operator_list.js";
 | 
			
		||||
import { PartialEvaluator } from "./evaluator.js";
 | 
			
		||||
import { StreamsSequenceStream } from "./decode_stream.js";
 | 
			
		||||
import { StructTreePage } from "./struct_tree.js";
 | 
			
		||||
import { writeObject } from "./writer.js";
 | 
			
		||||
import { XFAFactory } from "./xfa/factory.js";
 | 
			
		||||
import { XRef } from "./xref.js";
 | 
			
		||||
 | 
			
		||||
@ -261,6 +262,60 @@ class Page {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async saveNewAnnotations(handler, task, annotations) {
 | 
			
		||||
    if (this.xfaFactory) {
 | 
			
		||||
      throw new Error("XFA: Cannot save new annotations.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const partialEvaluator = new PartialEvaluator({
 | 
			
		||||
      xref: this.xref,
 | 
			
		||||
      handler,
 | 
			
		||||
      pageIndex: this.pageIndex,
 | 
			
		||||
      idFactory: this._localIdFactory,
 | 
			
		||||
      fontCache: this.fontCache,
 | 
			
		||||
      builtInCMapCache: this.builtInCMapCache,
 | 
			
		||||
      standardFontDataCache: this.standardFontDataCache,
 | 
			
		||||
      globalImageCache: this.globalImageCache,
 | 
			
		||||
      options: this.evaluatorOptions,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const pageDict = this.pageDict;
 | 
			
		||||
    const annotationsArray = this.annotations.slice();
 | 
			
		||||
    const newData = await AnnotationFactory.saveNewAnnotations(
 | 
			
		||||
      partialEvaluator,
 | 
			
		||||
      task,
 | 
			
		||||
      annotations
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (const { ref } of newData.annotations) {
 | 
			
		||||
      annotationsArray.push(ref);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const savedDict = pageDict.get("Annots");
 | 
			
		||||
    pageDict.set("Annots", annotationsArray);
 | 
			
		||||
    const buffer = [];
 | 
			
		||||
 | 
			
		||||
    let transform = null;
 | 
			
		||||
    if (this.xref.encrypt) {
 | 
			
		||||
      transform = this.xref.encrypt.createCipherTransform(
 | 
			
		||||
        this.ref.num,
 | 
			
		||||
        this.ref.gen
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    writeObject(this.ref, pageDict, buffer, transform);
 | 
			
		||||
    if (savedDict) {
 | 
			
		||||
      pageDict.set("Annots", savedDict);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const objects = newData.dependencies;
 | 
			
		||||
    objects.push(
 | 
			
		||||
      { ref: this.ref, data: buffer.join("") },
 | 
			
		||||
      ...newData.annotations
 | 
			
		||||
    );
 | 
			
		||||
    return objects;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  save(handler, task, annotationStorage) {
 | 
			
		||||
    const partialEvaluator = new PartialEvaluator({
 | 
			
		||||
      xref: this.xref,
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AbortException,
 | 
			
		||||
  AnnotationEditorPrefix,
 | 
			
		||||
  arrayByteLength,
 | 
			
		||||
  arraysToBytes,
 | 
			
		||||
  createPromiseCapability,
 | 
			
		||||
@ -557,6 +558,23 @@ class WorkerMessageHandler {
 | 
			
		||||
      function ({ isPureXfa, numPages, annotationStorage, filename }) {
 | 
			
		||||
        pdfManager.requestLoadedStream();
 | 
			
		||||
 | 
			
		||||
        const newAnnotationsByPage = new Map();
 | 
			
		||||
        if (!isPureXfa) {
 | 
			
		||||
          // The concept of page in a XFA is very different, so
 | 
			
		||||
          // editing is just not implemented.
 | 
			
		||||
          for (const [key, value] of annotationStorage) {
 | 
			
		||||
            if (!key.startsWith(AnnotationEditorPrefix)) {
 | 
			
		||||
              continue;
 | 
			
		||||
            }
 | 
			
		||||
            let annotations = newAnnotationsByPage.get(value.pageIndex);
 | 
			
		||||
            if (!annotations) {
 | 
			
		||||
              annotations = [];
 | 
			
		||||
              newAnnotationsByPage.set(value.pageIndex, annotations);
 | 
			
		||||
            }
 | 
			
		||||
            annotations.push(value);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const promises = [
 | 
			
		||||
          pdfManager.onLoadedStream(),
 | 
			
		||||
          pdfManager.ensureCatalog("acroForm"),
 | 
			
		||||
@ -565,6 +583,19 @@ class WorkerMessageHandler {
 | 
			
		||||
          pdfManager.ensureDoc("startXRef"),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        for (const [pageIndex, annotations] of newAnnotationsByPage) {
 | 
			
		||||
          promises.push(
 | 
			
		||||
            pdfManager.getPage(pageIndex).then(page => {
 | 
			
		||||
              const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
 | 
			
		||||
              return page
 | 
			
		||||
                .saveNewAnnotations(handler, task, annotations)
 | 
			
		||||
                .finally(function () {
 | 
			
		||||
                  finishWorkerTask(task);
 | 
			
		||||
                });
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isPureXfa) {
 | 
			
		||||
          promises.push(pdfManager.serializeXfaData(annotationStorage));
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,16 @@ import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js";
 | 
			
		||||
import { BaseStream } from "./base_stream.js";
 | 
			
		||||
import { calculateMD5 } from "./crypto.js";
 | 
			
		||||
 | 
			
		||||
function writeObject(ref, obj, buffer, transform) {
 | 
			
		||||
  buffer.push(`${ref.num} ${ref.gen} obj\n`);
 | 
			
		||||
  if (obj instanceof Dict) {
 | 
			
		||||
    writeDict(obj, buffer, transform);
 | 
			
		||||
  } else if (obj instanceof BaseStream) {
 | 
			
		||||
    writeStream(obj, buffer, transform);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.push("\nendobj\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function writeDict(dict, buffer, transform) {
 | 
			
		||||
  buffer.push("<<");
 | 
			
		||||
  for (const key of dict.getKeys()) {
 | 
			
		||||
@ -328,4 +338,4 @@ function incrementalUpdate({
 | 
			
		||||
  return array;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { incrementalUpdate, writeDict };
 | 
			
		||||
export { incrementalUpdate, writeDict, writeObject };
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ class XRef {
 | 
			
		||||
 | 
			
		||||
  getNewRef() {
 | 
			
		||||
    if (this._newRefNum === null) {
 | 
			
		||||
      this._newRefNum = this.entries.length;
 | 
			
		||||
      this._newRefNum = this.entries.length || 1;
 | 
			
		||||
    }
 | 
			
		||||
    return Ref.get(this._newRefNum++, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ class AnnotationEditorLayer {
 | 
			
		||||
 | 
			
		||||
  #uiManager;
 | 
			
		||||
 | 
			
		||||
  static _l10nInitialized = false;
 | 
			
		||||
  static _initialized = false;
 | 
			
		||||
 | 
			
		||||
  static _keyboardManager = new KeyboardManager([
 | 
			
		||||
    [["ctrl+a", "mac+meta+a"], AnnotationEditorLayer.prototype.selectAll],
 | 
			
		||||
@ -73,9 +73,9 @@ class AnnotationEditorLayer {
 | 
			
		||||
   * @param {AnnotationEditorLayerOptions} options
 | 
			
		||||
   */
 | 
			
		||||
  constructor(options) {
 | 
			
		||||
    if (!AnnotationEditorLayer._l10nInitialized) {
 | 
			
		||||
      AnnotationEditorLayer._l10nInitialized = true;
 | 
			
		||||
      FreeTextEditor.setL10n(options.l10n);
 | 
			
		||||
    if (!AnnotationEditorLayer._initialized) {
 | 
			
		||||
      AnnotationEditorLayer._initialized = true;
 | 
			
		||||
      FreeTextEditor.initialize(options.l10n);
 | 
			
		||||
    }
 | 
			
		||||
    this.#uiManager = options.uiManager;
 | 
			
		||||
    this.annotationStorage = options.annotationStorage;
 | 
			
		||||
 | 
			
		||||
@ -140,6 +140,14 @@ class AnnotationEditor {
 | 
			
		||||
    this.div.style.height = `${height}px`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the translation used to position this editor when it's created.
 | 
			
		||||
   * @returns {Array<number>}
 | 
			
		||||
   */
 | 
			
		||||
  getInitialTranslation() {
 | 
			
		||||
    return [0, 0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render this editor in a div.
 | 
			
		||||
   * @returns {HTMLDivElement}
 | 
			
		||||
@ -150,6 +158,11 @@ class AnnotationEditor {
 | 
			
		||||
    this.div.setAttribute("id", this.id);
 | 
			
		||||
    this.div.draggable = true;
 | 
			
		||||
    this.div.tabIndex = 100;
 | 
			
		||||
 | 
			
		||||
    const [tx, ty] = this.getInitialTranslation();
 | 
			
		||||
    this.x = Math.round(this.x + tx);
 | 
			
		||||
    this.y = Math.round(this.y + ty);
 | 
			
		||||
 | 
			
		||||
    this.div.style.left = `${this.x}px`;
 | 
			
		||||
    this.div.style.top = `${this.y}px`;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,9 +13,15 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { AnnotationEditorType, Util } from "../../shared/util.js";
 | 
			
		||||
import {
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  assert,
 | 
			
		||||
  LINE_FACTOR,
 | 
			
		||||
  Util,
 | 
			
		||||
} from "../../shared/util.js";
 | 
			
		||||
import { AnnotationEditor } from "./editor.js";
 | 
			
		||||
import { bindEvents } from "./tools.js";
 | 
			
		||||
import { PixelsPerInch } from "../display_utils.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Basic text editor in order to create a FreeTex annotation.
 | 
			
		||||
@ -33,14 +39,36 @@ class FreeTextEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
  static _l10nPromise;
 | 
			
		||||
 | 
			
		||||
  static _internalPadding = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(params) {
 | 
			
		||||
    super({ ...params, name: "freeTextEditor" });
 | 
			
		||||
    this.#color = params.color || "CanvasText";
 | 
			
		||||
    this.#fontSize = params.fontSize || 10;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static setL10n(l10n) {
 | 
			
		||||
  static initialize(l10n) {
 | 
			
		||||
    this._l10nPromise = l10n.get("freetext_default_content");
 | 
			
		||||
    const style = getComputedStyle(document.documentElement);
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      typeof PDFJSDev === "undefined" ||
 | 
			
		||||
      PDFJSDev.test("!PRODUCTION || TESTING")
 | 
			
		||||
    ) {
 | 
			
		||||
      const lineHeight = parseFloat(
 | 
			
		||||
        style.getPropertyValue("--freetext-line-height"),
 | 
			
		||||
        10
 | 
			
		||||
      );
 | 
			
		||||
      assert(
 | 
			
		||||
        lineHeight === LINE_FACTOR,
 | 
			
		||||
        "Update the CSS variable to agree with the constant."
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._internalPadding = parseFloat(
 | 
			
		||||
      style.getPropertyValue("--freetext-padding"),
 | 
			
		||||
      10
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
@ -62,6 +90,16 @@ class FreeTextEditor extends AnnotationEditor {
 | 
			
		||||
    return editor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  getInitialTranslation() {
 | 
			
		||||
    // The start of the base line is where the user clicked.
 | 
			
		||||
    return [
 | 
			
		||||
      -FreeTextEditor._internalPadding * this.parent.zoomFactor,
 | 
			
		||||
      -(FreeTextEditor._internalPadding + this.#fontSize) *
 | 
			
		||||
        this.parent.zoomFactor,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  rebuild() {
 | 
			
		||||
    if (this.div === null) {
 | 
			
		||||
@ -174,7 +212,6 @@ class FreeTextEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
    const { style } = this.editorDiv;
 | 
			
		||||
    style.fontSize = `calc(${this.#fontSize}px * var(--zoom-factor))`;
 | 
			
		||||
    style.minHeight = `calc(${1.5 * this.#fontSize}px * var(--zoom-factor))`;
 | 
			
		||||
    style.color = this.#color;
 | 
			
		||||
 | 
			
		||||
    this.div.appendChild(this.editorDiv);
 | 
			
		||||
@ -200,21 +237,21 @@ class FreeTextEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  serialize() {
 | 
			
		||||
    const rect = this.div.getBoundingClientRect();
 | 
			
		||||
    const rect = this.editorDiv.getBoundingClientRect();
 | 
			
		||||
    const padding = FreeTextEditor._internalPadding * this.parent.zoomFactor;
 | 
			
		||||
    const [x1, y1] = Util.applyTransform(
 | 
			
		||||
      [this.x, this.y + rect.height],
 | 
			
		||||
      [this.x + padding, this.y + padding + rect.height],
 | 
			
		||||
      this.parent.inverseViewportTransform
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [x2, y2] = Util.applyTransform(
 | 
			
		||||
      [this.x + rect.width, this.y],
 | 
			
		||||
      [this.x + padding + rect.width, this.y + padding],
 | 
			
		||||
      this.parent.inverseViewportTransform
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      annotationType: AnnotationEditorType.FREETEXT,
 | 
			
		||||
      color: [0, 0, 0],
 | 
			
		||||
      fontSize: this.#fontSize,
 | 
			
		||||
      fontSize: this.#fontSize / PixelsPerInch.PDF_TO_CSS_UNITS,
 | 
			
		||||
      value: this.#content,
 | 
			
		||||
      pageIndex: this.parent.pageIndex,
 | 
			
		||||
      rect: [x1, y1, x2, y2],
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
 | 
			
		||||
// Represent the percentage of the height of a single-line field over
 | 
			
		||||
// the font size. Acrobat seems to use this value.
 | 
			
		||||
const LINE_FACTOR = 1.35;
 | 
			
		||||
const LINE_DESCENT_FACTOR = 0.35;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
 | 
			
		||||
@ -1175,6 +1176,7 @@ export {
 | 
			
		||||
  isArrayBuffer,
 | 
			
		||||
  isArrayEqual,
 | 
			
		||||
  isAscii,
 | 
			
		||||
  LINE_DESCENT_FACTOR,
 | 
			
		||||
  LINE_FACTOR,
 | 
			
		||||
  MissingPDFException,
 | 
			
		||||
  objectFromMap,
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ import {
 | 
			
		||||
} from "../../src/core/annotation.js";
 | 
			
		||||
import {
 | 
			
		||||
  AnnotationBorderStyleType,
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  AnnotationFieldFlag,
 | 
			
		||||
  AnnotationFlag,
 | 
			
		||||
  AnnotationType,
 | 
			
		||||
@ -3857,6 +3858,61 @@ describe("annotation", function () {
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("FreeTextAnnotation", () => {
 | 
			
		||||
    it("should create an new FreeText annotation", async () => {
 | 
			
		||||
      partialEvaluator.xref = new XRefMock();
 | 
			
		||||
      const task = new WorkerTask("test FreeText creation");
 | 
			
		||||
      const data = await AnnotationFactory.saveNewAnnotations(
 | 
			
		||||
        partialEvaluator,
 | 
			
		||||
        task,
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            annotationType: AnnotationEditorType.FREETEXT,
 | 
			
		||||
            rect: [12, 34, 56, 78],
 | 
			
		||||
            fontSize: 10,
 | 
			
		||||
            color: [0, 0, 0],
 | 
			
		||||
            value: "Hello PDF.js World!",
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
 | 
			
		||||
      expect(base).toEqual(
 | 
			
		||||
        "2 0 obj\n" +
 | 
			
		||||
          "<< /Type /Annot /Subtype /FreeText /CreationDate (date) " +
 | 
			
		||||
          "/Rect [12 34 56 78] /DA (/Helv 10 Tf 0 g) /Contents (Hello PDF.js World!) " +
 | 
			
		||||
          "/F 4 /Border [0 0 0] /Rotate 0 /AP << /N 3 0 R>>>>\n" +
 | 
			
		||||
          "endobj\n"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const font = data.dependencies[0].data;
 | 
			
		||||
      expect(font).toEqual(
 | 
			
		||||
        "1 0 obj\n" +
 | 
			
		||||
          "<< /BaseFont /Helvetica /Type /Font /Subtype /Type1 /Encoding " +
 | 
			
		||||
          "/WinAnsiEncoding>>\n" +
 | 
			
		||||
          "endobj\n"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const appearance = data.dependencies[1].data;
 | 
			
		||||
      expect(appearance).toEqual(
 | 
			
		||||
        "3 0 obj\n" +
 | 
			
		||||
          "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] " +
 | 
			
		||||
          "/Length 101 /Resources << /Font << /Helv 1 0 R>>>>>> stream\n" +
 | 
			
		||||
          "q\n" +
 | 
			
		||||
          "0 0 44 44 re W n\n" +
 | 
			
		||||
          "BT\n" +
 | 
			
		||||
          "1 0 0 1 0 47.5 Tm 0 Tc 0 g\n" +
 | 
			
		||||
          "/Helv 10 Tf\n" +
 | 
			
		||||
          "0 -13.5 Td (Hello PDF.js World!) Tj\n" +
 | 
			
		||||
          "ET\n" +
 | 
			
		||||
          "Q\n" +
 | 
			
		||||
          "endstream\n" +
 | 
			
		||||
          "\n" +
 | 
			
		||||
          "endobj\n"
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("InkAnnotation", function () {
 | 
			
		||||
    it("should handle a single ink list", async function () {
 | 
			
		||||
      const inkDict = new Dict();
 | 
			
		||||
 | 
			
		||||
@ -88,7 +88,7 @@ class XRefMock {
 | 
			
		||||
 | 
			
		||||
  getNewRef() {
 | 
			
		||||
    if (this._newRefNum === null) {
 | 
			
		||||
      this._newRefNum = Object.keys(this._map).length;
 | 
			
		||||
      this._newRefNum = Object.keys(this._map).length || 1;
 | 
			
		||||
    }
 | 
			
		||||
    return Ref.get(this._newRefNum++, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,8 @@
 | 
			
		||||
:root {
 | 
			
		||||
  --focus-outline: solid 2px red;
 | 
			
		||||
  --hover-outline: dashed 2px blue;
 | 
			
		||||
  --freetext-line-height: 1.35;
 | 
			
		||||
  --freetext-padding: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer {
 | 
			
		||||
@ -31,7 +33,7 @@
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  padding: calc(var(--freetext-padding) * var(--zoom-factor));
 | 
			
		||||
  resize: none;
 | 
			
		||||
  width: auto;
 | 
			
		||||
  height: auto;
 | 
			
		||||
@ -42,10 +44,11 @@
 | 
			
		||||
  border: none;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  min-height: 15px;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  resize: none;
 | 
			
		||||
  font: 10px sans-serif;
 | 
			
		||||
  line-height: var(--freetext-line-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor .overlay {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user