@@ -215,10 +215,10 @@ internal void Write(PdfDocument document)
215215 contentStreams . Add ( Encoding . Latin1 . GetBytes ( BuildContentStream ( pages [ i ] , embeddedFonts . Count > 0 , cpToFontSlot , embeddedFonts ) ) ) ;
216216
217217 // Allocate object numbers.
218- // 1 = Catalog, 2 = Pages, 3 = Font F1 (Helvetica/WinAnsi)
218+ // 1 = Catalog, 2 = Pages, 3 = Font F1 (Helvetica/WinAnsi), 4 = Font F1B (Helvetica-Bold/WinAnsi)
219219 // Per embedded font: 6 objects (ToUnicode, Descriptor, CIDFont, Type0, FontFile2, CIDToGIDMap)
220220 // Per page: content stream obj, N image XObject objs, page obj
221- var nextObj = 4 ;
221+ var nextObj = 5 ;
222222
223223 // Allocate font objects
224224 foreach ( var ef in embeddedFonts )
@@ -263,6 +263,10 @@ internal void Write(PdfDocument document)
263263 _objectOffsets [ 3 ] = Position ;
264264 WriteRaw ( "3 0 obj\n << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\n endobj\n " ) ;
265265
266+ // ── Object 4: Font F1B (Helvetica-Bold, built-in WinAnsiEncoding) ──
267+ _objectOffsets [ 4 ] = Position ;
268+ WriteRaw ( "4 0 obj\n << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\n endobj\n " ) ;
269+
266270 // ── Per-font objects (F2, F3, …) ───────────────────────────────────
267271 for ( var fi = 0 ; fi < embeddedFonts . Count ; fi ++ )
268272 {
@@ -356,8 +360,8 @@ internal void Write(PdfDocument document)
356360 WriteRaw ( $ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 { w } { h } ]\n ") ;
357361 WriteRaw ( $ "/Contents { contentObjNums [ i ] } 0 R\n ") ;
358362 WriteRaw ( "/Resources <<\n " ) ;
359- // Font dictionary: F1 + Fn for each embedded font
360- WriteRaw ( "/Font << /F1 3 0 R" ) ;
363+ // Font dictionary: F1, F1B + Fn for each embedded font
364+ WriteRaw ( "/Font << /F1 3 0 R /F1B 4 0 R " ) ;
361365 for ( var fi = 0 ; fi < embeddedFonts . Count ; fi ++ )
362366 WriteRaw ( $ " /F{ fi + 2 } { embeddedFonts [ fi ] . Type0Obj } 0 R") ;
363367 WriteRaw ( " >>\n " ) ;
@@ -514,15 +518,19 @@ private static string BuildContentStream(PdfPage page, bool hasUnicodeFont, Dict
514518
515519 if ( ! hasUnicodeFont || ! block . Text . Any ( c => ! IsWinAnsiHandled ( c ) ) )
516520 {
517- // Pure Latin-1 text — use F1 (Helvetica) as before
521+ // Pure Latin-1 text — use F1 (Helvetica) or F1B (Helvetica-Bold)
522+ var fontName = block . Bold ? "F1B" : "F1" ;
518523 var escapedText = EscapePdfString ( block . Text ) ;
519524 sb . Append ( "BT\n " ) ;
520525 sb . Append ( colorCmd ) ;
521- sb . Append ( $ "/F1 { fontSize } Tf\n ") ;
526+ sb . Append ( $ "/{ fontName } { fontSize } Tf\n ") ;
527+ // Apply character spacing (Tc)
528+ if ( block . CharSpacing != 0 )
529+ sb . Append ( $ "{ block . CharSpacing . ToString ( "F2" , CultureInfo . InvariantCulture ) } Tc\n ") ;
522530 // Apply horizontal scaling if text overflows MaxWidth
523531 if ( block . MaxWidth . HasValue )
524532 {
525- var naturalWidth = MeasureTextWidth ( block . Text , block . FontSize ) ;
533+ var naturalWidth = MeasureTextWidth ( block . Text , block . FontSize , block . CharSpacing ) ;
526534 if ( naturalWidth > block . MaxWidth . Value && naturalWidth > 0 )
527535 {
528536 var tzPercent = ( block . MaxWidth . Value / naturalWidth ) * 100.0 ;
@@ -544,10 +552,13 @@ private static string BuildContentStream(PdfPage page, bool hasUnicodeFont, Dict
544552 // emit each run with the appropriate Fn, using Td to advance.
545553 sb . Append ( "BT\n " ) ;
546554 sb . Append ( colorCmd ) ;
555+ // Apply character spacing (Tc)
556+ if ( block . CharSpacing != 0 )
557+ sb . Append ( $ "{ block . CharSpacing . ToString ( "F2" , CultureInfo . InvariantCulture ) } Tc\n ") ;
547558 // Apply horizontal scaling if text overflows MaxWidth
548559 if ( block . MaxWidth . HasValue )
549560 {
550- var naturalWidth = MeasureTextWidth ( block . Text , block . FontSize ) ;
561+ var naturalWidth = MeasureTextWidth ( block . Text , block . FontSize , block . CharSpacing ) ;
551562 if ( naturalWidth > block . MaxWidth . Value && naturalWidth > 0 )
552563 {
553564 var tzPercent = ( block . MaxWidth . Value / naturalWidth ) * 100.0 ;
@@ -594,6 +605,25 @@ private static string BuildContentStream(PdfPage page, bool hasUnicodeFont, Dict
594605 // Restore graphics state after clipping
595606 if ( hasClip )
596607 sb . Append ( "Q\n " ) ;
608+
609+ // Render underline as a line below the text
610+ if ( block . Underline )
611+ {
612+ var textWidth = MeasureTextWidth ( block . Text , block . FontSize , block . CharSpacing ) ;
613+ if ( block . MaxWidth . HasValue && textWidth > block . MaxWidth . Value )
614+ textWidth = block . MaxWidth . Value ;
615+ var ulY = block . Y - block . FontSize * 0.15f ; // position below baseline
616+ var ulThickness = Math . Max ( 0.5f , block . FontSize * 0.05f ) ;
617+ var x1 = block . X . ToString ( "F3" , CultureInfo . InvariantCulture ) ;
618+ var y1 = ulY . ToString ( "F3" , CultureInfo . InvariantCulture ) ;
619+ var x2 = ( block . X + textWidth ) . ToString ( "F3" , CultureInfo . InvariantCulture ) ;
620+ var lw = ulThickness . ToString ( "F3" , CultureInfo . InvariantCulture ) ;
621+ sb . Append ( $ "{ block . Color . R . ToString ( "F3" , CultureInfo . InvariantCulture ) } " +
622+ $ "{ block . Color . G . ToString ( "F3" , CultureInfo . InvariantCulture ) } " +
623+ $ "{ block . Color . B . ToString ( "F3" , CultureInfo . InvariantCulture ) } RG\n ") ;
624+ sb . Append ( $ "{ lw } w\n ") ;
625+ sb . Append ( $ "{ x1 } { y1 } m { x2 } { y1 } l S\n ") ;
626+ }
597627 }
598628
599629 return sb . ToString ( ) ;
@@ -603,7 +633,7 @@ private static string BuildContentStream(PdfPage page, bool hasUnicodeFont, Dict
603633 /// Measures the natural rendering width of text in Helvetica at the given font size.
604634 /// Uses the standard Helvetica character width table.
605635 /// </summary>
606- private static double MeasureTextWidth ( string text , float fontSize )
636+ private static double MeasureTextWidth ( string text , float fontSize , float charSpacing = 0 )
607637 {
608638 double total = 0 ;
609639 foreach ( var ch in text )
@@ -633,7 +663,11 @@ private static double MeasureTextWidth(string text, float fontSize)
633663 } ;
634664 total += w ;
635665 }
636- return total * fontSize / 1000.0 ;
666+ var result = total * fontSize / 1000.0 ;
667+ // Tc adds charSpacing points per character (except after the last)
668+ if ( charSpacing != 0 && text . Length > 1 )
669+ result += charSpacing * ( text . Length - 1 ) ;
670+ return result ;
637671 }
638672
639673 /// <summary>
@@ -806,12 +840,12 @@ private static string EscapePdfString(string text)
806840 '\u20AC ' => ( char ) 0x80 , // euro sign
807841 '\u00A0 ' => ' ' , // non-breaking space
808842 '\u0060 ' => '\' ' , // backtick → apostrophe
809- '\u00B7 ' => '* ' , // middle dot → asterisk
810- '\u00D7 ' => 'x ' , // multiplication sign
811- '\u00F7 ' => '/ ' , // division sign
843+ '\u00B7 ' => '\u00B7 ' , // middle dot (already in WinAnsi)
844+ '\u00D7 ' => '\u00D7 ' , // multiplication sign (already in WinAnsi)
845+ '\u00F7 ' => '\u00F7 ' , // division sign (already in WinAnsi)
812846 '\u2264 ' => "<=" , // ≤
813847 '\u2265 ' => ">=" , // ≥
814- '\u00B0 ' => " deg" , // degree sign
848+ '\u00B0 ' => ' \u00B0 ' , // degree sign (already in WinAnsi)
815849 '\u00AE ' => ( char ) 0xAE , // registered trademark (already in WinAnsi)
816850 '\u00A3 ' => '\u00A3 ' , // pound sign (already in WinAnsi)
817851 '\u00A5 ' => '\u00A5 ' , // yen sign (already in WinAnsi)
0 commit comments