mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-27 13:48:51 +08:00

* Take spaces into account when wrapping styled paragraph text * Fix starting position of styled paragraphs * Wrap text unconditionally when style paragraph height is requested * Fix styled paragraph text wrapping edge case * Fix text chunk wrapping edge case * Adapt text chunk test * Wrap text unconditionally when paragraph height is requested * Add text wrapping test case for styled paragraph using character spacing
222 lines
5.4 KiB
Go
222 lines
5.4 KiB
Go
/*
|
|
* This file is subject to the terms and conditions defined in
|
|
* file 'LICENSE.md', which is part of this source code package.
|
|
*/
|
|
|
|
package creator
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/unidoc/unipdf/v3/common"
|
|
"github.com/unidoc/unipdf/v3/core"
|
|
"github.com/unidoc/unipdf/v3/model"
|
|
)
|
|
|
|
// TextChunk represents a chunk of text along with a particular style.
|
|
type TextChunk struct {
|
|
// The text that is being rendered in the PDF.
|
|
Text string
|
|
|
|
// The style of the text being rendered.
|
|
Style TextStyle
|
|
|
|
// Text chunk annotation.
|
|
annotation *model.PdfAnnotation
|
|
|
|
// Internally used in order to skip processing the annotation
|
|
// if it has already been processed by the parent component.
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
isSpace := r == ' '
|
|
|
|
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
|
|
if !isSpace {
|
|
charWidth = w + style.CharSpacing*1000.0
|
|
}
|
|
|
|
if lineWidth+w > width*1000.0 {
|
|
// Goes out of bounds. Break on the character.
|
|
idx := -1
|
|
if !isSpace {
|
|
for i := len(line) - 1; i >= 0; i-- {
|
|
if line[i] == ' ' {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
text := string(line)
|
|
if idx > 0 {
|
|
// Back up to last space.
|
|
text = string(line[0 : idx+1])
|
|
|
|
// Remainder of line.
|
|
line = append(line[idx+1:], r)
|
|
widths = append(widths[idx+1:], charWidth)
|
|
|
|
lineWidth = 0
|
|
for _, width := range widths {
|
|
lineWidth += width
|
|
}
|
|
} else {
|
|
if isSpace {
|
|
line = []rune{}
|
|
widths = []float64{}
|
|
lineWidth = 0
|
|
} else {
|
|
line = []rune{r}
|
|
widths = []float64{charWidth}
|
|
lineWidth = charWidth
|
|
}
|
|
}
|
|
|
|
lines = append(lines, strings.TrimRightFunc(text, unicode.IsSpace))
|
|
} 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.
|
|
func newExternalLinkAnnotation(url string) *model.PdfAnnotation {
|
|
annotation := model.NewPdfAnnotationLink()
|
|
|
|
// Set border style.
|
|
bs := model.NewBorderStyle()
|
|
bs.SetBorderWidth(0)
|
|
annotation.BS = bs.ToPdfObject()
|
|
|
|
// Set link destination.
|
|
action := model.NewPdfActionURI()
|
|
action.URI = core.MakeString(url)
|
|
annotation.SetAction(action.PdfAction)
|
|
|
|
return annotation.PdfAnnotation
|
|
}
|
|
|
|
// newExternalLinkAnnotation returns a new internal link annotation.
|
|
func newInternalLinkAnnotation(page int64, x, y, zoom float64) *model.PdfAnnotation {
|
|
annotation := model.NewPdfAnnotationLink()
|
|
|
|
// Set border style.
|
|
bs := model.NewBorderStyle()
|
|
bs.SetBorderWidth(0)
|
|
annotation.BS = bs.ToPdfObject()
|
|
|
|
// Set link destination.
|
|
if page < 0 {
|
|
page = 0
|
|
}
|
|
|
|
annotation.Dest = core.MakeArray(
|
|
core.MakeInteger(page),
|
|
core.MakeName("XYZ"),
|
|
core.MakeFloat(x),
|
|
core.MakeFloat(y),
|
|
core.MakeFloat(zoom),
|
|
)
|
|
|
|
return annotation.PdfAnnotation
|
|
}
|
|
|
|
// copyLinkAnnotation returns a new link annotation based on an existing one.
|
|
func copyLinkAnnotation(link *model.PdfAnnotationLink) *model.PdfAnnotationLink {
|
|
if link == nil {
|
|
return nil
|
|
}
|
|
|
|
annotation := model.NewPdfAnnotationLink()
|
|
annotation.BS = link.BS
|
|
annotation.A = link.A
|
|
if action, err := link.GetAction(); err == nil && action != nil {
|
|
annotation.SetAction(action)
|
|
}
|
|
|
|
if annotDest, ok := link.Dest.(*core.PdfObjectArray); ok {
|
|
annotation.Dest = core.MakeArray(annotDest.Elements()...)
|
|
}
|
|
|
|
return annotation
|
|
}
|