Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
255 changes: 193 additions & 62 deletions cache_content_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,35 +160,158 @@ func (c *cacheContentText) write(w io.Writer, protection *PDFProtection) error {
if c.txtColorMode == "color" {
c.textColor.write(w, protection)
}
io.WriteString(w, "[<")

unitsPerEm := int(c.fontSubset.ttfp.UnitsPerEm())
var leftRune rune
var leftRuneIndex uint
for i, r := range c.text {

glyphindex, err := c.fontSubset.CharIndex(r)
if err == ErrCharNotFound {
continue
} else if err != nil {
return err
}

pairvalPdfUnit := 0
if i > 0 && c.fontSubset.ttfFontOption.UseKerning { //kerning
pairval := kern(c.fontSubset, leftRune, r, leftRuneIndex, glyphindex)
pairvalPdfUnit = convertTTFUnit2PDFUnit(int(pairval), unitsPerEm)
if pairvalPdfUnit != 0 {
fmt.Fprintf(w, ">%d<", (-1)*pairvalPdfUnit)
}
// shape the text and collect glyphs with advances and offsets
glyphs, adv, xoffs, yoffs, err := c.fontSubset.shapeTextMetrics(c.text, c.fontSize, c.charSpacing)
if err != nil {
return err
}
upem := int(c.fontSubset.ttfp.UnitsPerEm())
// Calibrate shaped advances to base widths so totals match even if font scale differs
sumAdv := 0
sumBase := 0
widths := c.fontSubset.ttfp.Widths()
for i := range glyphs {
sumAdv += adv[i]
gi := int(glyphs[i])
if gi < len(widths) {
sumBase += int(widths[gi])
} else if len(widths) > 0 {
sumBase += int(widths[len(widths)-1])
}

fmt.Fprintf(w, "%04X", glyphindex)
leftRune = r
leftRuneIndex = glyphindex
}
alpha := 1.0
if sumAdv != 0 {
alpha = float64(sumBase) / float64(sumAdv)
}
// Strategy: write normal glyphs in a single TJ segment using shaped advances only (no offsets),
// and isolate zero-advance marks to apply X/Y offsets locally with Ts/Td while cancelling width.
n := len(glyphs)
// precompute charSpacing contribution in PDF 1000 units
charPdf := 0
if c.charSpacing != 0 {
unitsPerPt := float64(upem) / float64(c.fontSize)
spaceWidthInTtf := int(math.Round(unitsPerPt * c.charSpacing))
charPdf = convertTTFUnit2PDFUnit(spaceWidthInTtf, upem)
}

// absolute per-glyph placement (robust for complex scripts; keeps text selectable)
{
xaccTTF := 0 // accumulate in TTF units
pairs := 0
for i := 0; i < n; i++ {
xAll := xaccTTF + xoffs[i]
xpts := x + float64(convertTTFUnit2PDFUnit(xAll, upem)) * (c.fontSize / 1000.0) + float64(pairs)*c.charSpacing
ypts := y + float64(convertTTFUnit2PDFUnit(yoffs[i], upem)) * (c.fontSize / 1000.0)
fmt.Fprintf(w, "1 0 0 1 %s %s Tm <%04X> Tj\n", FormatFloatTrim(xpts), FormatFloatTrim(ypts), glyphs[i])
// accumulate raw advance in TTF units
xaccTTF += adv[i]
if i+1 < n { pairs++ }
}
io.WriteString(w, "ET\n")
if c.fontStyle&Underline == Underline {
if err := c.underline(w); err != nil { return err }
}
c.drawBorder(w)
return nil
}

// helper: emit normal run s..e inclusive (constant Ts is assumed already set by caller)
writeNormal := func(s, e int, bridgeNext bool, next int) {
Comment thread
netDinger marked this conversation as resolved.
Outdated
if s > e { return }
io.WriteString(w, "[")
for i := s; i <= e; i++ {
// Apply delta XOffset as a pre-number: pre_i = -(xoff[i]-xoff[i-1]), first uses prev=0
prePdf := 0.0
prevX := 0
if i > s { prevX = xoffs[i-1] }
dx := xoffs[i] - prevX
if dx != 0 {
prePdf = -1000.0 * float64(dx) / (64.0 * float64(upem))
fmt.Fprintf(w, " %s ", FormatFloatTrim(prePdf))
}
fmt.Fprintf(w, "<%04X>", glyphs[i])
if i < e {
advScaledTTF := int(math.Round(float64(adv[i]) * alpha))
advPdfF := 1000.0 * float64(advScaledTTF) / float64(upem)
baseF := float64(int(c.fontSubset.GlyphIndexToPdfWidth(glyphs[i])))
// Inter-glyph spacing with delta-pre: a = base + char - adv
aF := baseF + float64(charPdf) - advPdfF
if aF != 0 {
fmt.Fprintf(w, " %s ", FormatFloatTrim(aF))
} else {
io.WriteString(w, " ")
}
}
}
// bridging adjustment to align to the next glyph origin (e.g., when the next glyph is a mark)
if bridgeNext && e >= s && next >= 0 && next < n {
advScaledTTF := int(math.Round(float64(adv[e]) * alpha))
advPdfF := 1000.0 * float64(advScaledTTF) / float64(upem)
baseF := float64(int(c.fontSubset.GlyphIndexToPdfWidth(glyphs[e])))
// bridge to mark: a = base + char - adv (mark will apply its own delta pre)
aF := baseF + float64(charPdf) - advPdfF
if aF != 0 {
fmt.Fprintf(w, " %s ", FormatFloatTrim(aF))
}
}
// tail adjustment when no bridge: ensure last glyph uses shaped advance without char spacing
if !bridgeNext && e >= s {
advScaledTTF := int(math.Round(float64(adv[e]) * alpha))
advPdfF := 1000.0 * float64(advScaledTTF) / float64(upem)
baseF := float64(int(c.fontSubset.GlyphIndexToPdfWidth(glyphs[e])))
// tail (no next glyph in this run): a = base - adv
aF := baseF - advPdfF
if aF != 0 {
fmt.Fprintf(w, " %s ", FormatFloatTrim(aF))
}
}
io.WriteString(w, "] TJ\n")
}

segStart := 0
for i := 0; i < n; i++ {
if adv[i] == 0 { // combining mark
// flush normals before the mark, and add a bridge to align to this mark's shaped origin
writeNormal(segStart, i-1, true, i)

// apply Y offset (rise) for the mark using precise 26.6 -> points conversion
risePts := (float64(yoffs[i]) * c.fontSize) / (64.0 * float64(upem))
if risePts != 0 { fmt.Fprintf(w, "%s Ts\n", FormatFloatTrim(risePts)) } else { io.WriteString(w, "0 Ts\n") }

// draw mark using pre-number for XOffset and cancel width so net advance equals shaped advance
io.WriteString(w, "[")
// delta pre for mark from previous glyph's offset
prePdf := 0.0
if i > 0 {
dmx := xoffs[i] - xoffs[i-1]
if dmx != 0 {
prePdf = -1000.0 * float64(dmx) / (64.0 * float64(upem))
fmt.Fprintf(w, " %s ", FormatFloatTrim(prePdf))
}
} else if xoffs[i] != 0 {
prePdf = -1000.0 * float64(xoffs[i]) / (64.0 * float64(upem))
fmt.Fprintf(w, " %s ", FormatFloatTrim(prePdf))
}
fmt.Fprintf(w, "<%04X>", glyphs[i])
baseF := float64(int(c.fontSubset.GlyphIndexToPdfWidth(glyphs[i])))
advScaledTTF := int(math.Round(float64(adv[i]) * alpha))
advPdfF := 1000.0 * float64(advScaledTTF) / float64(upem)
// For mark: include char spacing only if there is a following glyph in the same line
nextChar := 0.0
if i+1 < n { nextChar = float64(charPdf) }
// mark advance is zero: a = base + nextChar - adv
aF := baseF + nextChar - advPdfF
fmt.Fprintf(w, " %s ] TJ\n", FormatFloatTrim(aF))

// reset rise back to zero for following normals
io.WriteString(w, "0 Ts\n")

segStart = i + 1
}
}
// tail normals
writeNormal(segStart, n-1, false, -1)

io.WriteString(w, ">] TJ\n")
io.WriteString(w, "ET\n")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code

Copy link
Copy Markdown
Author

@netDinger netDinger Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just fixed that. Kindly review my commit please.


if c.fontStyle&Underline == Underline {
Expand Down Expand Up @@ -308,40 +431,48 @@ func (c *cacheContentText) createContent() (float64, float64, error) {
}

func createContent(f *SubsetFontObj, text string, fontSize float64, charSpacing float64, rectangle *Rect) (float64, float64, float64, error) {

unitsPerEm := int(f.ttfp.UnitsPerEm())
var leftRune rune
var leftRuneIndex uint
sumWidth := int(0)
//fmt.Printf("unitsPerEm = %d", unitsPerEm)
for i, r := range text {

glyphindex, err := f.CharIndex(r)
if err == ErrCharNotFound {
continue
} else if err != nil {
return 0, 0, 0, err
}

pairvalPdfUnit := 0
if i > 0 && f.ttfFontOption.UseKerning { //kerning
pairval := kern(f, leftRune, r, leftRuneIndex, glyphindex)
pairvalPdfUnit = convertTTFUnit2PDFUnit(int(pairval), unitsPerEm)
}

width, err := f.CharWidth(r)
if err != nil {
return 0, 0, 0, err
}

unitsPerPt := float64(unitsPerEm) / fontSize
spaceWidthInPt := unitsPerPt * charSpacing
spaceWidthPdfUnit := convertTTFUnit2PDFUnit(int(spaceWidthInPt), unitsPerEm)

sumWidth += int(width) + int(pairvalPdfUnit) + spaceWidthPdfUnit
leftRune = r
leftRuneIndex = glyphindex
}
// Use shaping to compute precise width (including GPOS kerning). Offsets do not affect advance width.
glyphs, adv, _, _, err := f.shapeTextMetrics(text, fontSize, charSpacing)
if err != nil {
return 0, 0, 0, err
}
// Calibrate shaped advances to base widths to keep placement and width consistent
sumAdv := 0
sumBase := 0
widths := f.ttfp.Widths()
for i := range glyphs {
sumAdv += adv[i]
gi := int(glyphs[i])
if gi < len(widths) {
sumBase += int(widths[gi])
} else if len(widths) > 0 {
sumBase += int(widths[len(widths)-1])
}
}
alpha := 1.0
if sumAdv != 0 {
alpha = float64(sumBase) / float64(sumAdv)
}
// Sum width in PDF units using: sum(adv[0..n-2]) + base[n-1]
unitsPerEm := int(f.ttfp.UnitsPerEm())
advSumPdf := 0
n := len(glyphs)
for i := 0; i < n-1; i++ {
advScaledTTF := int(math.Round(float64(adv[i]) * alpha))
advSumPdf += convertTTFUnit2PDFUnit(advScaledTTF, unitsPerEm)
}
lastBasePdf := 0
if n > 0 {
lastBasePdf = int(f.GlyphIndexToPdfWidth(glyphs[n-1]))
}
sumWidth := advSumPdf + lastBasePdf
// Add charSpacing between glyphs (N-1 times), as applied by the PDF Tc operator
if n > 1 && charSpacing != 0 {
unitsPerPt := float64(unitsPerEm) / fontSize
spaceWidthInTtf := unitsPerPt * charSpacing
spaceWidthPdfUnit := convertTTFUnit2PDFUnit(int(spaceWidthInTtf), unitsPerEm)
sumWidth += (n - 1) * spaceWidthPdfUnit
}

cellWidthPdfUnit := float64(0)
cellHeightPdfUnit := float64(0)
Expand All @@ -354,8 +485,8 @@ func createContent(f *SubsetFontObj, text string, fontSize float64, charSpacing
cellWidthPdfUnit = rectangle.W
cellHeightPdfUnit = rectangle.H
}
textWidthPdfUnit := float64(sumWidth) * (float64(fontSize) / 1000.0)
return cellWidthPdfUnit, cellHeightPdfUnit, textWidthPdfUnit, nil
textWidthPdfUnit := float64(sumWidth) * (float64(fontSize) / 1000.0)
return cellWidthPdfUnit, cellHeightPdfUnit, textWidthPdfUnit, nil
}

func kern(f *SubsetFontObj, leftRune rune, rightRune rune, leftIndex uint, rightIndex uint) int16 {
Expand Down
13 changes: 13 additions & 0 deletions cid_font_obj.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,22 @@ func (ci *CIDFontObj) write(w io.Writer, objID int) error {
fmt.Fprintf(w, "/FontDescriptor %d 0 R\n", ci.indexObjSubfontDescriptor+1) //TODO fix
io.WriteString(w, "/Subtype /CIDFontType2\n")
io.WriteString(w, "/Type /Font\n")
// Build width list including shaped-only glyphs
glyphIndexs := ci.PtrToSubsetFontObj.CharacterToGlyphIndex.AllVals()
if extra := ci.PtrToSubsetFontObj.ExtraGlyphs(); extra != nil {
for gid := range extra {
glyphIndexs = append(glyphIndexs, gid)
}
}
// de-duplicate while preserving order (small lists)
seen := make(map[uint]bool)
io.WriteString(w, "/DW 0\n")
io.WriteString(w, "/W [")
for _, v := range glyphIndexs {
if seen[v] {
continue
}
seen[v] = true
width := ci.PtrToSubsetFontObj.GlyphIndexToPdfWidth(v)
fmt.Fprintf(w, "%d[%d]", v, width)
}
Expand Down
12 changes: 10 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
module github.com/signintech/gopdf

go 1.13
go 1.20

require github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311
require (
github.com/go-text/typesetting v0.3.0
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311
)

require (
github.com/pkg/errors v0.8.1 // indirect
golang.org/x/image v0.23.0 // indirect
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
6 changes: 6 additions & 0 deletions pdf_dictionary_obj.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ func (p *PdfDictionaryObj) makeGlyfAndLocaTable() ([]byte, []int, error) {
numGlyphs := int(ttfp.NumGlyphs())

glyphArray := p.completeGlyphClosure(p.PtrToSubsetFontObj.CharacterToGlyphIndex)
// also include any shaped-only glyphs collected during layout
if extra := p.PtrToSubsetFontObj.ExtraGlyphs(); extra != nil {
for gid := range extra {
glyphArray = append(glyphArray, int(gid))
}
}
sort.Ints(glyphArray)
glyphArray = p.distinctInts(glyphArray)
glyphCount := len(glyphArray)
Expand Down
6 changes: 6 additions & 0 deletions subset_font_obj.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"io"

"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/harfbuzz"
"github.com/signintech/gopdf/fontmaker/core"
)

Expand All @@ -26,6 +28,10 @@ type SubsetFontObj struct {
funcKernOverride FuncKernOverride
funcGetRoot func() *GoPdf
addCharsBuff []rune
// harfbuzz/typesetting integration
hbFace *font.Face
hbFont *harfbuzz.Font
extraGlyphs map[uint]rune
}

func (s *SubsetFontObj) init(funcGetRoot func() *GoPdf) {
Expand Down
Binary file removed test/res/LiberationSerif-Regular.ttf
Binary file not shown.
Loading
Loading