@@ -2418,6 +2418,12 @@ class PartialEvaluator {
24182418 spaceInFlowMin : 0 ,
24192419 spaceInFlowMax : 0 ,
24202420 trackingSpaceMin : Infinity ,
2421+ useCharWidthThreshold : false ,
2422+ wideSpaceMax : Infinity ,
2423+ minAdvance : Infinity ,
2424+ pendingSpaceIndex : - 1 ,
2425+ pendingSpaceAdvance : 0 ,
2426+ pendingSpaceExtraCharId : - 1 ,
24212427 negativeSpaceMax : - Infinity ,
24222428 notASpace : - Infinity ,
24232429 transform : null ,
@@ -2483,21 +2489,37 @@ class PartialEvaluator {
24832489 // even if one is present in the text stream.
24842490 const NOT_A_SPACE_FACTOR = 0.03 ;
24852491
2492+ // A reported space wider than fontSize * MAX_SPACE_WIDTH_FACTOR is treated
2493+ // as unreliable metrics, so trackingSpaceMin isn't derived from it.
2494+ const MAX_SPACE_WIDTH_FACTOR = 1 / 3 ;
2495+
24862496 // A negative white < fontSize * NEGATIVE_SPACE_FACTOR induces
24872497 // a break (a new chunk of text is created).
24882498 // It doesn't change anything when the text is copied but
24892499 // it improves potential mismatch between text layer and canvas.
24902500 const NEGATIVE_SPACE_FACTOR = - 0.2 ;
24912501
2492- // A white with a width in [fontSize * MIN_FACTOR ; fontSize * MAX_FACTOR]
2493- // is a space which will be inserted in the current flow of words.
2502+ // A white with a width in [trackingSpaceMin ; fontSize * MAX_FACTOR] is a
2503+ // space which will be inserted in the current flow of words.
24942504 // If the width is outside of this range then the flow is broken
24952505 // (which means a new span in the text layer).
24962506 // It's useful to adjust the best as possible the span in the layer
24972507 // to what is displayed in the canvas.
2498- const SPACE_IN_FLOW_MIN_FACTOR = 0.102 ;
24992508 const SPACE_IN_FLOW_MAX_FACTOR = 0.6 ;
25002509
2510+ // When every gap within a run is wider than the space threshold (e.g.
2511+ // letter-spaced text), the threshold is raised to this multiple of the
2512+ // smallest gap, capped at fontSize * WIDE_SPACE_MAX_FACTOR, so the gaps
2513+ // between letters of a single word aren't turned into spaces.
2514+ const WIDE_SPACE_MULT = 1.3 ;
2515+ const WIDE_SPACE_MAX_FACTOR = 0.4 ;
2516+
2517+ // For fonts without usable space metrics (Type3), the space threshold is
2518+ // derived from the current glyph's own advance width instead of the font
2519+ // size, since the latter is unreliable when the font matrix does the
2520+ // scaling.
2521+ const CHAR_WIDTH_SPACE_FACTOR = 0.25 ;
2522+
25012523 // If a char is too high/too low compared to the previous we just create
25022524 // a new chunk.
25032525 // If the advance isn't in the +/-VERTICAL_SHIFT_RATIO * height range then
@@ -2516,6 +2538,7 @@ class PartialEvaluator {
25162538 const preprocessor = new EvaluatorPreprocessor ( stream , xref , stateManager ) ;
25172539
25182540 let textState , currentTextState ;
2541+ let lastFakeSpaceExtraCharId = - 1 ;
25192542
25202543 function pushWhitespace ( {
25212544 width = 0 ,
@@ -2610,10 +2633,21 @@ class PartialEvaluator {
26102633 textContentItem . textAdvanceScale = scaleCtmX * scaleLineX ;
26112634
26122635 const { fontSize } = textState ;
2613- textContentItem . trackingSpaceMin = fontSize * TRACKING_SPACE_FACTOR ;
2636+
2637+ let trackingFactor = TRACKING_SPACE_FACTOR ;
2638+ if ( ! font . isType3Font ) {
2639+ const spaceEm = font . spaceWidth / 1000 ;
2640+ if ( spaceEm > 0 && spaceEm <= MAX_SPACE_WIDTH_FACTOR ) {
2641+ trackingFactor = Math . max ( 0.5 * spaceEm , TRACKING_SPACE_FACTOR ) ;
2642+ }
2643+ }
2644+ textContentItem . trackingSpaceMin = fontSize * trackingFactor ;
2645+ textContentItem . useCharWidthThreshold = font . isType3Font ;
2646+ textContentItem . spaceInFlowMin = fontSize * trackingFactor ;
2647+ textContentItem . wideSpaceMax = fontSize * WIDE_SPACE_MAX_FACTOR ;
2648+ resetAdaptiveSpacing ( ) ;
26142649 textContentItem . notASpace = fontSize * NOT_A_SPACE_FACTOR ;
26152650 textContentItem . negativeSpaceMax = fontSize * NEGATIVE_SPACE_FACTOR ;
2616- textContentItem . spaceInFlowMin = fontSize * SPACE_IN_FLOW_MIN_FACTOR ;
26172651 textContentItem . spaceInFlowMax = fontSize * SPACE_IN_FLOW_MAX_FACTOR ;
26182652 textContentItem . hasEOL = false ;
26192653
@@ -2690,6 +2724,81 @@ class PartialEvaluator {
26902724 ] ;
26912725 }
26922726
2727+ function getTrackingSpaceMin ( glyphWidth ) {
2728+ if ( textContentItem . useCharWidthThreshold && glyphWidth ) {
2729+ return (
2730+ Math . abs ( glyphWidth * textState . textHScale ) * CHAR_WIDTH_SPACE_FACTOR
2731+ ) ;
2732+ }
2733+ return textContentItem . trackingSpaceMin ;
2734+ }
2735+
2736+ function getSpaceThreshold ( trackingSpaceMin ) {
2737+ const { minAdvance, wideSpaceMax } = textContentItem ;
2738+ if (
2739+ minAdvance > trackingSpaceMin &&
2740+ minAdvance < WIDE_SPACE_MULT * trackingSpaceMin
2741+ ) {
2742+ return Math . min ( WIDE_SPACE_MULT * minAdvance , wideSpaceMax ) ;
2743+ }
2744+ return trackingSpaceMin ;
2745+ }
2746+
2747+ function resetAdaptiveSpacing ( ) {
2748+ textContentItem . minAdvance = Infinity ;
2749+ textContentItem . pendingSpaceIndex = - 1 ;
2750+ textContentItem . pendingSpaceAdvance = 0 ;
2751+ textContentItem . pendingSpaceExtraCharId = - 1 ;
2752+ }
2753+
2754+ function recordPendingSpace ( advance , textOrientation ) {
2755+ if ( textContentItem . minAdvance < Infinity ) {
2756+ return ;
2757+ }
2758+ textContentItem . pendingSpaceIndex = textContentItem . str . length - 1 ;
2759+ textContentItem . pendingSpaceAdvance = textOrientation * advance ;
2760+ textContentItem . pendingSpaceExtraCharId = lastFakeSpaceExtraCharId ;
2761+ }
2762+
2763+ function resolvePendingSpace (
2764+ advance ,
2765+ textOrientation ,
2766+ trackingSpaceMin ,
2767+ spaceThreshold
2768+ ) {
2769+ if ( textContentItem . pendingSpaceIndex < 0 ) {
2770+ return ;
2771+ }
2772+ const { pendingSpaceAdvance } = textContentItem ;
2773+ const forwardAdvance = textOrientation * advance ;
2774+ const baseline = Math . min ( pendingSpaceAdvance , forwardAdvance ) ;
2775+ if (
2776+ baseline > trackingSpaceMin / WIDE_SPACE_MULT &&
2777+ forwardAdvance <= spaceThreshold &&
2778+ pendingSpaceAdvance <= spaceThreshold
2779+ ) {
2780+ textContentItem . str . splice ( textContentItem . pendingSpaceIndex , 1 ) ;
2781+ if ( textContentItem . pendingSpaceExtraCharId >= 0 ) {
2782+ intersector ?. removeExtraChar ( textContentItem . pendingSpaceExtraCharId ) ;
2783+ }
2784+ }
2785+ textContentItem . pendingSpaceIndex = - 1 ;
2786+ textContentItem . pendingSpaceExtraCharId = - 1 ;
2787+ }
2788+
2789+ function recordMinAdvance ( advance , textOrientation , trackingSpaceMin ) {
2790+ if ( textContentItem . useCharWidthThreshold ) {
2791+ return ;
2792+ }
2793+ const forwardAdvance = textOrientation * advance ;
2794+ if (
2795+ forwardAdvance > trackingSpaceMin &&
2796+ forwardAdvance < textContentItem . minAdvance
2797+ ) {
2798+ textContentItem . minAdvance = forwardAdvance ;
2799+ }
2800+ }
2801+
26932802 function compareWithLastPosition ( glyphWidth ) {
26942803 const currentTransform = getCurrentTextTransform ( ) ;
26952804 let posX = currentTransform [ 4 ] ;
@@ -2722,6 +2831,7 @@ class PartialEvaluator {
27222831 let lastPosY = textContentItem . prevTransform [ 5 ] ;
27232832
27242833 if ( lastPosX === posX && lastPosY === posY ) {
2834+ textContentItem . minAdvance = Infinity ;
27252835 return true ;
27262836 }
27272837
@@ -2808,7 +2918,15 @@ class PartialEvaluator {
28082918 resetLastChars ( ) ;
28092919 }
28102920
2811- if ( advanceY <= textOrientation * textContentItem . trackingSpaceMin ) {
2921+ const trackingSpaceMin = getTrackingSpaceMin ( glyphWidth ) ;
2922+ const spaceThreshold = getSpaceThreshold ( trackingSpaceMin ) ;
2923+ resolvePendingSpace (
2924+ advanceY ,
2925+ textOrientation ,
2926+ trackingSpaceMin ,
2927+ spaceThreshold
2928+ ) ;
2929+ if ( advanceY <= textOrientation * spaceThreshold ) {
28122930 if ( shouldAddWhitepsace ( ) ) {
28132931 // The space is very thin, hence it deserves to have its own span in
28142932 // order to avoid too much shift between the canvas and the text
@@ -2831,9 +2949,12 @@ class PartialEvaluator {
28312949 pushWhitespace ( { height : Math . abs ( advanceY ) } ) ;
28322950 } else {
28332951 textContentItem . height += advanceY ;
2952+ recordPendingSpace ( advanceY , textOrientation ) ;
28342953 }
28352954 }
28362955
2956+ recordMinAdvance ( advanceY , textOrientation , trackingSpaceMin ) ;
2957+
28372958 if ( Math . abs ( advanceX ) > textContentItem . width * VERTICAL_SHIFT_RATIO ) {
28382959 flushTextContentItem ( ) ;
28392960 }
@@ -2888,7 +3009,15 @@ class PartialEvaluator {
28883009 resetLastChars ( ) ;
28893010 }
28903011
2891- if ( advanceX <= textOrientation * textContentItem . trackingSpaceMin ) {
3012+ const trackingSpaceMin = getTrackingSpaceMin ( glyphWidth ) ;
3013+ const spaceThreshold = getSpaceThreshold ( trackingSpaceMin ) ;
3014+ resolvePendingSpace (
3015+ advanceX ,
3016+ textOrientation ,
3017+ trackingSpaceMin ,
3018+ spaceThreshold
3019+ ) ;
3020+ if ( advanceX <= textOrientation * spaceThreshold ) {
28923021 if ( shouldAddWhitepsace ( ) ) {
28933022 // The space is very thin, hence it deserves to have its own span in
28943023 // order to avoid too much shift between the canvas and the text
@@ -2907,9 +3036,12 @@ class PartialEvaluator {
29073036 pushWhitespace ( { width : Math . abs ( advanceX ) } ) ;
29083037 } else {
29093038 textContentItem . width += advanceX ;
3039+ recordPendingSpace ( advanceX , textOrientation ) ;
29103040 }
29113041 }
29123042
3043+ recordMinAdvance ( advanceX , textOrientation , trackingSpaceMin ) ;
3044+
29133045 if ( Math . abs ( advanceY ) > textContentItem . height * VERTICAL_SHIFT_RATIO ) {
29143046 flushTextContentItem ( ) ;
29153047 }
@@ -3092,14 +3224,15 @@ class PartialEvaluator {
30923224 }
30933225
30943226 function addFakeSpaces ( width , transf , textOrientation ) {
3227+ lastFakeSpaceExtraCharId = - 1 ;
30953228 if (
30963229 textOrientation * textContentItem . spaceInFlowMin <= width &&
30973230 width <= textOrientation * textContentItem . spaceInFlowMax
30983231 ) {
30993232 if ( textContentItem . initialized ) {
31003233 resetLastChars ( ) ;
31013234 textContentItem . str . push ( " " ) ;
3102- intersector ?. addExtraChar ( " " ) ;
3235+ lastFakeSpaceExtraCharId = intersector ?. addExtraChar ( " " ) ?? - 1 ;
31033236 }
31043237 return false ;
31053238 }
@@ -3140,6 +3273,7 @@ class PartialEvaluator {
31403273
31413274 textContent . items . push ( runBidiTransform ( textContentItem ) ) ;
31423275 textContentItem . initialized = false ;
3276+ resetAdaptiveSpacing ( ) ;
31433277 textContentItem . str . length = 0 ;
31443278 }
31453279
@@ -3216,7 +3350,10 @@ class PartialEvaluator {
32163350 textState . textRise = args [ 0 ] ;
32173351 break ;
32183352 case OPS . setHScale :
3219- textState . textHScale = args [ 0 ] / 100 ;
3353+ if ( textState . textHScale !== args [ 0 ] / 100 ) {
3354+ textState . textHScale = args [ 0 ] / 100 ;
3355+ resetAdaptiveSpacing ( ) ;
3356+ }
32203357 break ;
32213358 case OPS . setLeading :
32223359 textState . leading = args [ 0 ] ;
@@ -3251,12 +3388,19 @@ class PartialEvaluator {
32513388 args [ 5 ]
32523389 ) ;
32533390 updateAdvanceScale ( ) ;
3391+ resetAdaptiveSpacing ( ) ;
32543392 break ;
32553393 case OPS . setCharSpacing :
3256- textState . charSpacing = args [ 0 ] ;
3394+ if ( textState . charSpacing !== args [ 0 ] ) {
3395+ textState . charSpacing = args [ 0 ] ;
3396+ resetAdaptiveSpacing ( ) ;
3397+ }
32573398 break ;
32583399 case OPS . setWordSpacing :
3259- textState . wordSpacing = args [ 0 ] ;
3400+ if ( textState . wordSpacing !== args [ 0 ] ) {
3401+ textState . wordSpacing = args [ 0 ] ;
3402+ resetAdaptiveSpacing ( ) ;
3403+ }
32603404 break ;
32613405 case OPS . beginText :
32623406 textState . textMatrix = IDENTITY_MATRIX . slice ( ) ;
@@ -3328,8 +3472,14 @@ class PartialEvaluator {
33283472 self . ensureStateFont ( stateManager . state ) ;
33293473 continue ;
33303474 }
3331- textState . wordSpacing = args [ 0 ] ;
3332- textState . charSpacing = args [ 1 ] ;
3475+ if (
3476+ textState . wordSpacing !== args [ 0 ] ||
3477+ textState . charSpacing !== args [ 1 ]
3478+ ) {
3479+ textState . wordSpacing = args [ 0 ] ;
3480+ textState . charSpacing = args [ 1 ] ;
3481+ resetAdaptiveSpacing ( ) ;
3482+ }
33333483 textState . carriageReturn ( ) ;
33343484 buildTextContentItem ( {
33353485 chars : args [ 2 ] ,
0 commit comments