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

* Add more functionality to text chunks * Add creator text chunk test cases * Improve documentation of the text chunk Fit method
870 lines
22 KiB
Go
870 lines
22 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"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/unidoc/unipdf/v3/common"
|
|
"github.com/unidoc/unipdf/v3/contentstream"
|
|
"github.com/unidoc/unipdf/v3/contentstream/draw"
|
|
"github.com/unidoc/unipdf/v3/core"
|
|
"github.com/unidoc/unipdf/v3/model"
|
|
)
|
|
|
|
// StyledParagraph represents text drawn with a specified font and can wrap across lines and pages.
|
|
// By default occupies the available width in the drawing context.
|
|
type StyledParagraph struct {
|
|
// Text chunks with styles that compose the paragraph.
|
|
chunks []*TextChunk
|
|
|
|
// Style used for the paragraph for spacing and offsets.
|
|
defaultStyle TextStyle
|
|
|
|
// Style used for the paragraph link annotations.
|
|
defaultLinkStyle TextStyle
|
|
|
|
// Text alignment: Align left/right/center/justify.
|
|
alignment TextAlignment
|
|
|
|
// The line relative height (default 1).
|
|
lineHeight float64
|
|
|
|
// Wrapping properties.
|
|
enableWrap bool
|
|
wrapWidth float64
|
|
|
|
// defaultWrap defines whether wrapping has been defined explictly or whether default behavior should
|
|
// be observed. Default behavior depends on context: normally wrap is expected, except for example in
|
|
// table cells wrapping is off by default.
|
|
defaultWrap bool
|
|
|
|
// Rotation angle (degrees).
|
|
angle float64
|
|
|
|
// Margins to be applied around the block when drawing on Page.
|
|
margins margins
|
|
|
|
// Positioning: relative / absolute.
|
|
positioning positioning
|
|
|
|
// Absolute coordinates (when in absolute mode).
|
|
xPos float64
|
|
yPos float64
|
|
|
|
// Scaling factors (1 default).
|
|
scaleX float64
|
|
scaleY float64
|
|
|
|
// Text chunk lines after wrapping to available width.
|
|
lines [][]*TextChunk
|
|
|
|
// Before render callback.
|
|
beforeRender func(p *StyledParagraph, ctx DrawContext)
|
|
}
|
|
|
|
// newStyledParagraph creates a new styled paragraph.
|
|
func newStyledParagraph(style TextStyle) *StyledParagraph {
|
|
// TODO: Can we wrap intellectually, only if given width is known?
|
|
return &StyledParagraph{
|
|
chunks: []*TextChunk{},
|
|
defaultStyle: style,
|
|
defaultLinkStyle: newLinkStyle(style.Font),
|
|
lineHeight: 1.0,
|
|
alignment: TextAlignmentLeft,
|
|
enableWrap: true,
|
|
defaultWrap: true,
|
|
angle: 0,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
positioning: positionRelative,
|
|
}
|
|
}
|
|
|
|
// appendChunk adds the provided text chunk to the paragraph.
|
|
func (p *StyledParagraph) appendChunk(chunk *TextChunk) *TextChunk {
|
|
p.chunks = append(p.chunks, chunk)
|
|
p.wrapText()
|
|
return chunk
|
|
}
|
|
|
|
// Append adds a new text chunk to the paragraph.
|
|
func (p *StyledParagraph) Append(text string) *TextChunk {
|
|
chunk := NewTextChunk(text, p.defaultStyle)
|
|
return p.appendChunk(chunk)
|
|
}
|
|
|
|
// Insert adds a new text chunk at the specified position in the paragraph.
|
|
func (p *StyledParagraph) Insert(index uint, text string) *TextChunk {
|
|
l := uint(len(p.chunks))
|
|
if index > l {
|
|
index = l
|
|
}
|
|
|
|
chunk := NewTextChunk(text, p.defaultStyle)
|
|
p.chunks = append(p.chunks[:index], append([]*TextChunk{chunk}, p.chunks[index:]...)...)
|
|
p.wrapText()
|
|
|
|
return chunk
|
|
}
|
|
|
|
// AddExternalLink adds a new external link to the paragraph.
|
|
// 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.annotation = newExternalLinkAnnotation(url)
|
|
return p.appendChunk(chunk)
|
|
}
|
|
|
|
// AddInternalLink adds a new internal link to the paragraph.
|
|
// The text parameter represents the text that is displayed.
|
|
// The user is taken to the specified page, at the specified x and y
|
|
// coordinates. Position 0, 0 is at the top left of the page.
|
|
// 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.annotation = newInternalLinkAnnotation(page-1, x, y, zoom)
|
|
return p.appendChunk(chunk)
|
|
}
|
|
|
|
// Reset removes all the text chunks the paragraph contains.
|
|
func (p *StyledParagraph) Reset() {
|
|
p.chunks = []*TextChunk{}
|
|
}
|
|
|
|
// SetText replaces all the text of the paragraph with the specified one.
|
|
func (p *StyledParagraph) SetText(text string) *TextChunk {
|
|
p.Reset()
|
|
return p.Append(text)
|
|
}
|
|
|
|
// SetTextAlignment sets the horizontal alignment of the text within the space provided.
|
|
func (p *StyledParagraph) SetTextAlignment(align TextAlignment) {
|
|
p.alignment = align
|
|
}
|
|
|
|
// SetLineHeight sets the line height (1.0 default).
|
|
func (p *StyledParagraph) SetLineHeight(lineheight float64) {
|
|
p.lineHeight = lineheight
|
|
}
|
|
|
|
// SetEnableWrap sets the line wrapping enabled flag.
|
|
func (p *StyledParagraph) SetEnableWrap(enableWrap bool) {
|
|
p.enableWrap = enableWrap
|
|
p.defaultWrap = false
|
|
}
|
|
|
|
// SetPos sets absolute positioning with specified coordinates.
|
|
func (p *StyledParagraph) SetPos(x, y float64) {
|
|
p.positioning = positionAbsolute
|
|
p.xPos = x
|
|
p.yPos = y
|
|
}
|
|
|
|
// SetAngle sets the rotation angle of the text.
|
|
func (p *StyledParagraph) SetAngle(angle float64) {
|
|
p.angle = angle
|
|
}
|
|
|
|
// SetMargins sets the Paragraph's margins.
|
|
func (p *StyledParagraph) SetMargins(left, right, top, bottom float64) {
|
|
p.margins.left = left
|
|
p.margins.right = right
|
|
p.margins.top = top
|
|
p.margins.bottom = bottom
|
|
}
|
|
|
|
// GetMargins returns the Paragraph's margins: left, right, top, bottom.
|
|
func (p *StyledParagraph) GetMargins() (float64, float64, float64, float64) {
|
|
return p.margins.left, p.margins.right, p.margins.top, p.margins.bottom
|
|
}
|
|
|
|
// SetWidth sets the the Paragraph width. This is essentially the wrapping width,
|
|
// i.e. the width the text can extend to prior to wrapping over to next line.
|
|
func (p *StyledParagraph) SetWidth(width float64) {
|
|
p.wrapWidth = width
|
|
p.wrapText()
|
|
}
|
|
|
|
// Width returns the width of the Paragraph.
|
|
func (p *StyledParagraph) Width() float64 {
|
|
if p.enableWrap && int(p.wrapWidth) > 0 {
|
|
return p.wrapWidth
|
|
}
|
|
|
|
return p.getTextWidth() / 1000.0
|
|
}
|
|
|
|
// Height returns the height of the Paragraph. The height is calculated based on the input text and how it is wrapped
|
|
// within the container. Does not include Margins.
|
|
func (p *StyledParagraph) Height() float64 {
|
|
if p.lines == nil || len(p.lines) == 0 {
|
|
p.wrapText()
|
|
}
|
|
|
|
var height float64
|
|
for _, line := range p.lines {
|
|
var lineHeight float64
|
|
for _, chunk := range line {
|
|
h := p.lineHeight * chunk.Style.FontSize
|
|
if h > lineHeight {
|
|
lineHeight = h
|
|
}
|
|
}
|
|
|
|
height += lineHeight
|
|
}
|
|
|
|
return height
|
|
}
|
|
|
|
// getLineHeight returns both the capheight and the font size based height of
|
|
// the line with the specified index.
|
|
func (p *StyledParagraph) getLineHeight(idx int) (capHeight, height float64) {
|
|
if p.lines == nil || len(p.lines) == 0 {
|
|
p.wrapText()
|
|
}
|
|
if idx < 0 || idx > len(p.lines)-1 {
|
|
common.Log.Debug("ERROR: invalid paragraph line index %d. Returning 0, 0", idx)
|
|
return 0, 0
|
|
}
|
|
|
|
line := p.lines[idx]
|
|
for _, chunk := range line {
|
|
descriptor, err := chunk.Style.Font.GetFontDescriptor()
|
|
if err != nil {
|
|
common.Log.Debug("ERROR: Unable to get font descriptor")
|
|
}
|
|
|
|
var fontCapHeight float64
|
|
if descriptor != nil {
|
|
if fontCapHeight, err = descriptor.GetCapHeight(); err != nil {
|
|
common.Log.Debug("ERROR: Unable to get font CapHeight: %v", err)
|
|
}
|
|
}
|
|
if int(fontCapHeight) <= 0 {
|
|
common.Log.Debug("WARN: CapHeight not available - setting to 1000")
|
|
fontCapHeight = 1000
|
|
}
|
|
|
|
h := fontCapHeight / 1000.0 * chunk.Style.FontSize * p.lineHeight
|
|
if h > capHeight {
|
|
capHeight = h
|
|
}
|
|
|
|
h = p.lineHeight * chunk.Style.FontSize
|
|
if h > height {
|
|
height = h
|
|
}
|
|
}
|
|
|
|
return capHeight, height
|
|
}
|
|
|
|
// getTextWidth calculates the text width as if all in one line (not taking
|
|
// wrapping into account).
|
|
func (p *StyledParagraph) getTextWidth() float64 {
|
|
var width float64
|
|
lenChunks := len(p.chunks)
|
|
|
|
for i, chunk := range p.chunks {
|
|
style := &chunk.Style
|
|
lenRunes := len(chunk.Text)
|
|
|
|
for j, r := range chunk.Text {
|
|
// Ignore newline for this. Handles as if all in one line.
|
|
if r == '\u000A' { // LF
|
|
continue
|
|
}
|
|
|
|
metrics, found := style.Font.GetRuneMetrics(r)
|
|
if !found {
|
|
common.Log.Debug("Rune char metrics not found! %v\n", r)
|
|
|
|
// FIXME: return error.
|
|
return -1
|
|
}
|
|
|
|
width += style.FontSize * metrics.Wx
|
|
|
|
// Do not add character spacing for the last character of the line.
|
|
if i != lenChunks-1 || j != lenRunes-1 {
|
|
width += style.CharSpacing * 1000.0
|
|
}
|
|
}
|
|
}
|
|
|
|
return width
|
|
}
|
|
|
|
// getTextLineWidth calculates the text width of a provided collection of text chunks.
|
|
func (p *StyledParagraph) getTextLineWidth(line []*TextChunk) float64 {
|
|
var width float64
|
|
lenChunks := len(line)
|
|
|
|
for i, chunk := range line {
|
|
style := &chunk.Style
|
|
lenRunes := len(chunk.Text)
|
|
|
|
for j, r := range chunk.Text {
|
|
// Ignore newline for this. Handles as if all in one line.
|
|
if r == '\u000A' { // LF
|
|
continue
|
|
}
|
|
|
|
metrics, found := style.Font.GetRuneMetrics(r)
|
|
if !found {
|
|
common.Log.Debug("Rune char metrics not found! %v\n", r)
|
|
|
|
// FIXME: return error.
|
|
return -1
|
|
}
|
|
|
|
width += style.FontSize * metrics.Wx
|
|
|
|
// Do not add character spacing for the last character of the line.
|
|
if i != lenChunks-1 || j != lenRunes-1 {
|
|
width += style.CharSpacing * 1000.0
|
|
}
|
|
}
|
|
}
|
|
|
|
return width
|
|
}
|
|
|
|
// getMaxLineWidth returns the width of the longest line of text in the paragraph.
|
|
func (p *StyledParagraph) getMaxLineWidth() float64 {
|
|
if p.lines == nil || len(p.lines) == 0 {
|
|
p.wrapText()
|
|
}
|
|
|
|
var width float64
|
|
for _, line := range p.lines {
|
|
w := p.getTextLineWidth(line)
|
|
if w > width {
|
|
width = w
|
|
}
|
|
}
|
|
|
|
return width
|
|
}
|
|
|
|
// getTextHeight calculates the text height as if all in one line (not taking wrapping into account).
|
|
func (p *StyledParagraph) getTextHeight() float64 {
|
|
var height float64
|
|
for _, chunk := range p.chunks {
|
|
h := chunk.Style.FontSize * p.lineHeight
|
|
if h > height {
|
|
height = h
|
|
}
|
|
}
|
|
|
|
return height
|
|
}
|
|
|
|
// wrapText splits text into lines. It uses a simple greedy algorithm to wrap
|
|
// fill the lines.
|
|
// TODO: Consider the Knuth/Plass algorithm or an alternative.
|
|
func (p *StyledParagraph) wrapText() error {
|
|
if !p.enableWrap || int(p.wrapWidth) <= 0 {
|
|
p.lines = [][]*TextChunk{p.chunks}
|
|
return nil
|
|
}
|
|
|
|
p.lines = [][]*TextChunk{}
|
|
var line []*TextChunk
|
|
var lineWidth float64
|
|
|
|
copyAnnotation := func(src *model.PdfAnnotation) *model.PdfAnnotation {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
|
|
var annotation *model.PdfAnnotation
|
|
switch t := src.GetContext().(type) {
|
|
case *model.PdfAnnotationLink:
|
|
if annot := copyLinkAnnotation(t); annot != nil {
|
|
annotation = annot.PdfAnnotation
|
|
}
|
|
}
|
|
|
|
return annotation
|
|
}
|
|
|
|
for _, chunk := range p.chunks {
|
|
style := chunk.Style
|
|
annotation := chunk.annotation
|
|
|
|
var (
|
|
part []rune
|
|
widths []float64
|
|
)
|
|
|
|
for _, r := range chunk.Text {
|
|
// newline wrapping.
|
|
if r == '\u000A' { // LF
|
|
// moves to next line.
|
|
line = append(line, &TextChunk{
|
|
Text: strings.TrimRightFunc(string(part), unicode.IsSpace),
|
|
Style: style,
|
|
annotation: copyAnnotation(annotation),
|
|
})
|
|
p.lines = append(p.lines, line)
|
|
line = nil
|
|
|
|
lineWidth = 0
|
|
part = nil
|
|
widths = nil
|
|
continue
|
|
}
|
|
|
|
metrics, found := style.Font.GetRuneMetrics(r)
|
|
if !found {
|
|
common.Log.Debug("Rune char metrics not found! %v\n", r)
|
|
return errors.New("glyph char metrics missing")
|
|
}
|
|
|
|
w := style.FontSize * metrics.Wx
|
|
charWidth := w + style.CharSpacing*1000.0
|
|
|
|
if lineWidth+w > p.wrapWidth*1000.0 {
|
|
// Goes out of bounds: Wrap.
|
|
// Breaks on the character.
|
|
// TODO: when goes outside: back up to next space,
|
|
// otherwise break on the character.
|
|
idx := -1
|
|
for j := len(part) - 1; j >= 0; j-- {
|
|
if part[j] == ' ' {
|
|
idx = j
|
|
break
|
|
}
|
|
}
|
|
|
|
text := string(part)
|
|
if idx >= 0 {
|
|
text = string(part[0 : idx+1])
|
|
|
|
part = part[idx+1:]
|
|
part = append(part, r)
|
|
widths = widths[idx+1:]
|
|
widths = append(widths, charWidth)
|
|
|
|
lineWidth = 0
|
|
for _, width := range widths {
|
|
lineWidth += width
|
|
}
|
|
} else {
|
|
lineWidth = charWidth
|
|
part = []rune{r}
|
|
widths = []float64{charWidth}
|
|
}
|
|
|
|
line = append(line, &TextChunk{
|
|
Text: strings.TrimRightFunc(string(text), unicode.IsSpace),
|
|
Style: style,
|
|
annotation: copyAnnotation(annotation),
|
|
})
|
|
p.lines = append(p.lines, line)
|
|
line = []*TextChunk{}
|
|
} else {
|
|
lineWidth += charWidth
|
|
part = append(part, r)
|
|
widths = append(widths, charWidth)
|
|
}
|
|
}
|
|
|
|
if len(part) > 0 {
|
|
line = append(line, &TextChunk{
|
|
Text: string(part),
|
|
Style: style,
|
|
annotation: copyAnnotation(annotation),
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(line) > 0 {
|
|
p.lines = append(p.lines, line)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GeneratePageBlocks generates the page blocks. Multiple blocks are generated
|
|
// if the contents wrap over multiple pages. Implements the Drawable interface.
|
|
func (p *StyledParagraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
|
|
origContext := ctx
|
|
var blocks []*Block
|
|
|
|
blk := NewBlock(ctx.PageWidth, ctx.PageHeight)
|
|
if p.positioning.isRelative() {
|
|
// Account for Paragraph Margins.
|
|
ctx.X += p.margins.left
|
|
ctx.Y += p.margins.top
|
|
ctx.Width -= p.margins.left + p.margins.right
|
|
ctx.Height -= p.margins.top + p.margins.bottom
|
|
|
|
// Use available space.
|
|
p.SetWidth(ctx.Width)
|
|
|
|
if p.Height() > ctx.Height {
|
|
// Goes out of the bounds. Write on a new template instead and create a new context at upper
|
|
// left corner.
|
|
// TODO: Handle case when Paragraph is larger than the Page...
|
|
// Should be fine if we just break on the paragraph, i.e. splitting it up over 2+ pages
|
|
|
|
blocks = append(blocks, blk)
|
|
blk = NewBlock(ctx.PageWidth, ctx.PageHeight)
|
|
|
|
// New Page.
|
|
ctx.Page++
|
|
newContext := ctx
|
|
newContext.Y = ctx.Margins.top // + p.Margins.top
|
|
newContext.X = ctx.Margins.left + p.margins.left
|
|
newContext.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom - p.margins.bottom
|
|
newContext.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right - p.margins.left - p.margins.right
|
|
ctx = newContext
|
|
}
|
|
} else {
|
|
// Absolute.
|
|
if int(p.wrapWidth) <= 0 {
|
|
// Use necessary space.
|
|
p.SetWidth(p.getTextWidth())
|
|
}
|
|
ctx.X = p.xPos
|
|
ctx.Y = p.yPos
|
|
}
|
|
|
|
if p.beforeRender != nil {
|
|
p.beforeRender(p, ctx)
|
|
}
|
|
|
|
// Place the Paragraph on the template at position (x,y) based on the ctx.
|
|
ctx, err := drawStyledParagraphOnBlock(blk, p, ctx)
|
|
if err != nil {
|
|
common.Log.Debug("ERROR: %v", err)
|
|
return nil, ctx, err
|
|
}
|
|
|
|
blocks = append(blocks, blk)
|
|
if p.positioning.isRelative() {
|
|
ctx.X -= p.margins.left // Move back.
|
|
ctx.Width = origContext.Width
|
|
return blocks, ctx, nil
|
|
}
|
|
// Absolute: not changing the context.
|
|
return blocks, origContext, nil
|
|
}
|
|
|
|
// Draw block on specified location on Page, adding to the content stream.
|
|
func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext) (DrawContext, error) {
|
|
// Find first free index for the font resources of the paragraph.
|
|
num := 1
|
|
fontName := core.PdfObjectName(fmt.Sprintf("Font%d", num))
|
|
for blk.resources.HasFontByName(fontName) {
|
|
num++
|
|
fontName = core.PdfObjectName(fmt.Sprintf("Font%d", num))
|
|
}
|
|
|
|
// Add default font to the page resources.
|
|
err := blk.resources.SetFontByName(fontName, p.defaultStyle.Font.ToPdfObject())
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
num++
|
|
|
|
defaultFontName := fontName
|
|
defaultFontSize := p.defaultStyle.FontSize
|
|
|
|
// Wrap the text into lines.
|
|
p.wrapText()
|
|
|
|
// Add the fonts of all chunks to the page resources.
|
|
var fonts [][]core.PdfObjectName
|
|
|
|
for _, line := range p.lines {
|
|
var fontLine []core.PdfObjectName
|
|
|
|
for _, chunk := range line {
|
|
fontName = core.PdfObjectName(fmt.Sprintf("Font%d", num))
|
|
|
|
err := blk.resources.SetFontByName(fontName, chunk.Style.Font.ToPdfObject())
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
|
|
fontLine = append(fontLine, fontName)
|
|
num++
|
|
}
|
|
|
|
fonts = append(fonts, fontLine)
|
|
}
|
|
|
|
// Create the content stream.
|
|
cc := contentstream.NewContentCreator()
|
|
cc.Add_q()
|
|
|
|
yPos := ctx.PageHeight - ctx.Y - defaultFontSize*p.lineHeight
|
|
cc.Translate(ctx.X, yPos)
|
|
|
|
if p.angle != 0 {
|
|
cc.RotateDeg(p.angle)
|
|
}
|
|
|
|
cc.Add_BT()
|
|
|
|
currY := yPos
|
|
for idx, line := range p.lines {
|
|
currX := ctx.X
|
|
|
|
if idx != 0 {
|
|
// Move to next line if not first.
|
|
cc.Add_Tstar()
|
|
}
|
|
|
|
isLastLine := idx == len(p.lines)-1
|
|
|
|
// Get width of the line (excluding spaces).
|
|
var (
|
|
width float64
|
|
height float64
|
|
spaceWidth float64
|
|
spaces uint
|
|
)
|
|
|
|
var chunkWidths []float64
|
|
for _, chunk := range line {
|
|
style := &chunk.Style
|
|
|
|
if style.FontSize > height {
|
|
height = style.FontSize
|
|
}
|
|
|
|
spaceMetrics, found := style.Font.GetRuneMetrics(' ')
|
|
if !found {
|
|
return ctx, errors.New("the font does not have a space glyph")
|
|
}
|
|
|
|
var chunkSpaces uint
|
|
var chunkWidth float64
|
|
lenChunk := len(chunk.Text)
|
|
for i, r := range chunk.Text {
|
|
if r == ' ' {
|
|
chunkSpaces++
|
|
continue
|
|
}
|
|
if r == '\u000A' { // LF
|
|
continue
|
|
}
|
|
|
|
metrics, found := style.Font.GetRuneMetrics(r)
|
|
if !found {
|
|
common.Log.Debug("Unsupported rune %v in font\n", r)
|
|
return ctx, errors.New("unsupported text glyph")
|
|
}
|
|
|
|
chunkWidth += style.FontSize * metrics.Wx
|
|
|
|
// Do not add character spacing for the last character of the line.
|
|
if i != lenChunk-1 {
|
|
chunkWidth += style.CharSpacing * 1000.0
|
|
}
|
|
}
|
|
|
|
chunkWidths = append(chunkWidths, chunkWidth)
|
|
width += chunkWidth
|
|
|
|
spaceWidth += float64(chunkSpaces) * spaceMetrics.Wx * style.FontSize
|
|
spaces += chunkSpaces
|
|
}
|
|
height *= p.lineHeight
|
|
|
|
// Add line shifts.
|
|
var objs []core.PdfObject
|
|
|
|
wrapWidth := p.wrapWidth * 1000.0
|
|
if p.alignment == TextAlignmentJustify {
|
|
// Do not justify last line.
|
|
if spaces > 0 && !isLastLine {
|
|
spaceWidth = (wrapWidth - width) / float64(spaces) / defaultFontSize
|
|
}
|
|
} else if p.alignment == TextAlignmentCenter {
|
|
// Start with an offset of half of the remaining line space.
|
|
offset := (wrapWidth - width - spaceWidth) / 2
|
|
shift := offset / defaultFontSize
|
|
objs = append(objs, core.MakeFloat(-shift))
|
|
|
|
currX += offset / 1000.0
|
|
} else if p.alignment == TextAlignmentRight {
|
|
// Push the text at the end of the line.
|
|
offset := (wrapWidth - width - spaceWidth)
|
|
shift := offset / defaultFontSize
|
|
objs = append(objs, core.MakeFloat(-shift))
|
|
|
|
currX += offset / 1000.0
|
|
}
|
|
|
|
if len(objs) > 0 {
|
|
cc.Add_Tf(defaultFontName, defaultFontSize).
|
|
Add_TL(defaultFontSize * p.lineHeight).
|
|
Add_TJ(objs...)
|
|
}
|
|
|
|
// Render line text chunks.
|
|
for k, chunk := range line {
|
|
style := &chunk.Style
|
|
|
|
r, g, b := style.Color.ToRGB()
|
|
fontName := defaultFontName
|
|
fontSize := defaultFontSize
|
|
|
|
// Set chunk rendering mode.
|
|
cc.Add_Tr(int64(style.RenderingMode))
|
|
|
|
// Set chunk character spacing.
|
|
cc.Add_Tc(style.CharSpacing)
|
|
|
|
if p.alignment != TextAlignmentJustify || isLastLine {
|
|
spaceMetrics, found := style.Font.GetRuneMetrics(' ')
|
|
if !found {
|
|
return ctx, errors.New("the font does not have a space glyph")
|
|
}
|
|
|
|
fontName = fonts[idx][k]
|
|
fontSize = style.FontSize
|
|
spaceWidth = spaceMetrics.Wx
|
|
}
|
|
enc := style.Font.Encoder()
|
|
|
|
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).
|
|
Add_Tf(fonts[idx][k], style.FontSize).
|
|
Add_TL(style.FontSize * p.lineHeight).
|
|
Add_TJ([]core.PdfObject{core.MakeStringFromBytes(encStr)}...)
|
|
|
|
encStr = nil
|
|
}
|
|
|
|
cc.Add_Tf(fontName, fontSize).
|
|
Add_TL(fontSize * p.lineHeight).
|
|
Add_TJ([]core.PdfObject{core.MakeFloat(-spaceWidth)}...)
|
|
|
|
chunkWidths[k] += spaceWidth * fontSize
|
|
} else {
|
|
if _, ok := enc.RuneToCharcode(rn); !ok {
|
|
common.Log.Debug("unsupported rune in text encoding: %#x (%c)", rn, rn)
|
|
continue
|
|
}
|
|
encStr = append(encStr, enc.Encode(string(rn))...)
|
|
}
|
|
}
|
|
|
|
if len(encStr) > 0 {
|
|
cc.Add_rg(r, g, b).
|
|
Add_Tf(fonts[idx][k], style.FontSize).
|
|
Add_TL(style.FontSize * p.lineHeight).
|
|
Add_TJ([]core.PdfObject{core.MakeStringFromBytes(encStr)}...)
|
|
}
|
|
|
|
chunkWidth := chunkWidths[k] / 1000.0
|
|
|
|
// Add annotations.
|
|
if chunk.annotation != nil {
|
|
var annotRect *core.PdfObjectArray
|
|
|
|
// Process annotation.
|
|
if !chunk.annotationProcessed {
|
|
switch t := chunk.annotation.GetContext().(type) {
|
|
case *model.PdfAnnotationLink:
|
|
// Initialize annotation rectangle.
|
|
annotRect = core.MakeArray()
|
|
t.Rect = annotRect
|
|
|
|
// Reverse the Y axis of the destination coordinates.
|
|
// The user passes in the annotation coordinates as if
|
|
// position 0, 0 is at the top left of the page.
|
|
// However, position 0, 0 in the PDF is at the bottom
|
|
// left of the page.
|
|
annotDest, ok := t.Dest.(*core.PdfObjectArray)
|
|
if ok && annotDest.Len() == 5 {
|
|
t, ok := annotDest.Get(1).(*core.PdfObjectName)
|
|
if ok && t.String() == "XYZ" {
|
|
y, err := core.GetNumberAsFloat(annotDest.Get(3))
|
|
if err == nil {
|
|
annotDest.Set(3, core.MakeFloat(ctx.PageHeight-y))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
chunk.annotationProcessed = true
|
|
}
|
|
|
|
// Set the coordinates of the annotation.
|
|
if annotRect != nil {
|
|
// Calculate rotated annotation position.
|
|
annotPos := draw.NewPoint(currX-ctx.X, currY-yPos).Rotate(p.angle)
|
|
annotPos.X += ctx.X
|
|
annotPos.Y += yPos
|
|
|
|
// Calculate rotated annotation bounding box.
|
|
offX, offY, annotW, annotH := rotateRect(chunkWidth, height, p.angle)
|
|
annotPos.X += offX
|
|
annotPos.Y += offY
|
|
|
|
annotRect.Clear()
|
|
annotRect.Append(core.MakeFloat(annotPos.X))
|
|
annotRect.Append(core.MakeFloat(annotPos.Y))
|
|
annotRect.Append(core.MakeFloat(annotPos.X + annotW))
|
|
annotRect.Append(core.MakeFloat(annotPos.Y + annotH))
|
|
}
|
|
|
|
blk.AddAnnotation(chunk.annotation)
|
|
}
|
|
|
|
currX += chunkWidth
|
|
|
|
// Reset rendering mode.
|
|
cc.Add_Tr(int64(TextRenderingModeFill))
|
|
|
|
// Reset character spacing.
|
|
cc.Add_Tc(0)
|
|
}
|
|
|
|
currY -= height
|
|
}
|
|
cc.Add_ET()
|
|
cc.Add_Q()
|
|
|
|
ops := cc.Operations()
|
|
ops.WrapIfNeeded()
|
|
|
|
blk.addContents(ops)
|
|
|
|
if p.positioning.isRelative() {
|
|
pHeight := p.Height() + p.margins.bottom
|
|
ctx.Y += pHeight
|
|
ctx.Height -= pHeight
|
|
|
|
// If the division is inline, calculate context new X coordinate.
|
|
if ctx.Inline {
|
|
ctx.X += p.Width() + p.margins.right
|
|
}
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|