Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 174 additions & 13 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2418,6 +2418,12 @@ class PartialEvaluator {
spaceInFlowMin: 0,
spaceInFlowMax: 0,
trackingSpaceMin: Infinity,
useCharWidthThreshold: false,
wideSpaceMax: Infinity,
minAdvance: Infinity,
pendingSpaceIndex: -1,
pendingSpaceAdvance: 0,
pendingSpaceExtraCharId: -1,
negativeSpaceMax: -Infinity,
notASpace: -Infinity,
transform: null,
Expand Down Expand Up @@ -2483,21 +2489,37 @@ class PartialEvaluator {
// even if one is present in the text stream.
const NOT_A_SPACE_FACTOR = 0.03;

// A reported space wider than fontSize * MAX_SPACE_WIDTH_FACTOR is treated
// as unreliable metrics, so trackingSpaceMin isn't derived from it.
const MAX_SPACE_WIDTH_FACTOR = 1 / 3;

// A negative white < fontSize * NEGATIVE_SPACE_FACTOR induces
// a break (a new chunk of text is created).
// It doesn't change anything when the text is copied but
// it improves potential mismatch between text layer and canvas.
const NEGATIVE_SPACE_FACTOR = -0.2;

// A white with a width in [fontSize * MIN_FACTOR; fontSize * MAX_FACTOR]
// is a space which will be inserted in the current flow of words.
// A white with a width in [trackingSpaceMin; fontSize * MAX_FACTOR] is a
// space which will be inserted in the current flow of words.
// If the width is outside of this range then the flow is broken
// (which means a new span in the text layer).
// It's useful to adjust the best as possible the span in the layer
// to what is displayed in the canvas.
const SPACE_IN_FLOW_MIN_FACTOR = 0.102;
const SPACE_IN_FLOW_MAX_FACTOR = 0.6;

// When every gap within a run is wider than the space threshold (e.g.
// letter-spaced text), the threshold is raised to this multiple of the
// smallest gap, capped at fontSize * WIDE_SPACE_MAX_FACTOR, so the gaps
// between letters of a single word aren't turned into spaces.
const WIDE_SPACE_MULT = 1.3;
const WIDE_SPACE_MAX_FACTOR = 0.4;

// For fonts without usable space metrics (Type3), the space threshold is
// derived from the current glyph's own advance width instead of the font
// size, since the latter is unreliable when the font matrix does the
// scaling.
const CHAR_WIDTH_SPACE_FACTOR = 0.25;

// If a char is too high/too low compared to the previous we just create
// a new chunk.
// If the advance isn't in the +/-VERTICAL_SHIFT_RATIO * height range then
Expand All @@ -2516,6 +2538,7 @@ class PartialEvaluator {
const preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);

let textState, currentTextState;
let lastFakeSpaceExtraCharId = -1;

function pushWhitespace({
width = 0,
Expand Down Expand Up @@ -2610,10 +2633,21 @@ class PartialEvaluator {
textContentItem.textAdvanceScale = scaleCtmX * scaleLineX;

const { fontSize } = textState;
textContentItem.trackingSpaceMin = fontSize * TRACKING_SPACE_FACTOR;

let trackingFactor = TRACKING_SPACE_FACTOR;
if (!font.isType3Font) {
const spaceEm = font.spaceWidth / 1000;
if (spaceEm > 0 && spaceEm <= MAX_SPACE_WIDTH_FACTOR) {
trackingFactor = Math.max(0.5 * spaceEm, TRACKING_SPACE_FACTOR);
}
}
textContentItem.trackingSpaceMin = fontSize * trackingFactor;
textContentItem.useCharWidthThreshold = font.isType3Font;
textContentItem.spaceInFlowMin = fontSize * trackingFactor;
textContentItem.wideSpaceMax = fontSize * WIDE_SPACE_MAX_FACTOR;
resetAdaptiveSpacing();
textContentItem.notASpace = fontSize * NOT_A_SPACE_FACTOR;
textContentItem.negativeSpaceMax = fontSize * NEGATIVE_SPACE_FACTOR;
textContentItem.spaceInFlowMin = fontSize * SPACE_IN_FLOW_MIN_FACTOR;
textContentItem.spaceInFlowMax = fontSize * SPACE_IN_FLOW_MAX_FACTOR;
textContentItem.hasEOL = false;

Expand Down Expand Up @@ -2690,6 +2724,84 @@ class PartialEvaluator {
];
}

function getTrackingSpaceMin(glyphWidth) {
if (textContentItem.useCharWidthThreshold && glyphWidth) {
return (
Math.abs(glyphWidth * textState.textHScale) * CHAR_WIDTH_SPACE_FACTOR
);
}
return textContentItem.trackingSpaceMin;
}

function getSpaceThreshold(trackingSpaceMin) {
const { minAdvance, wideSpaceMax } = textContentItem;
if (
minAdvance > trackingSpaceMin &&
minAdvance < WIDE_SPACE_MULT * trackingSpaceMin
) {
return Math.min(WIDE_SPACE_MULT * minAdvance, wideSpaceMax);
}
return trackingSpaceMin;
}

function resetAdaptiveSpacing() {
textContentItem.minAdvance = Infinity;
textContentItem.pendingSpaceIndex = -1;
textContentItem.pendingSpaceAdvance = 0;
textContentItem.pendingSpaceExtraCharId = -1;
}

function recordPendingSpace(advance, textOrientation) {
if (
textContentItem.useCharWidthThreshold ||
textContentItem.minAdvance < Infinity
) {
return;
}
textContentItem.pendingSpaceIndex = textContentItem.str.length - 1;
textContentItem.pendingSpaceAdvance = textOrientation * advance;
textContentItem.pendingSpaceExtraCharId = lastFakeSpaceExtraCharId;
}

function resolvePendingSpace(
advance,
textOrientation,
trackingSpaceMin,
spaceThreshold
) {
if (textContentItem.pendingSpaceIndex < 0) {
return;
}
const { pendingSpaceAdvance } = textContentItem;
const forwardAdvance = textOrientation * advance;
const baseline = Math.min(pendingSpaceAdvance, forwardAdvance);
if (
baseline > trackingSpaceMin / WIDE_SPACE_MULT &&
forwardAdvance <= spaceThreshold &&
pendingSpaceAdvance <= spaceThreshold
) {
textContentItem.str.splice(textContentItem.pendingSpaceIndex, 1);
if (textContentItem.pendingSpaceExtraCharId >= 0) {
intersector?.removeExtraChar(textContentItem.pendingSpaceExtraCharId);
}
}
textContentItem.pendingSpaceIndex = -1;
textContentItem.pendingSpaceExtraCharId = -1;
}

function recordMinAdvance(advance, textOrientation, trackingSpaceMin) {
if (textContentItem.useCharWidthThreshold) {
return;
}
const forwardAdvance = textOrientation * advance;
if (
forwardAdvance > trackingSpaceMin &&
forwardAdvance < textContentItem.minAdvance
) {
textContentItem.minAdvance = forwardAdvance;
}
}

function compareWithLastPosition(glyphWidth) {
const currentTransform = getCurrentTextTransform();
let posX = currentTransform[4];
Expand Down Expand Up @@ -2722,6 +2834,7 @@ class PartialEvaluator {
let lastPosY = textContentItem.prevTransform[5];

if (lastPosX === posX && lastPosY === posY) {
textContentItem.minAdvance = Infinity;
return true;
}

Expand Down Expand Up @@ -2806,9 +2919,18 @@ class PartialEvaluator {
// The real spacing between 2 consecutive chars is thin enough to be
// considered a non-space.
resetLastChars();
textContentItem.minAdvance = Infinity;
}

if (advanceY <= textOrientation * textContentItem.trackingSpaceMin) {
const trackingSpaceMin = getTrackingSpaceMin(glyphWidth);
const spaceThreshold = getSpaceThreshold(trackingSpaceMin);
resolvePendingSpace(
advanceY,
textOrientation,
trackingSpaceMin,
spaceThreshold
);
if (advanceY <= textOrientation * spaceThreshold) {
if (shouldAddWhitepsace()) {
// The space is very thin, hence it deserves to have its own span in
// order to avoid too much shift between the canvas and the text
Expand All @@ -2831,9 +2953,12 @@ class PartialEvaluator {
pushWhitespace({ height: Math.abs(advanceY) });
} else {
textContentItem.height += advanceY;
recordPendingSpace(advanceY, textOrientation);
}
}

recordMinAdvance(advanceY, textOrientation, trackingSpaceMin);

if (Math.abs(advanceX) > textContentItem.width * VERTICAL_SHIFT_RATIO) {
flushTextContentItem();
}
Expand Down Expand Up @@ -2886,9 +3011,18 @@ class PartialEvaluator {
// The real spacing between 2 consecutive chars is thin enough to be
// considered a non-space.
resetLastChars();
textContentItem.minAdvance = Infinity;
}

if (advanceX <= textOrientation * textContentItem.trackingSpaceMin) {
const trackingSpaceMin = getTrackingSpaceMin(glyphWidth);
const spaceThreshold = getSpaceThreshold(trackingSpaceMin);
resolvePendingSpace(
advanceX,
textOrientation,
trackingSpaceMin,
spaceThreshold
);
if (advanceX <= textOrientation * spaceThreshold) {
if (shouldAddWhitepsace()) {
// The space is very thin, hence it deserves to have its own span in
// order to avoid too much shift between the canvas and the text
Expand All @@ -2907,9 +3041,12 @@ class PartialEvaluator {
pushWhitespace({ width: Math.abs(advanceX) });
} else {
textContentItem.width += advanceX;
recordPendingSpace(advanceX, textOrientation);
}
}

recordMinAdvance(advanceX, textOrientation, trackingSpaceMin);

if (Math.abs(advanceY) > textContentItem.height * VERTICAL_SHIFT_RATIO) {
flushTextContentItem();
}
Expand Down Expand Up @@ -3092,14 +3229,15 @@ class PartialEvaluator {
}

function addFakeSpaces(width, transf, textOrientation) {
lastFakeSpaceExtraCharId = -1;
if (
textOrientation * textContentItem.spaceInFlowMin <= width &&
width <= textOrientation * textContentItem.spaceInFlowMax
) {
if (textContentItem.initialized) {
resetLastChars();
textContentItem.str.push(" ");
intersector?.addExtraChar(" ");
lastFakeSpaceExtraCharId = intersector?.addExtraChar(" ") ?? -1;
}
return false;
}
Expand Down Expand Up @@ -3140,6 +3278,7 @@ class PartialEvaluator {

textContent.items.push(runBidiTransform(textContentItem));
textContentItem.initialized = false;
resetAdaptiveSpacing();
textContentItem.str.length = 0;
}

Expand Down Expand Up @@ -3216,22 +3355,28 @@ class PartialEvaluator {
textState.textRise = args[0];
break;
case OPS.setHScale:
textState.textHScale = args[0] / 100;
if (textState.textHScale !== args[0] / 100) {
textState.textHScale = args[0] / 100;
resetAdaptiveSpacing();
}
break;
case OPS.setLeading:
textState.leading = args[0];
break;
case OPS.moveText:
textState.translateTextLineMatrix(args[0], args[1]);
textState.textMatrix = textState.textLineMatrix.slice();
resetAdaptiveSpacing();
break;
case OPS.setLeadingMoveText:
textState.leading = -args[1];
textState.translateTextLineMatrix(args[0], args[1]);
textState.textMatrix = textState.textLineMatrix.slice();
resetAdaptiveSpacing();
break;
case OPS.nextLine:
textState.carriageReturn();
resetAdaptiveSpacing();
break;
case OPS.setTextMatrix:
textState.setTextMatrix(
Expand All @@ -3251,16 +3396,24 @@ class PartialEvaluator {
args[5]
);
updateAdvanceScale();
resetAdaptiveSpacing();
break;
case OPS.setCharSpacing:
textState.charSpacing = args[0];
if (textState.charSpacing !== args[0]) {
textState.charSpacing = args[0];
resetAdaptiveSpacing();
}
break;
case OPS.setWordSpacing:
textState.wordSpacing = args[0];
if (textState.wordSpacing !== args[0]) {
textState.wordSpacing = args[0];
resetAdaptiveSpacing();
}
break;
case OPS.beginText:
textState.textMatrix = IDENTITY_MATRIX.slice();
textState.textLineMatrix = IDENTITY_MATRIX.slice();
resetAdaptiveSpacing();
break;
case OPS.showSpacedText:
if (!stateManager.state.font) {
Expand Down Expand Up @@ -3318,6 +3471,7 @@ class PartialEvaluator {
continue;
}
textState.carriageReturn();
resetAdaptiveSpacing();
buildTextContentItem({
chars: args[0],
extraSpacing: 0,
Expand All @@ -3328,9 +3482,16 @@ class PartialEvaluator {
self.ensureStateFont(stateManager.state);
continue;
}
textState.wordSpacing = args[0];
textState.charSpacing = args[1];
if (
textState.wordSpacing !== args[0] ||
textState.charSpacing !== args[1]
) {
textState.wordSpacing = args[0];
textState.charSpacing = args[1];
resetAdaptiveSpacing();
}
textState.carriageReturn();
resetAdaptiveSpacing();
buildTextContentItem({
chars: args[2],
extraSpacing: 0,
Expand Down
9 changes: 3 additions & 6 deletions src/core/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3462,10 +3462,7 @@ class Font {
return builder.toArray();
}

/**
* @private
*/
get _spaceWidth() {
get spaceWidth() {
// trying to estimate space character width
const possibleSpaceReplacements = ["space", "minus", "one", "i", "I"];
let width;
Expand Down Expand Up @@ -3500,7 +3497,7 @@ class Font {
break; // the non-zero width found
}
}
return shadow(this, "_spaceWidth", width || this.defaultWidth);
return shadow(this, "spaceWidth", width || this.defaultWidth);
}

/**
Expand Down Expand Up @@ -3552,7 +3549,7 @@ class Font {
if (glyphName === "") {
// Ensure that other relevant glyph properties are also updated
// (fixes issue18059.pdf).
width ||= this._spaceWidth;
width ||= this.spaceWidth;
unicode = String.fromCharCode(fontCharCode);
}
}
Expand Down
Loading
Loading