XFA - Add the possibily to layout and measure text
- some containers doesn't always have their 2 dimensions and those dimensions re based on contents; - so in order to measure text, we must get the glyph widths (for the xfa fonts) before starting the layout; - implement a word-wrap algorithm; - handle font change during text layout.
This commit is contained in:
		
							parent
							
								
									335d4cb2fc
								
							
						
					
					
						commit
						8eeb7ab4a3
					
				@ -857,6 +857,10 @@ class PDFDocument {
 | 
			
		||||
    return shadow(this, "xfaFaxtory", null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get isPureXfa() {
 | 
			
		||||
    return this.xfaFactory && this.xfaFactory.isValid();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get htmlForXfa() {
 | 
			
		||||
    if (this.xfaFactory) {
 | 
			
		||||
      return this.xfaFactory.getPages();
 | 
			
		||||
@ -898,8 +902,14 @@ class PDFDocument {
 | 
			
		||||
      options,
 | 
			
		||||
    });
 | 
			
		||||
    const operatorList = new OperatorList();
 | 
			
		||||
    const pdfFonts = [];
 | 
			
		||||
    const initialState = {
 | 
			
		||||
      font: null,
 | 
			
		||||
      get font() {
 | 
			
		||||
        return pdfFonts[pdfFonts.length - 1];
 | 
			
		||||
      },
 | 
			
		||||
      set font(font) {
 | 
			
		||||
        pdfFonts.push(font);
 | 
			
		||||
      },
 | 
			
		||||
      clone() {
 | 
			
		||||
        return this;
 | 
			
		||||
      },
 | 
			
		||||
@ -947,6 +957,7 @@ class PDFDocument {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
    this.xfaFactory.setFonts(pdfFonts);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get formInfo() {
 | 
			
		||||
 | 
			
		||||
@ -872,6 +872,7 @@ class Font {
 | 
			
		||||
    this.capHeight = properties.capHeight / PDF_GLYPH_SPACE_UNITS;
 | 
			
		||||
    this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
 | 
			
		||||
    this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
 | 
			
		||||
    this.lineHeight = this.ascent - this.descent;
 | 
			
		||||
    this.fontMatrix = properties.fontMatrix;
 | 
			
		||||
    this.bbox = properties.bbox;
 | 
			
		||||
    this.defaultEncoding = properties.defaultEncoding;
 | 
			
		||||
@ -2495,13 +2496,16 @@ class Font {
 | 
			
		||||
      unitsPerEm: int16(tables.head.data[18], tables.head.data[19]),
 | 
			
		||||
      yMax: int16(tables.head.data[42], tables.head.data[43]),
 | 
			
		||||
      yMin: signedInt16(tables.head.data[38], tables.head.data[39]),
 | 
			
		||||
      ascent: int16(tables.hhea.data[4], tables.hhea.data[5]),
 | 
			
		||||
      ascent: signedInt16(tables.hhea.data[4], tables.hhea.data[5]),
 | 
			
		||||
      descent: signedInt16(tables.hhea.data[6], tables.hhea.data[7]),
 | 
			
		||||
      lineGap: signedInt16(tables.hhea.data[8], tables.hhea.data[9]),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // PDF FontDescriptor metrics lie -- using data from actual font.
 | 
			
		||||
    this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
 | 
			
		||||
    this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
 | 
			
		||||
    this.lineGap = metricsOverride.lineGap / metricsOverride.unitsPerEm;
 | 
			
		||||
    this.lineHeight = this.ascent - this.descent + this.lineGap;
 | 
			
		||||
 | 
			
		||||
    // The 'post' table has glyphs names.
 | 
			
		||||
    if (tables.post) {
 | 
			
		||||
 | 
			
		||||
@ -187,13 +187,8 @@ class WorkerMessageHandler {
 | 
			
		||||
        await pdfManager.ensureDoc("checkFirstPage");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const [numPages, fingerprint, htmlForXfa] = await Promise.all([
 | 
			
		||||
        pdfManager.ensureDoc("numPages"),
 | 
			
		||||
        pdfManager.ensureDoc("fingerprint"),
 | 
			
		||||
        pdfManager.ensureDoc("htmlForXfa"),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      if (htmlForXfa) {
 | 
			
		||||
      const isPureXfa = await pdfManager.ensureDoc("isPureXfa");
 | 
			
		||||
      if (isPureXfa) {
 | 
			
		||||
        const task = new WorkerTask("loadXfaFonts");
 | 
			
		||||
        startWorkerTask(task);
 | 
			
		||||
        await pdfManager
 | 
			
		||||
@ -203,6 +198,17 @@ class WorkerMessageHandler {
 | 
			
		||||
          })
 | 
			
		||||
          .then(() => finishWorkerTask(task));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const [numPages, fingerprint] = await Promise.all([
 | 
			
		||||
        pdfManager.ensureDoc("numPages"),
 | 
			
		||||
        pdfManager.ensureDoc("fingerprint"),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      // Get htmlForXfa after numPages to avoid to create HTML twice.
 | 
			
		||||
      const htmlForXfa = isPureXfa
 | 
			
		||||
        ? await pdfManager.ensureDoc("htmlForXfa")
 | 
			
		||||
        : null;
 | 
			
		||||
 | 
			
		||||
      return { numPages, fingerprint, htmlForXfa };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,9 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { $toHTML } from "./xfa_object.js";
 | 
			
		||||
import { $fonts, $toHTML } from "./xfa_object.js";
 | 
			
		||||
import { Binder } from "./bind.js";
 | 
			
		||||
import { warn } from "../../shared/util.js";
 | 
			
		||||
import { XFAParser } from "./parser.js";
 | 
			
		||||
 | 
			
		||||
class XFAFactory {
 | 
			
		||||
@ -22,18 +23,25 @@ class XFAFactory {
 | 
			
		||||
    try {
 | 
			
		||||
      this.root = new XFAParser().parse(XFAFactory._createDocument(data));
 | 
			
		||||
      this.form = new Binder(this.root).bind();
 | 
			
		||||
      this._createPages();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log(e);
 | 
			
		||||
      warn(`XFA - an error occured during parsing and binding: ${e}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isValid() {
 | 
			
		||||
    return this.root && this.form;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _createPages() {
 | 
			
		||||
    try {
 | 
			
		||||
      this.pages = this.form[$toHTML]();
 | 
			
		||||
      this.dims = this.pages.children.map(c => {
 | 
			
		||||
        const { width, height } = c.attributes.style;
 | 
			
		||||
        return [0, 0, parseInt(width), parseInt(height)];
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      warn(`XFA - an error occured during layout: ${e}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBoundingBox(pageIndex) {
 | 
			
		||||
@ -41,9 +49,35 @@ class XFAFactory {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get numberPages() {
 | 
			
		||||
    if (!this.pages) {
 | 
			
		||||
      this._createPages();
 | 
			
		||||
    }
 | 
			
		||||
    return this.dims.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFonts(fonts) {
 | 
			
		||||
    this.form[$fonts] = Object.create(null);
 | 
			
		||||
    for (const font of fonts) {
 | 
			
		||||
      const cssFontInfo = font.cssFontInfo;
 | 
			
		||||
      const name = cssFontInfo.fontFamily;
 | 
			
		||||
      if (!this.form[$fonts][name]) {
 | 
			
		||||
        this.form[$fonts][name] = Object.create(null);
 | 
			
		||||
      }
 | 
			
		||||
      let property = "regular";
 | 
			
		||||
      if (cssFontInfo.italicAngle !== "0") {
 | 
			
		||||
        if (parseFloat(cssFontInfo.fontWeight) >= 700) {
 | 
			
		||||
          property = "bolditalic";
 | 
			
		||||
        } else {
 | 
			
		||||
          property = "italic";
 | 
			
		||||
        }
 | 
			
		||||
      } else if (parseFloat(cssFontInfo.fontWeight) >= 700) {
 | 
			
		||||
        property = "bold";
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.form[$fonts][name][property] = font;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getPages() {
 | 
			
		||||
    if (!this.pages) {
 | 
			
		||||
      this._createPages();
 | 
			
		||||
 | 
			
		||||
@ -18,18 +18,14 @@ import {
 | 
			
		||||
  $getParent,
 | 
			
		||||
  $getSubformParent,
 | 
			
		||||
  $nodeName,
 | 
			
		||||
  $pushGlyphs,
 | 
			
		||||
  $toStyle,
 | 
			
		||||
  XFAObject,
 | 
			
		||||
} from "./xfa_object.js";
 | 
			
		||||
import { getMeasurement } from "./utils.js";
 | 
			
		||||
import { TextMeasure } from "./text.js";
 | 
			
		||||
import { warn } from "../../shared/util.js";
 | 
			
		||||
 | 
			
		||||
const wordNonWordRegex = new RegExp(
 | 
			
		||||
  "([\\p{N}\\p{L}\\p{M}]+)|([^\\p{N}\\p{L}\\p{M}]+)",
 | 
			
		||||
  "gu"
 | 
			
		||||
);
 | 
			
		||||
const wordFirstRegex = new RegExp("^[\\p{N}\\p{L}\\p{M}]", "u");
 | 
			
		||||
 | 
			
		||||
function measureToString(m) {
 | 
			
		||||
  if (typeof m === "string") {
 | 
			
		||||
    return "0px";
 | 
			
		||||
@ -192,65 +188,15 @@ const converters = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function layoutText(text, fontSize, space) {
 | 
			
		||||
  // Try to guess width and height for the given text in taking into
 | 
			
		||||
  // account the space where the text should fit.
 | 
			
		||||
  // The computed dimensions are just an overestimation.
 | 
			
		||||
  // TODO: base this estimation on real metrics.
 | 
			
		||||
  let width = 0;
 | 
			
		||||
  let height = 0;
 | 
			
		||||
  let totalWidth = 0;
 | 
			
		||||
  const lineHeight = fontSize * 1.5;
 | 
			
		||||
  const averageCharSize = fontSize * 0.4;
 | 
			
		||||
  const maxCharOnLine = Math.floor(space.width / averageCharSize);
 | 
			
		||||
  const chunks = text.match(wordNonWordRegex);
 | 
			
		||||
  let treatedChars = 0;
 | 
			
		||||
 | 
			
		||||
  let i = 0;
 | 
			
		||||
  let chunk = chunks[0];
 | 
			
		||||
  while (chunk) {
 | 
			
		||||
    const w = chunk.length * averageCharSize;
 | 
			
		||||
    if (width + w <= space.width) {
 | 
			
		||||
      width += w;
 | 
			
		||||
      treatedChars += chunk.length;
 | 
			
		||||
      chunk = chunks[i++];
 | 
			
		||||
      continue;
 | 
			
		||||
function layoutText(text, xfaFont, fonts, width) {
 | 
			
		||||
  const measure = new TextMeasure(xfaFont, fonts);
 | 
			
		||||
  if (typeof text === "string") {
 | 
			
		||||
    measure.addString(text);
 | 
			
		||||
  } else {
 | 
			
		||||
    text[$pushGlyphs](measure);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (!wordFirstRegex.test(chunk) || chunk.length > maxCharOnLine) {
 | 
			
		||||
      const numOfCharOnLine = Math.floor(
 | 
			
		||||
        (space.width - width) / averageCharSize
 | 
			
		||||
      );
 | 
			
		||||
      chunk = chunk.slice(numOfCharOnLine);
 | 
			
		||||
      treatedChars += numOfCharOnLine;
 | 
			
		||||
      if (height + lineHeight > space.height) {
 | 
			
		||||
        return { width: 0, height: 0, splitPos: treatedChars };
 | 
			
		||||
      }
 | 
			
		||||
      totalWidth = Math.max(width, totalWidth);
 | 
			
		||||
      width = 0;
 | 
			
		||||
      height += lineHeight;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (height + lineHeight > space.height) {
 | 
			
		||||
      return { width: 0, height: 0, splitPos: treatedChars };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    totalWidth = Math.max(width, totalWidth);
 | 
			
		||||
    width = w;
 | 
			
		||||
    height += lineHeight;
 | 
			
		||||
    chunk = chunks[i++];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (totalWidth === 0) {
 | 
			
		||||
    totalWidth = width;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (totalWidth !== 0) {
 | 
			
		||||
    height += lineHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { width: totalWidth, height, splitPos: -1 };
 | 
			
		||||
  return measure.compute(width);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function computeBbox(node, html, availableSpace) {
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ import {
 | 
			
		||||
  $extra,
 | 
			
		||||
  $finalize,
 | 
			
		||||
  $flushHTML,
 | 
			
		||||
  $fonts,
 | 
			
		||||
  $getAvailableSpace,
 | 
			
		||||
  $getChildren,
 | 
			
		||||
  $getContainedChildren,
 | 
			
		||||
@ -1522,14 +1523,51 @@ class Draw extends XFAObject {
 | 
			
		||||
 | 
			
		||||
    fixDimensions(this);
 | 
			
		||||
 | 
			
		||||
    if (this.w !== "" && this.h === "" && this.value) {
 | 
			
		||||
    if ((this.w === "" || this.h === "") && this.value) {
 | 
			
		||||
      const maxWidth = this.w === "" ? availableSpace.width : this.w;
 | 
			
		||||
      const fonts = getRoot(this)[$fonts];
 | 
			
		||||
      let font = this.font;
 | 
			
		||||
      if (!font) {
 | 
			
		||||
        let parent = this[$getParent]();
 | 
			
		||||
        while (!(parent instanceof Template)) {
 | 
			
		||||
          if (parent.font) {
 | 
			
		||||
            font = parent.font;
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          parent = parent[$getParent]();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let height = null;
 | 
			
		||||
      let width = null;
 | 
			
		||||
      if (
 | 
			
		||||
        this.value.exData &&
 | 
			
		||||
        this.value.exData[$content] &&
 | 
			
		||||
        this.value.exData.contentType === "text/html"
 | 
			
		||||
      ) {
 | 
			
		||||
        const res = layoutText(
 | 
			
		||||
          this.value.exData[$content],
 | 
			
		||||
          font,
 | 
			
		||||
          fonts,
 | 
			
		||||
          maxWidth
 | 
			
		||||
        );
 | 
			
		||||
        width = res.width;
 | 
			
		||||
        height = res.height;
 | 
			
		||||
      } else {
 | 
			
		||||
        const text = this.value[$text]();
 | 
			
		||||
        if (text) {
 | 
			
		||||
        const { height } = layoutText(text, this.font.size, {
 | 
			
		||||
          width: this.w,
 | 
			
		||||
          height: Infinity,
 | 
			
		||||
        });
 | 
			
		||||
        this.h = height || "";
 | 
			
		||||
          const res = layoutText(text, font, fonts, maxWidth);
 | 
			
		||||
          width = res.width;
 | 
			
		||||
          height = res.height;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (width !== null && this.w === "") {
 | 
			
		||||
        this.w = width;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (height !== null && this.h === "") {
 | 
			
		||||
        this.h = height;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2623,7 +2661,7 @@ class Font extends XFAObject {
 | 
			
		||||
    ]);
 | 
			
		||||
    this.posture = getStringOption(attributes.posture, ["normal", "italic"]);
 | 
			
		||||
    this.size = getMeasurement(attributes.size, "10pt");
 | 
			
		||||
    this.typeface = attributes.typeface || "";
 | 
			
		||||
    this.typeface = attributes.typeface || "Courier";
 | 
			
		||||
    this.underline = getInteger({
 | 
			
		||||
      data: attributes.underline,
 | 
			
		||||
      defaultValue: 0,
 | 
			
		||||
@ -4484,7 +4522,6 @@ class Template extends XFAObject {
 | 
			
		||||
        children: [],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this[$extra] = {
 | 
			
		||||
      overflowNode: null,
 | 
			
		||||
      pageNumber: 1,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										218
									
								
								src/core/xfa/text.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								src/core/xfa/text.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,218 @@
 | 
			
		||||
/* Copyright 2021 Mozilla Foundation
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const WIDTH_FACTOR = 1.2;
 | 
			
		||||
const HEIGHT_FACTOR = 1.2;
 | 
			
		||||
 | 
			
		||||
class FontInfo {
 | 
			
		||||
  constructor(xfaFont, fonts) {
 | 
			
		||||
    if (!xfaFont) {
 | 
			
		||||
      [this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.xfaFont = xfaFont;
 | 
			
		||||
    let typeface = fonts[xfaFont.typeface];
 | 
			
		||||
    if (!typeface) {
 | 
			
		||||
      typeface = fonts[`${xfaFont.typeface}-PdfJS-XFA`];
 | 
			
		||||
    }
 | 
			
		||||
    if (!typeface) {
 | 
			
		||||
      [this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.pdfFont = null;
 | 
			
		||||
    if (xfaFont.posture === "italic") {
 | 
			
		||||
      if (xfaFont.weight === "bold") {
 | 
			
		||||
        this.pdfFont = typeface.bolditalic;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.pdfFont = typeface.italic;
 | 
			
		||||
      }
 | 
			
		||||
    } else if (xfaFont.weigth === "bold") {
 | 
			
		||||
      this.pdfFont = typeface.bold;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.pdfFont = typeface.regular;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.pdfFont) {
 | 
			
		||||
      [this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  defaultFont(fonts) {
 | 
			
		||||
    // TODO: Add a default font based on Liberation.
 | 
			
		||||
    const font =
 | 
			
		||||
      fonts.Helvetica ||
 | 
			
		||||
      fonts["Myriad Pro"] ||
 | 
			
		||||
      fonts.Arial ||
 | 
			
		||||
      fonts.ArialMT ||
 | 
			
		||||
      Object.values(fonts)[0];
 | 
			
		||||
    const pdfFont = font.regular;
 | 
			
		||||
    const info = this.pdfFont.cssFontInfo;
 | 
			
		||||
    const xfaFont = {
 | 
			
		||||
      typeface: info.fontFamily,
 | 
			
		||||
      posture: "normal",
 | 
			
		||||
      weight: "normal",
 | 
			
		||||
      size: 10,
 | 
			
		||||
    };
 | 
			
		||||
    return [pdfFont, xfaFont];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FontSelector {
 | 
			
		||||
  constructor(defaultXfaFont, fonts) {
 | 
			
		||||
    this.fonts = fonts;
 | 
			
		||||
    this.stack = [new FontInfo(defaultXfaFont, fonts)];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pushFont(xfaFont) {
 | 
			
		||||
    const lastFont = this.stack[this.stack.length - 1];
 | 
			
		||||
    for (const name of ["typeface", "posture", "weight", "size"]) {
 | 
			
		||||
      if (!xfaFont[name]) {
 | 
			
		||||
        xfaFont[name] = lastFont.xfaFont[name];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fontInfo = new FontInfo(xfaFont, this.fonts);
 | 
			
		||||
    if (!fontInfo.pdfFont) {
 | 
			
		||||
      fontInfo.pdfFont = lastFont.pdfFont;
 | 
			
		||||
    }
 | 
			
		||||
    this.stack.push(fontInfo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  popFont() {
 | 
			
		||||
    this.stack.pop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  topFont() {
 | 
			
		||||
    return this.stack[this.stack.length - 1];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Compute a text area dimensions based on font metrics.
 | 
			
		||||
 */
 | 
			
		||||
class TextMeasure {
 | 
			
		||||
  constructor(defaultXfaFont, fonts) {
 | 
			
		||||
    this.glyphs = [];
 | 
			
		||||
    this.fontSelector = new FontSelector(defaultXfaFont, fonts);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pushFont(xfaFont) {
 | 
			
		||||
    return this.fontSelector.pushFont(xfaFont);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  popFont(xfaFont) {
 | 
			
		||||
    return this.fontSelector.popFont();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addString(str) {
 | 
			
		||||
    if (!str) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lastFont = this.fontSelector.topFont();
 | 
			
		||||
    const pdfFont = lastFont.pdfFont;
 | 
			
		||||
    const fontSize = lastFont.xfaFont.size;
 | 
			
		||||
    const lineHeight = Math.round(Math.max(1, pdfFont.lineHeight) * fontSize);
 | 
			
		||||
    const scale = fontSize / 1000;
 | 
			
		||||
 | 
			
		||||
    for (const line of str.split(/[\u2029\n]/)) {
 | 
			
		||||
      const encodedLine = pdfFont.encodeString(line).join("");
 | 
			
		||||
      const glyphs = pdfFont.charsToGlyphs(encodedLine);
 | 
			
		||||
 | 
			
		||||
      for (const glyph of glyphs) {
 | 
			
		||||
        this.glyphs.push([
 | 
			
		||||
          glyph.width * scale,
 | 
			
		||||
          lineHeight,
 | 
			
		||||
          glyph.unicode === " ",
 | 
			
		||||
          false,
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.glyphs.push([0, 0, false, true]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.glyphs.pop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  compute(maxWidth) {
 | 
			
		||||
    let lastSpacePos = -1,
 | 
			
		||||
      lastSpaceWidth = 0,
 | 
			
		||||
      width = 0,
 | 
			
		||||
      height = 0,
 | 
			
		||||
      currentLineWidth = 0,
 | 
			
		||||
      currentLineHeight = 0;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0, ii = this.glyphs.length; i < ii; i++) {
 | 
			
		||||
      const [glyphWidth, glyphHeight, isSpace, isEOL] = this.glyphs[i];
 | 
			
		||||
      if (isEOL) {
 | 
			
		||||
        width = Math.max(width, currentLineWidth);
 | 
			
		||||
        currentLineWidth = 0;
 | 
			
		||||
        height += currentLineHeight;
 | 
			
		||||
        currentLineHeight = glyphHeight;
 | 
			
		||||
        lastSpacePos = -1;
 | 
			
		||||
        lastSpaceWidth = 0;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isSpace) {
 | 
			
		||||
        if (currentLineWidth + glyphWidth > maxWidth) {
 | 
			
		||||
          // We can break here but the space is not taken into account.
 | 
			
		||||
          width = Math.max(width, currentLineWidth);
 | 
			
		||||
          currentLineWidth = 0;
 | 
			
		||||
          height += currentLineHeight;
 | 
			
		||||
          currentLineHeight = glyphHeight;
 | 
			
		||||
          lastSpacePos = -1;
 | 
			
		||||
          lastSpaceWidth = 0;
 | 
			
		||||
        } else {
 | 
			
		||||
          currentLineHeight = Math.max(glyphHeight, currentLineHeight);
 | 
			
		||||
          lastSpaceWidth = currentLineWidth;
 | 
			
		||||
          currentLineWidth += glyphWidth;
 | 
			
		||||
          lastSpacePos = i;
 | 
			
		||||
        }
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (currentLineWidth + glyphWidth > maxWidth) {
 | 
			
		||||
        // We must break to the last white position (if available)
 | 
			
		||||
        height += currentLineHeight;
 | 
			
		||||
        currentLineHeight = glyphHeight;
 | 
			
		||||
        if (lastSpacePos !== -1) {
 | 
			
		||||
          i = lastSpacePos;
 | 
			
		||||
          width = Math.max(width, lastSpaceWidth);
 | 
			
		||||
          currentLineWidth = 0;
 | 
			
		||||
          lastSpacePos = -1;
 | 
			
		||||
          lastSpaceWidth = 0;
 | 
			
		||||
        } else {
 | 
			
		||||
          // Just break in the middle of the word
 | 
			
		||||
          width = Math.max(width, currentLineWidth);
 | 
			
		||||
          currentLineWidth = glyphWidth;
 | 
			
		||||
        }
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      currentLineWidth += glyphWidth;
 | 
			
		||||
      currentLineHeight = Math.max(glyphHeight, currentLineHeight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    width = Math.max(width, currentLineWidth);
 | 
			
		||||
    height += currentLineHeight;
 | 
			
		||||
 | 
			
		||||
    return { width: WIDTH_FACTOR * width, height: HEIGHT_FACTOR * height };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { TextMeasure };
 | 
			
		||||
@ -34,6 +34,7 @@ const $dump = Symbol();
 | 
			
		||||
const $extra = Symbol("extra");
 | 
			
		||||
const $finalize = Symbol();
 | 
			
		||||
const $flushHTML = Symbol();
 | 
			
		||||
const $fonts = Symbol();
 | 
			
		||||
const $getAttributeIt = Symbol();
 | 
			
		||||
const $getAvailableSpace = Symbol();
 | 
			
		||||
const $getChildrenByClass = Symbol();
 | 
			
		||||
@ -46,6 +47,7 @@ const $getContainedChildren = Symbol();
 | 
			
		||||
const $getNextPage = Symbol();
 | 
			
		||||
const $getSubformParent = Symbol();
 | 
			
		||||
const $getParent = Symbol();
 | 
			
		||||
const $pushGlyphs = Symbol();
 | 
			
		||||
const $global = Symbol();
 | 
			
		||||
const $hasItem = Symbol();
 | 
			
		||||
const $hasSettableValue = Symbol();
 | 
			
		||||
@ -970,6 +972,7 @@ export {
 | 
			
		||||
  $extra,
 | 
			
		||||
  $finalize,
 | 
			
		||||
  $flushHTML,
 | 
			
		||||
  $fonts,
 | 
			
		||||
  $getAttributeIt,
 | 
			
		||||
  $getAvailableSpace,
 | 
			
		||||
  $getChildren,
 | 
			
		||||
@ -998,6 +1001,7 @@ export {
 | 
			
		||||
  $onChild,
 | 
			
		||||
  $onChildCheck,
 | 
			
		||||
  $onText,
 | 
			
		||||
  $pushGlyphs,
 | 
			
		||||
  $removeChild,
 | 
			
		||||
  $resolvePrototypes,
 | 
			
		||||
  $root,
 | 
			
		||||
 | 
			
		||||
@ -18,8 +18,10 @@ import {
 | 
			
		||||
  $childrenToHTML,
 | 
			
		||||
  $content,
 | 
			
		||||
  $extra,
 | 
			
		||||
  $getChildren,
 | 
			
		||||
  $nodeName,
 | 
			
		||||
  $onText,
 | 
			
		||||
  $pushGlyphs,
 | 
			
		||||
  $text,
 | 
			
		||||
  $toHTML,
 | 
			
		||||
  XmlObject,
 | 
			
		||||
@ -167,6 +169,39 @@ class XhtmlObject extends XmlObject {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$pushGlyphs](measure) {
 | 
			
		||||
    const xfaFont = Object.create(null);
 | 
			
		||||
    for (const [key, value] of this.style
 | 
			
		||||
      .split(";")
 | 
			
		||||
      .map(s => s.split(":", 2))) {
 | 
			
		||||
      if (!key.startsWith("font-")) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      if (key === "font-family") {
 | 
			
		||||
        xfaFont.typeface = value;
 | 
			
		||||
      } else if (key === "font-size") {
 | 
			
		||||
        xfaFont.size = getMeasurement(value);
 | 
			
		||||
      } else if (key === "font-weight") {
 | 
			
		||||
        xfaFont.weight = value;
 | 
			
		||||
      } else if (key === "font-style") {
 | 
			
		||||
        xfaFont.posture = value;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    measure.pushFont(xfaFont);
 | 
			
		||||
    if (this[$content]) {
 | 
			
		||||
      measure.addString(this[$content]);
 | 
			
		||||
    } else {
 | 
			
		||||
      for (const child of this[$getChildren]()) {
 | 
			
		||||
        if (child[$nodeName] === "#text") {
 | 
			
		||||
          measure.addString(child[$content]);
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        child[$pushGlyphs](measure);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    measure.popFont();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$toHTML](availableSpace) {
 | 
			
		||||
    const children = [];
 | 
			
		||||
    this[$extra] = {
 | 
			
		||||
@ -202,6 +237,12 @@ class B extends XhtmlObject {
 | 
			
		||||
  constructor(attributes) {
 | 
			
		||||
    super(attributes, "b");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$pushGlyphs](measure) {
 | 
			
		||||
    measure.pushFont({ weight: "bold" });
 | 
			
		||||
    super[$pushGlyphs](measure);
 | 
			
		||||
    measure.popFont();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Body extends XhtmlObject {
 | 
			
		||||
@ -230,6 +271,10 @@ class Br extends XhtmlObject {
 | 
			
		||||
    return "\n";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$pushGlyphs](measure) {
 | 
			
		||||
    measure.addString("\n");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$toHTML](availableSpace) {
 | 
			
		||||
    return HTMLResult.success({
 | 
			
		||||
      name: "br",
 | 
			
		||||
@ -282,6 +327,12 @@ class I extends XhtmlObject {
 | 
			
		||||
  constructor(attributes) {
 | 
			
		||||
    super(attributes, "i");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$pushGlyphs](measure) {
 | 
			
		||||
    measure.pushFont({ posture: "italic" });
 | 
			
		||||
    super[$pushGlyphs](measure);
 | 
			
		||||
    measure.popFont();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Li extends XhtmlObject {
 | 
			
		||||
@ -301,6 +352,11 @@ class P extends XhtmlObject {
 | 
			
		||||
    super(attributes, "p");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$pushGlyphs](measure) {
 | 
			
		||||
    super[$pushGlyphs](measure);
 | 
			
		||||
    measure.addString("\n");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [$text]() {
 | 
			
		||||
    return super[$text]() + "\n";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								test/pdfs/xfa_issue13500.pdf.link
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/pdfs/xfa_issue13500.pdf.link
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
https://github.com/mozilla/pdf.js/files/6602628/Acrobat.pdf
 | 
			
		||||
@ -5341,5 +5341,13 @@
 | 
			
		||||
      "type": "eq",
 | 
			
		||||
      "forms": true,
 | 
			
		||||
      "lastPage": 1
 | 
			
		||||
    },
 | 
			
		||||
    {  "id": "xfa_issue13500",
 | 
			
		||||
       "file": "pdfs/xfa_issue13500.pdf",
 | 
			
		||||
       "md5": "b81274a19f5a95c1466db3648f1be491",
 | 
			
		||||
       "link": true,
 | 
			
		||||
       "rounds": 1,
 | 
			
		||||
       "enableXfa": true,
 | 
			
		||||
       "type": "eq"
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -188,7 +188,7 @@ describe("XFAFactory", function () {
 | 
			
		||||
        </pageArea>
 | 
			
		||||
      </pageSet>
 | 
			
		||||
      <subform name="first">
 | 
			
		||||
        <draw><value><text>foo</text></value></draw>
 | 
			
		||||
        <draw w="1pt" h="1pt"><value><text>foo</text></value></draw>
 | 
			
		||||
      </subform>
 | 
			
		||||
    </subform>
 | 
			
		||||
  </template>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user