Skip to content

Commit

Permalink
Improve text chunk component (#181)
Browse files Browse the repository at this point in the history
* Add more functionality to text chunks
* Add creator text chunk test cases
* Improve documentation of the text chunk Fit method
  • Loading branch information
adrg authored and gunnsth committed Oct 8, 2019
1 parent b7dc677 commit eef3b8f
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 78 deletions.
80 changes: 11 additions & 69 deletions creator/paragraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,81 +260,20 @@ func (p *Paragraph) wrapText() error {
return nil
}

var line []rune
lineWidth := 0.0
p.textLines = nil
chunk := NewTextChunk(p.text, TextStyle{
Font: p.textFont,
FontSize: p.fontSize,
})

runes := []rune(p.text)
var widths []float64

for _, r := range runes {
// Newline wrapping.
if r == '\u000A' { // LF
// Moves to next line.
p.textLines = append(p.textLines, string(line))
line = nil
lineWidth = 0
widths = nil
continue
}

metrics, found := p.textFont.GetRuneMetrics(r)
if !found {
common.Log.Debug("ERROR: Rune char metrics not found! rune=0x%04x=%c font=%s %#q",
r, r, p.textFont.BaseFont(), p.textFont.Subtype())
common.Log.Trace("Font: %#v", p.textFont)
common.Log.Trace("Encoder: %#v", p.textFont.Encoder())
return errors.New("glyph char metrics missing")
}

w := p.fontSize * metrics.Wx
if lineWidth+w > p.wrapWidth*1000.0 {
// Goes out of bounds: Wrap.
// Breaks on the character.
idx := -1
for i := len(line) - 1; i >= 0; i-- {
if line[i] == ' ' { // TODO: What about other space glyphs like controlHT?
idx = i
break
}
}
if idx > 0 {
// Back up to last space.
p.textLines = append(p.textLines, string(line[0:idx+1]))

// Remainder of line.
line = append(line[idx+1:], r)
widths = append(widths[idx+1:], w)
lineWidth = sum(widths)

} else {
p.textLines = append(p.textLines, string(line))
line = []rune{r}
widths = []float64{w}
lineWidth = w
}
} else {
line = append(line, r)
lineWidth += w
widths = append(widths, w)
}
}
if len(line) > 0 {
p.textLines = append(p.textLines, string(line))
lines, err := chunk.Wrap(p.wrapWidth)
if err != nil {
return err
}

p.textLines = lines
return nil
}

// sum returns the sums of the elements in `widths`.
func sum(widths []float64) float64 {
total := 0.0
for _, w := range widths {
total += w
}
return total
}

// GeneratePageBlocks generates the page blocks. Multiple blocks are generated if the contents wrap
// over multiple pages. Implements the Drawable interface.
func (p *Paragraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
Expand Down Expand Up @@ -489,6 +428,9 @@ func drawParagraphOnBlock(blk *Block, p *Paragraph, ctx DrawContext) (DrawContex

var encoded []byte
for _, r := range runes {
if r == '\u000A' { // LF
continue
}
if r == ' ' { // TODO: What about \t and other spaces.
if len(encoded) > 0 {
objs = append(objs, core.MakeStringFromBytes(encoded))
Expand Down
11 changes: 7 additions & 4 deletions creator/styled_paragraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (p *StyledParagraph) appendChunk(chunk *TextChunk) *TextChunk {

// Append adds a new text chunk to the paragraph.
func (p *StyledParagraph) Append(text string) *TextChunk {
chunk := newTextChunk(text, p.defaultStyle)
chunk := NewTextChunk(text, p.defaultStyle)
return p.appendChunk(chunk)
}

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

chunk := newTextChunk(text, p.defaultStyle)
chunk := NewTextChunk(text, p.defaultStyle)
p.chunks = append(p.chunks[:index], append([]*TextChunk{chunk}, p.chunks[index:]...)...)
p.wrapText()

Expand All @@ -118,7 +118,7 @@ func (p *StyledParagraph) Insert(index uint, text string) *TextChunk {
// The text parameter represents the text that is displayed and the url
// parameter sets the destionation of the link.
func (p *StyledParagraph) AddExternalLink(text, url string) *TextChunk {
chunk := newTextChunk(text, p.defaultLinkStyle)
chunk := NewTextChunk(text, p.defaultLinkStyle)
chunk.annotation = newExternalLinkAnnotation(url)
return p.appendChunk(chunk)
}
Expand All @@ -130,7 +130,7 @@ func (p *StyledParagraph) AddExternalLink(text, url string) *TextChunk {
// The zoom of the destination page is controlled with the zoom
// parameter. Pass in 0 to keep the current zoom value.
func (p *StyledParagraph) AddInternalLink(text string, page int64, x, y, zoom float64) *TextChunk {
chunk := newTextChunk(text, p.defaultLinkStyle)
chunk := NewTextChunk(text, p.defaultLinkStyle)
chunk.annotation = newInternalLinkAnnotation(page-1, x, y, zoom)
return p.appendChunk(chunk)
}
Expand Down Expand Up @@ -745,6 +745,9 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)

var encStr []byte
for _, rn := range chunk.Text {
if r == '\u000A' { // LF
continue
}
if rn == ' ' {
if len(encStr) > 0 {
cc.Add_rg(r, g, b).
Expand Down
113 changes: 108 additions & 5 deletions creator/text_chunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
package creator

import (
"errors"
"strings"
"unicode"

"github.com/unidoc/unipdf/v3/common"
"github.com/unidoc/unipdf/v3/core"
"github.com/unidoc/unipdf/v3/model"
)
Expand All @@ -26,17 +31,115 @@ type TextChunk struct {
annotationProcessed bool
}

// NewTextChunk returns a new text chunk instance.
func NewTextChunk(text string, style TextStyle) *TextChunk {
return &TextChunk{
Text: text,
Style: style,
}
}

// SetAnnotation sets a annotation on a TextChunk.
func (tc *TextChunk) SetAnnotation(annotation *model.PdfAnnotation) {
tc.annotation = annotation
}

// newTextChunk returns a new text chunk instance.
func newTextChunk(text string, style TextStyle) *TextChunk {
return &TextChunk{
Text: text,
Style: style,
// Wrap wraps the text of the chunk into lines based on its style and the
// specified width.
func (tc *TextChunk) Wrap(width float64) ([]string, error) {
if int(width) <= 0 {
return []string{tc.Text}, nil
}

var lines []string
var line []rune
var lineWidth float64
var widths []float64

style := tc.Style
runes := []rune(tc.Text)

for _, r := range runes {
// Move to the next line due to newline wrapping (LF).
if r == '\u000A' {
lines = append(lines, strings.TrimRightFunc(string(line), unicode.IsSpace)+string(r))
line = nil
lineWidth = 0
widths = nil
continue
}

metrics, found := style.Font.GetRuneMetrics(r)
if !found {
common.Log.Debug("ERROR: Rune char metrics not found! rune=0x%04x=%c font=%s %#q",
r, r, style.Font.BaseFont(), style.Font.Subtype())
common.Log.Trace("Font: %#v", style.Font)
common.Log.Trace("Encoder: %#v", style.Font.Encoder())
return nil, errors.New("glyph char metrics missing")
}

w := style.FontSize * metrics.Wx
charWidth := w + style.CharSpacing*1000.0
if lineWidth+w > width*1000.0 {
// Goes out of bounds. Break on the character.
idx := -1
for i := len(line) - 1; i >= 0; i-- {
if line[i] == ' ' {
idx = i
break
}
}
if idx > 0 {
// Back up to last space.
lines = append(lines, strings.TrimRightFunc(string(line[0:idx+1]), unicode.IsSpace))

// Remainder of line.
line = append(line[idx+1:], r)
widths = append(widths[idx+1:], charWidth)

lineWidth = 0
for _, width := range widths {
lineWidth += width
}
} else {
lines = append(lines, strings.TrimRightFunc(string(line), unicode.IsSpace))
line = []rune{r}
widths = []float64{charWidth}
lineWidth = charWidth
}
} else {
line = append(line, r)
lineWidth += charWidth
widths = append(widths, charWidth)
}
}
if len(line) > 0 {
lines = append(lines, string(line))
}

return lines, nil
}

// Fit fits the chunk into the specified bounding box, cropping off the
// remainder in a new chunk, if it exceeds the specified dimensions.
// NOTE: The method assumes a line height of 1.0. In order to account for other
// line height values, the passed in height must be divided by the line height:
// height = height / lineHeight
func (tc *TextChunk) Fit(width, height float64) (*TextChunk, error) {
lines, err := tc.Wrap(width)
if err != nil {
return nil, err
}

fit := int(height / tc.Style.FontSize)
if fit >= len(lines) {
return nil, nil
}
lf := "\u000A"
tc.Text = strings.Replace(strings.Join(lines[:fit], " "), lf+" ", lf, -1)

remainder := strings.Replace(strings.Join(lines[fit:], " "), lf+" ", lf, -1)
return NewTextChunk(remainder, tc.Style), nil
}

// newExternalLinkAnnotation returns a new external link annotation.
Expand Down
Loading

0 comments on commit eef3b8f

Please sign in to comment.