Skip to content

Commit eef3b8f

Browse files
adrggunnsth
authored andcommitted
Improve text chunk component (#181)
* Add more functionality to text chunks * Add creator text chunk test cases * Improve documentation of the text chunk Fit method
1 parent b7dc677 commit eef3b8f

File tree

4 files changed

+256
-78
lines changed

4 files changed

+256
-78
lines changed

creator/paragraph.go

Lines changed: 11 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -260,81 +260,20 @@ func (p *Paragraph) wrapText() error {
260260
return nil
261261
}
262262

263-
var line []rune
264-
lineWidth := 0.0
265-
p.textLines = nil
263+
chunk := NewTextChunk(p.text, TextStyle{
264+
Font: p.textFont,
265+
FontSize: p.fontSize,
266+
})
266267

267-
runes := []rune(p.text)
268-
var widths []float64
269-
270-
for _, r := range runes {
271-
// Newline wrapping.
272-
if r == '\u000A' { // LF
273-
// Moves to next line.
274-
p.textLines = append(p.textLines, string(line))
275-
line = nil
276-
lineWidth = 0
277-
widths = nil
278-
continue
279-
}
280-
281-
metrics, found := p.textFont.GetRuneMetrics(r)
282-
if !found {
283-
common.Log.Debug("ERROR: Rune char metrics not found! rune=0x%04x=%c font=%s %#q",
284-
r, r, p.textFont.BaseFont(), p.textFont.Subtype())
285-
common.Log.Trace("Font: %#v", p.textFont)
286-
common.Log.Trace("Encoder: %#v", p.textFont.Encoder())
287-
return errors.New("glyph char metrics missing")
288-
}
289-
290-
w := p.fontSize * metrics.Wx
291-
if lineWidth+w > p.wrapWidth*1000.0 {
292-
// Goes out of bounds: Wrap.
293-
// Breaks on the character.
294-
idx := -1
295-
for i := len(line) - 1; i >= 0; i-- {
296-
if line[i] == ' ' { // TODO: What about other space glyphs like controlHT?
297-
idx = i
298-
break
299-
}
300-
}
301-
if idx > 0 {
302-
// Back up to last space.
303-
p.textLines = append(p.textLines, string(line[0:idx+1]))
304-
305-
// Remainder of line.
306-
line = append(line[idx+1:], r)
307-
widths = append(widths[idx+1:], w)
308-
lineWidth = sum(widths)
309-
310-
} else {
311-
p.textLines = append(p.textLines, string(line))
312-
line = []rune{r}
313-
widths = []float64{w}
314-
lineWidth = w
315-
}
316-
} else {
317-
line = append(line, r)
318-
lineWidth += w
319-
widths = append(widths, w)
320-
}
321-
}
322-
if len(line) > 0 {
323-
p.textLines = append(p.textLines, string(line))
268+
lines, err := chunk.Wrap(p.wrapWidth)
269+
if err != nil {
270+
return err
324271
}
325272

273+
p.textLines = lines
326274
return nil
327275
}
328276

329-
// sum returns the sums of the elements in `widths`.
330-
func sum(widths []float64) float64 {
331-
total := 0.0
332-
for _, w := range widths {
333-
total += w
334-
}
335-
return total
336-
}
337-
338277
// GeneratePageBlocks generates the page blocks. Multiple blocks are generated if the contents wrap
339278
// over multiple pages. Implements the Drawable interface.
340279
func (p *Paragraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
@@ -489,6 +428,9 @@ func drawParagraphOnBlock(blk *Block, p *Paragraph, ctx DrawContext) (DrawContex
489428

490429
var encoded []byte
491430
for _, r := range runes {
431+
if r == '\u000A' { // LF
432+
continue
433+
}
492434
if r == ' ' { // TODO: What about \t and other spaces.
493435
if len(encoded) > 0 {
494436
objs = append(objs, core.MakeStringFromBytes(encoded))

creator/styled_paragraph.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (p *StyledParagraph) appendChunk(chunk *TextChunk) *TextChunk {
9696

9797
// Append adds a new text chunk to the paragraph.
9898
func (p *StyledParagraph) Append(text string) *TextChunk {
99-
chunk := newTextChunk(text, p.defaultStyle)
99+
chunk := NewTextChunk(text, p.defaultStyle)
100100
return p.appendChunk(chunk)
101101
}
102102

@@ -107,7 +107,7 @@ func (p *StyledParagraph) Insert(index uint, text string) *TextChunk {
107107
index = l
108108
}
109109

110-
chunk := newTextChunk(text, p.defaultStyle)
110+
chunk := NewTextChunk(text, p.defaultStyle)
111111
p.chunks = append(p.chunks[:index], append([]*TextChunk{chunk}, p.chunks[index:]...)...)
112112
p.wrapText()
113113

@@ -118,7 +118,7 @@ func (p *StyledParagraph) Insert(index uint, text string) *TextChunk {
118118
// The text parameter represents the text that is displayed and the url
119119
// parameter sets the destionation of the link.
120120
func (p *StyledParagraph) AddExternalLink(text, url string) *TextChunk {
121-
chunk := newTextChunk(text, p.defaultLinkStyle)
121+
chunk := NewTextChunk(text, p.defaultLinkStyle)
122122
chunk.annotation = newExternalLinkAnnotation(url)
123123
return p.appendChunk(chunk)
124124
}
@@ -130,7 +130,7 @@ func (p *StyledParagraph) AddExternalLink(text, url string) *TextChunk {
130130
// The zoom of the destination page is controlled with the zoom
131131
// parameter. Pass in 0 to keep the current zoom value.
132132
func (p *StyledParagraph) AddInternalLink(text string, page int64, x, y, zoom float64) *TextChunk {
133-
chunk := newTextChunk(text, p.defaultLinkStyle)
133+
chunk := NewTextChunk(text, p.defaultLinkStyle)
134134
chunk.annotation = newInternalLinkAnnotation(page-1, x, y, zoom)
135135
return p.appendChunk(chunk)
136136
}
@@ -745,6 +745,9 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
745745

746746
var encStr []byte
747747
for _, rn := range chunk.Text {
748+
if r == '\u000A' { // LF
749+
continue
750+
}
748751
if rn == ' ' {
749752
if len(encStr) > 0 {
750753
cc.Add_rg(r, g, b).

creator/text_chunk.go

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
package creator
77

88
import (
9+
"errors"
10+
"strings"
11+
"unicode"
12+
13+
"github.com/unidoc/unipdf/v3/common"
914
"github.com/unidoc/unipdf/v3/core"
1015
"github.com/unidoc/unipdf/v3/model"
1116
)
@@ -26,17 +31,115 @@ type TextChunk struct {
2631
annotationProcessed bool
2732
}
2833

34+
// NewTextChunk returns a new text chunk instance.
35+
func NewTextChunk(text string, style TextStyle) *TextChunk {
36+
return &TextChunk{
37+
Text: text,
38+
Style: style,
39+
}
40+
}
41+
2942
// SetAnnotation sets a annotation on a TextChunk.
3043
func (tc *TextChunk) SetAnnotation(annotation *model.PdfAnnotation) {
3144
tc.annotation = annotation
3245
}
3346

34-
// newTextChunk returns a new text chunk instance.
35-
func newTextChunk(text string, style TextStyle) *TextChunk {
36-
return &TextChunk{
37-
Text: text,
38-
Style: style,
47+
// Wrap wraps the text of the chunk into lines based on its style and the
48+
// specified width.
49+
func (tc *TextChunk) Wrap(width float64) ([]string, error) {
50+
if int(width) <= 0 {
51+
return []string{tc.Text}, nil
3952
}
53+
54+
var lines []string
55+
var line []rune
56+
var lineWidth float64
57+
var widths []float64
58+
59+
style := tc.Style
60+
runes := []rune(tc.Text)
61+
62+
for _, r := range runes {
63+
// Move to the next line due to newline wrapping (LF).
64+
if r == '\u000A' {
65+
lines = append(lines, strings.TrimRightFunc(string(line), unicode.IsSpace)+string(r))
66+
line = nil
67+
lineWidth = 0
68+
widths = nil
69+
continue
70+
}
71+
72+
metrics, found := style.Font.GetRuneMetrics(r)
73+
if !found {
74+
common.Log.Debug("ERROR: Rune char metrics not found! rune=0x%04x=%c font=%s %#q",
75+
r, r, style.Font.BaseFont(), style.Font.Subtype())
76+
common.Log.Trace("Font: %#v", style.Font)
77+
common.Log.Trace("Encoder: %#v", style.Font.Encoder())
78+
return nil, errors.New("glyph char metrics missing")
79+
}
80+
81+
w := style.FontSize * metrics.Wx
82+
charWidth := w + style.CharSpacing*1000.0
83+
if lineWidth+w > width*1000.0 {
84+
// Goes out of bounds. Break on the character.
85+
idx := -1
86+
for i := len(line) - 1; i >= 0; i-- {
87+
if line[i] == ' ' {
88+
idx = i
89+
break
90+
}
91+
}
92+
if idx > 0 {
93+
// Back up to last space.
94+
lines = append(lines, strings.TrimRightFunc(string(line[0:idx+1]), unicode.IsSpace))
95+
96+
// Remainder of line.
97+
line = append(line[idx+1:], r)
98+
widths = append(widths[idx+1:], charWidth)
99+
100+
lineWidth = 0
101+
for _, width := range widths {
102+
lineWidth += width
103+
}
104+
} else {
105+
lines = append(lines, strings.TrimRightFunc(string(line), unicode.IsSpace))
106+
line = []rune{r}
107+
widths = []float64{charWidth}
108+
lineWidth = charWidth
109+
}
110+
} else {
111+
line = append(line, r)
112+
lineWidth += charWidth
113+
widths = append(widths, charWidth)
114+
}
115+
}
116+
if len(line) > 0 {
117+
lines = append(lines, string(line))
118+
}
119+
120+
return lines, nil
121+
}
122+
123+
// Fit fits the chunk into the specified bounding box, cropping off the
124+
// remainder in a new chunk, if it exceeds the specified dimensions.
125+
// NOTE: The method assumes a line height of 1.0. In order to account for other
126+
// line height values, the passed in height must be divided by the line height:
127+
// height = height / lineHeight
128+
func (tc *TextChunk) Fit(width, height float64) (*TextChunk, error) {
129+
lines, err := tc.Wrap(width)
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
fit := int(height / tc.Style.FontSize)
135+
if fit >= len(lines) {
136+
return nil, nil
137+
}
138+
lf := "\u000A"
139+
tc.Text = strings.Replace(strings.Join(lines[:fit], " "), lf+" ", lf, -1)
140+
141+
remainder := strings.Replace(strings.Join(lines[fit:], " "), lf+" ", lf, -1)
142+
return NewTextChunk(remainder, tc.Style), nil
40143
}
41144

42145
// newExternalLinkAnnotation returns a new external link annotation.

0 commit comments

Comments
 (0)