@@ -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 , NOT_A_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,78 @@ 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+ const forwardAdvance = textOrientation * advance ;
2791+ if (
2792+ forwardAdvance > trackingSpaceMin &&
2793+ forwardAdvance < textContentItem . minAdvance
2794+ ) {
2795+ textContentItem . minAdvance = forwardAdvance ;
2796+ }
2797+ }
2798+
26932799 function compareWithLastPosition ( glyphWidth ) {
26942800 const currentTransform = getCurrentTextTransform ( ) ;
26952801 let posX = currentTransform [ 4 ] ;
@@ -2808,7 +2914,15 @@ class PartialEvaluator {
28082914 resetLastChars ( ) ;
28092915 }
28102916
2811- if ( advanceY <= textOrientation * textContentItem . trackingSpaceMin ) {
2917+ const trackingSpaceMin = getTrackingSpaceMin ( glyphWidth ) ;
2918+ const spaceThreshold = getSpaceThreshold ( trackingSpaceMin ) ;
2919+ resolvePendingSpace (
2920+ advanceY ,
2921+ textOrientation ,
2922+ trackingSpaceMin ,
2923+ spaceThreshold
2924+ ) ;
2925+ if ( advanceY <= textOrientation * spaceThreshold ) {
28122926 if ( shouldAddWhitepsace ( ) ) {
28132927 // The space is very thin, hence it deserves to have its own span in
28142928 // order to avoid too much shift between the canvas and the text
@@ -2831,9 +2945,12 @@ class PartialEvaluator {
28312945 pushWhitespace ( { height : Math . abs ( advanceY ) } ) ;
28322946 } else {
28332947 textContentItem . height += advanceY ;
2948+ recordPendingSpace ( advanceY , textOrientation ) ;
28342949 }
28352950 }
28362951
2952+ recordMinAdvance ( advanceY , textOrientation , trackingSpaceMin ) ;
2953+
28372954 if ( Math . abs ( advanceX ) > textContentItem . width * VERTICAL_SHIFT_RATIO ) {
28382955 flushTextContentItem ( ) ;
28392956 }
@@ -2888,7 +3005,15 @@ class PartialEvaluator {
28883005 resetLastChars ( ) ;
28893006 }
28903007
2891- if ( advanceX <= textOrientation * textContentItem . trackingSpaceMin ) {
3008+ const trackingSpaceMin = getTrackingSpaceMin ( glyphWidth ) ;
3009+ const spaceThreshold = getSpaceThreshold ( trackingSpaceMin ) ;
3010+ resolvePendingSpace (
3011+ advanceX ,
3012+ textOrientation ,
3013+ trackingSpaceMin ,
3014+ spaceThreshold
3015+ ) ;
3016+ if ( advanceX <= textOrientation * spaceThreshold ) {
28923017 if ( shouldAddWhitepsace ( ) ) {
28933018 // The space is very thin, hence it deserves to have its own span in
28943019 // order to avoid too much shift between the canvas and the text
@@ -2907,9 +3032,12 @@ class PartialEvaluator {
29073032 pushWhitespace ( { width : Math . abs ( advanceX ) } ) ;
29083033 } else {
29093034 textContentItem . width += advanceX ;
3035+ recordPendingSpace ( advanceX , textOrientation ) ;
29103036 }
29113037 }
29123038
3039+ recordMinAdvance ( advanceX , textOrientation , trackingSpaceMin ) ;
3040+
29133041 if ( Math . abs ( advanceY ) > textContentItem . height * VERTICAL_SHIFT_RATIO ) {
29143042 flushTextContentItem ( ) ;
29153043 }
@@ -3092,14 +3220,15 @@ class PartialEvaluator {
30923220 }
30933221
30943222 function addFakeSpaces ( width , transf , textOrientation ) {
3223+ lastFakeSpaceExtraCharId = - 1 ;
30953224 if (
30963225 textOrientation * textContentItem . spaceInFlowMin <= width &&
30973226 width <= textOrientation * textContentItem . spaceInFlowMax
30983227 ) {
30993228 if ( textContentItem . initialized ) {
31003229 resetLastChars ( ) ;
31013230 textContentItem . str . push ( " " ) ;
3102- intersector ?. addExtraChar ( " " ) ;
3231+ lastFakeSpaceExtraCharId = intersector ?. addExtraChar ( " " ) ?? - 1 ;
31033232 }
31043233 return false ;
31053234 }
@@ -3140,6 +3269,7 @@ class PartialEvaluator {
31403269
31413270 textContent . items . push ( runBidiTransform ( textContentItem ) ) ;
31423271 textContentItem . initialized = false ;
3272+ resetAdaptiveSpacing ( ) ;
31433273 textContentItem . str . length = 0 ;
31443274 }
31453275
@@ -3216,7 +3346,10 @@ class PartialEvaluator {
32163346 textState . textRise = args [ 0 ] ;
32173347 break ;
32183348 case OPS . setHScale :
3219- textState . textHScale = args [ 0 ] / 100 ;
3349+ if ( textState . textHScale !== args [ 0 ] / 100 ) {
3350+ textState . textHScale = args [ 0 ] / 100 ;
3351+ resetAdaptiveSpacing ( ) ;
3352+ }
32203353 break ;
32213354 case OPS . setLeading :
32223355 textState . leading = args [ 0 ] ;
@@ -3251,12 +3384,19 @@ class PartialEvaluator {
32513384 args [ 5 ]
32523385 ) ;
32533386 updateAdvanceScale ( ) ;
3387+ resetAdaptiveSpacing ( ) ;
32543388 break ;
32553389 case OPS . setCharSpacing :
3256- textState . charSpacing = args [ 0 ] ;
3390+ if ( textState . charSpacing !== args [ 0 ] ) {
3391+ textState . charSpacing = args [ 0 ] ;
3392+ resetAdaptiveSpacing ( ) ;
3393+ }
32573394 break ;
32583395 case OPS . setWordSpacing :
3259- textState . wordSpacing = args [ 0 ] ;
3396+ if ( textState . wordSpacing !== args [ 0 ] ) {
3397+ textState . wordSpacing = args [ 0 ] ;
3398+ resetAdaptiveSpacing ( ) ;
3399+ }
32603400 break ;
32613401 case OPS . beginText :
32623402 textState . textMatrix = IDENTITY_MATRIX . slice ( ) ;
@@ -3328,8 +3468,14 @@ class PartialEvaluator {
33283468 self . ensureStateFont ( stateManager . state ) ;
33293469 continue ;
33303470 }
3331- textState . wordSpacing = args [ 0 ] ;
3332- textState . charSpacing = args [ 1 ] ;
3471+ if (
3472+ textState . wordSpacing !== args [ 0 ] ||
3473+ textState . charSpacing !== args [ 1 ]
3474+ ) {
3475+ textState . wordSpacing = args [ 0 ] ;
3476+ textState . charSpacing = args [ 1 ] ;
3477+ resetAdaptiveSpacing ( ) ;
3478+ }
33333479 textState . carriageReturn ( ) ;
33343480 buildTextContentItem ( {
33353481 chars : args [ 2 ] ,
0 commit comments