2017-07-05 23:10:57 +00:00
|
|
|
/*
|
|
|
|
* 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"
|
2019-01-01 22:15:22 +02:00
|
|
|
"fmt"
|
2018-12-26 18:42:38 +02:00
|
|
|
"strconv"
|
2017-07-05 23:10:57 +00:00
|
|
|
|
|
|
|
"github.com/unidoc/unidoc/common"
|
|
|
|
"github.com/unidoc/unidoc/pdf/contentstream"
|
|
|
|
"github.com/unidoc/unidoc/pdf/core"
|
|
|
|
"github.com/unidoc/unidoc/pdf/model"
|
|
|
|
)
|
|
|
|
|
2017-07-31 13:07:02 +00:00
|
|
|
// Paragraph represents text drawn with a specified font and can wrap across lines and pages.
|
2018-08-15 17:36:42 +10:00
|
|
|
// By default it occupies the available width in the drawing context.
|
2017-07-31 13:07:02 +00:00
|
|
|
type Paragraph struct {
|
2017-07-05 23:10:57 +00:00
|
|
|
// The input utf-8 text as a string (series of runes).
|
|
|
|
text string
|
|
|
|
|
|
|
|
// The font to be used to draw the text.
|
2018-08-15 09:32:13 +10:00
|
|
|
textFont *model.PdfFont
|
2017-07-05 23:10:57 +00:00
|
|
|
|
|
|
|
// The font size (points).
|
|
|
|
fontSize float64
|
|
|
|
|
|
|
|
// The line relative height (default 1).
|
|
|
|
lineHeight float64
|
|
|
|
|
|
|
|
// The text color.
|
|
|
|
color model.PdfColorDeviceRGB
|
|
|
|
|
|
|
|
// Text alignment: Align left/right/center/justify.
|
|
|
|
alignment TextAlignment
|
|
|
|
|
|
|
|
// Wrapping properties.
|
|
|
|
enableWrap bool
|
|
|
|
wrapWidth float64
|
|
|
|
|
2018-07-18 00:03:18 +00:00
|
|
|
// 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
|
|
|
|
|
2017-07-05 23:10:57 +00:00
|
|
|
// Rotation angle (degrees).
|
|
|
|
angle float64
|
|
|
|
|
2017-07-06 16:26:22 +00:00
|
|
|
// Margins to be applied around the block when drawing on Page.
|
2017-07-05 23:10:57 +00:00
|
|
|
margins margins
|
|
|
|
|
|
|
|
// Positioning: relative / absolute.
|
|
|
|
positioning positioning
|
|
|
|
|
|
|
|
// Absolute coordinates (when in absolute mode).
|
|
|
|
xPos float64
|
|
|
|
yPos float64
|
|
|
|
|
|
|
|
// Scaling factors (1 default).
|
|
|
|
scaleX, scaleY float64
|
|
|
|
|
|
|
|
// Text lines after wrapping to available width.
|
|
|
|
textLines []string
|
|
|
|
}
|
|
|
|
|
2018-10-12 23:00:02 +03:00
|
|
|
// newParagraph create a new text paragraph. Uses default parameters: Helvetica, WinAnsiEncoding and
|
2018-08-15 09:32:13 +10:00
|
|
|
// wrap enabled with a wrap width of 100 points.
|
2018-12-15 18:45:22 +05:00
|
|
|
//
|
|
|
|
// Standard font may will have an encdoing set to WinAnsiEncoding. To set a different encoding, make a new font
|
|
|
|
// and use SetFont on the paragraph to override the defaut one.
|
2018-10-12 23:00:02 +03:00
|
|
|
func newParagraph(text string, style TextStyle) *Paragraph {
|
2018-12-09 19:37:07 +02:00
|
|
|
// TODO(dennwc): style is unused
|
|
|
|
|
2018-12-15 18:45:22 +05:00
|
|
|
p := &Paragraph{text: text}
|
2018-08-22 14:44:16 +10:00
|
|
|
|
2018-12-09 18:56:18 +02:00
|
|
|
font, err := model.NewStandard14Font("Helvetica")
|
2018-08-22 14:44:16 +10:00
|
|
|
if err != nil {
|
|
|
|
common.Log.Debug("ERROR: NewStandard14FontWithEncoding failed err=%v. Falling back.", err)
|
|
|
|
p.textFont = model.DefaultFont()
|
|
|
|
}
|
|
|
|
p.textFont = font
|
|
|
|
|
2017-07-05 23:10:57 +00:00
|
|
|
p.fontSize = 10
|
|
|
|
p.lineHeight = 1.0
|
2017-07-17 11:36:02 +00:00
|
|
|
|
|
|
|
// TODO: Can we wrap intellectually, only if given width is known?
|
2018-10-07 18:45:26 +03:00
|
|
|
p.enableWrap = true
|
2018-07-18 00:03:18 +00:00
|
|
|
p.defaultWrap = true
|
2017-07-14 21:58:16 +00:00
|
|
|
p.SetColor(ColorRGBFrom8bit(0, 0, 0))
|
2017-07-05 23:10:57 +00:00
|
|
|
p.alignment = TextAlignmentLeft
|
|
|
|
p.angle = 0
|
|
|
|
|
|
|
|
p.scaleX = 1
|
|
|
|
p.scaleY = 1
|
|
|
|
|
|
|
|
p.positioning = positionRelative
|
|
|
|
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetFont sets the Paragraph's font.
|
2018-08-15 09:32:13 +10:00
|
|
|
func (p *Paragraph) SetFont(font *model.PdfFont) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.textFont = font
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetFontSize sets the font size in document units (points).
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetFontSize(fontSize float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.fontSize = fontSize
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetTextAlignment sets the horizontal alignment of the text within the space provided.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetTextAlignment(align TextAlignment) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.alignment = align
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetLineHeight sets the line height (1.0 default).
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetLineHeight(lineheight float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.lineHeight = lineheight
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetText sets the text content of the Paragraph.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetText(text string) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.text = text
|
|
|
|
}
|
|
|
|
|
2018-02-15 13:11:32 +11:00
|
|
|
// Text sets the text content of the Paragraph.
|
|
|
|
func (p *Paragraph) Text() string {
|
|
|
|
return p.text
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetEnableWrap sets the line wrapping enabled flag.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetEnableWrap(enableWrap bool) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.enableWrap = enableWrap
|
2018-07-18 00:03:18 +00:00
|
|
|
p.defaultWrap = false
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
2018-08-17 08:41:35 +10:00
|
|
|
// SetColor sets the color of the Paragraph text.
|
2017-07-14 21:58:16 +00:00
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
// 1. p := NewParagraph("Red paragraph")
|
|
|
|
// // Set to red color with a hex code:
|
|
|
|
// p.SetColor(creator.ColorRGBFromHex("#ff0000"))
|
|
|
|
//
|
2017-07-31 13:07:02 +00:00
|
|
|
// 2. Make Paragraph green with 8-bit rgb values (0-255 each component)
|
2017-07-14 21:58:16 +00:00
|
|
|
// p.SetColor(creator.ColorRGBFrom8bit(0, 255, 0)
|
|
|
|
//
|
2017-07-31 13:07:02 +00:00
|
|
|
// 3. Make Paragraph blue with arithmetic (0-1) rgb components.
|
2017-07-14 21:58:16 +00:00
|
|
|
// p.SetColor(creator.ColorRGBFromArithmetic(0, 0, 1.0)
|
|
|
|
//
|
2017-07-31 14:06:47 +00:00
|
|
|
func (p *Paragraph) SetColor(col Color) {
|
2017-07-17 11:36:02 +00:00
|
|
|
pdfColor := model.NewPdfColorDeviceRGB(col.ToRGB())
|
2017-07-14 21:58:16 +00:00
|
|
|
p.color = *pdfColor
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetPos sets absolute positioning with specified coordinates.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetPos(x, y float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.positioning = positionAbsolute
|
|
|
|
p.xPos = x
|
|
|
|
p.yPos = y
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetAngle sets the rotation angle of the text.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetAngle(angle float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.angle = angle
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// SetMargins sets the Paragraph's margins.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetMargins(left, right, top, bottom float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.margins.left = left
|
|
|
|
p.margins.right = right
|
|
|
|
p.margins.top = top
|
|
|
|
p.margins.bottom = bottom
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// GetMargins returns the Paragraph's margins: left, right, top, bottom.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) GetMargins() (float64, float64, float64, float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
return p.margins.left, p.margins.right, p.margins.top, p.margins.bottom
|
|
|
|
}
|
|
|
|
|
2018-08-15 09:32:13 +10:00
|
|
|
// 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.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) SetWidth(width float64) {
|
2017-07-05 23:10:57 +00:00
|
|
|
p.wrapWidth = width
|
|
|
|
p.wrapText()
|
|
|
|
}
|
|
|
|
|
2017-08-01 13:10:48 +00:00
|
|
|
// Width returns the width of the Paragraph.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) Width() float64 {
|
2018-10-07 18:45:26 +03:00
|
|
|
if p.enableWrap && int(p.wrapWidth) > 0 {
|
2017-07-17 20:41:10 +00:00
|
|
|
return p.wrapWidth
|
|
|
|
}
|
2018-08-15 17:36:42 +10:00
|
|
|
return p.getTextWidth() / 1000.0
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
2018-08-15 09:32:13 +10:00
|
|
|
// 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.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) Height() float64 {
|
2017-07-05 23:10:57 +00:00
|
|
|
if p.textLines == nil || len(p.textLines) == 0 {
|
|
|
|
p.wrapText()
|
|
|
|
}
|
|
|
|
|
2018-08-15 17:36:42 +10:00
|
|
|
return float64(len(p.textLines)) * p.lineHeight * p.fontSize
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
2018-07-26 13:52:35 +00:00
|
|
|
// getTextWidth calculates the text width as if all in one line (not taking wrapping into account).
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) getTextWidth() float64 {
|
2018-08-15 09:32:13 +10:00
|
|
|
w := 0.0
|
2017-07-05 23:10:57 +00:00
|
|
|
|
2018-08-15 09:32:13 +10:00
|
|
|
for _, r := range p.text {
|
2018-07-18 11:24:24 +00:00
|
|
|
// Ignore newline for this.. Handles as if all in one line.
|
2018-12-28 01:38:48 +02:00
|
|
|
if r == '\u000A' { // LF
|
2018-07-18 11:24:24 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-12-28 01:38:48 +02:00
|
|
|
metrics, found := p.textFont.GetRuneMetrics(r)
|
2017-07-05 23:10:57 +00:00
|
|
|
if !found {
|
2018-12-28 01:38:48 +02:00
|
|
|
common.Log.Debug("ERROR: Rune char metrics not found! (rune 0x%04x=%c)", r, r)
|
2018-12-09 21:37:27 +02:00
|
|
|
return -1 // FIXME: return error.
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
w += p.fontSize * metrics.Wx
|
|
|
|
}
|
|
|
|
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
2018-10-08 17:33:55 +03:00
|
|
|
// getTextLineWidth calculates the text width of a provided line of text.
|
|
|
|
func (p *Paragraph) getTextLineWidth(line string) float64 {
|
|
|
|
var width float64
|
|
|
|
for _, r := range line {
|
|
|
|
// Ignore newline for this.. Handles as if all in one line.
|
2018-12-28 01:38:48 +02:00
|
|
|
if r == '\u000A' { // LF
|
2018-10-08 17:33:55 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-12-28 01:38:48 +02:00
|
|
|
metrics, found := p.textFont.GetRuneMetrics(r)
|
2018-10-08 17:33:55 +03:00
|
|
|
if !found {
|
2018-12-28 01:38:48 +02:00
|
|
|
common.Log.Debug("ERROR: Rune char metrics not found! (rune 0x%04x=%c)", r, r)
|
2018-12-09 21:37:27 +02:00
|
|
|
return -1 // FIXME: return error.
|
2018-10-08 17:33:55 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
width += p.fontSize * metrics.Wx
|
|
|
|
}
|
|
|
|
|
|
|
|
return width
|
|
|
|
}
|
|
|
|
|
|
|
|
// getMaxLineWidth returns the width of the longest line of text in the paragraph.
|
|
|
|
func (p *Paragraph) getMaxLineWidth() float64 {
|
|
|
|
if p.textLines == nil || len(p.textLines) == 0 {
|
|
|
|
p.wrapText()
|
|
|
|
}
|
|
|
|
|
|
|
|
var width float64
|
|
|
|
for _, line := range p.textLines {
|
|
|
|
w := p.getTextLineWidth(line)
|
|
|
|
if w > width {
|
|
|
|
width = w
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return width
|
|
|
|
}
|
|
|
|
|
2017-07-05 23:10:57 +00:00
|
|
|
// Simple algorithm to wrap the text into lines (greedy algorithm - fill the lines).
|
2018-12-09 21:37:27 +02:00
|
|
|
// TODO: Consider the Knuth/Plass algorithm or an alternative.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) wrapText() error {
|
2018-10-07 18:45:26 +03:00
|
|
|
if !p.enableWrap || int(p.wrapWidth) <= 0 {
|
2018-08-17 08:41:35 +10:00
|
|
|
p.textLines = []string{p.text}
|
2017-07-05 23:10:57 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-11-29 23:24:40 +02:00
|
|
|
var line []rune
|
2018-08-15 17:36:42 +10:00
|
|
|
lineWidth := 0.0
|
2018-11-29 23:24:40 +02:00
|
|
|
p.textLines = nil
|
2017-07-05 23:10:57 +00:00
|
|
|
|
|
|
|
runes := []rune(p.text)
|
2018-12-28 01:38:48 +02:00
|
|
|
var widths []float64
|
2017-07-05 23:10:57 +00:00
|
|
|
|
2018-12-28 01:38:48 +02:00
|
|
|
for _, r := range runes {
|
2018-07-18 11:24:24 +00:00
|
|
|
// Newline wrapping.
|
2018-12-28 01:38:48 +02:00
|
|
|
if r == '\u000A' { // LF
|
2018-07-18 11:24:24 +00:00
|
|
|
// Moves to next line.
|
|
|
|
p.textLines = append(p.textLines, string(line))
|
2018-11-29 23:24:40 +02:00
|
|
|
line = nil
|
2018-07-18 11:24:24 +00:00
|
|
|
lineWidth = 0
|
2018-11-29 23:24:40 +02:00
|
|
|
widths = nil
|
2018-07-18 11:24:24 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-12-28 01:38:48 +02:00
|
|
|
metrics, found := p.textFont.GetRuneMetrics(r)
|
2017-07-05 23:10:57 +00:00
|
|
|
if !found {
|
2018-12-28 01:38:48 +02:00
|
|
|
common.Log.Debug("ERROR: Rune char metrics not found! rune=0x%04x=%c font=%s %#q",
|
|
|
|
r, r, p.textFont.BaseFont(), p.textFont.Subtype())
|
2018-06-07 14:55:37 +00:00
|
|
|
common.Log.Trace("Font: %#v", p.textFont)
|
|
|
|
common.Log.Trace("Encoder: %#v", p.textFont.Encoder())
|
2018-12-08 19:16:52 +02:00
|
|
|
return errors.New("glyph char metrics missing")
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
w := p.fontSize * metrics.Wx
|
|
|
|
if lineWidth+w > p.wrapWidth*1000.0 {
|
|
|
|
// Goes out of bounds: Wrap.
|
|
|
|
// Breaks on the character.
|
|
|
|
idx := -1
|
2018-12-28 01:38:48 +02:00
|
|
|
for i := len(line) - 1; i >= 0; i-- {
|
|
|
|
if line[i] == ' ' { // TODO: What about other space glyphs like controlHT?
|
2017-07-05 23:10:57 +00:00
|
|
|
idx = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if idx > 0 {
|
2018-08-15 17:36:42 +10:00
|
|
|
// Back up to last space.
|
2017-07-05 23:10:57 +00:00
|
|
|
p.textLines = append(p.textLines, string(line[0:idx+1]))
|
|
|
|
|
2018-08-15 17:36:42 +10:00
|
|
|
// Remainder of line.
|
2018-12-28 01:38:48 +02:00
|
|
|
line = append(line[idx+1:], r)
|
2018-08-15 17:36:42 +10:00
|
|
|
widths = append(widths[idx+1:], w)
|
|
|
|
lineWidth = sum(widths)
|
2017-07-05 23:10:57 +00:00
|
|
|
|
|
|
|
} else {
|
|
|
|
p.textLines = append(p.textLines, string(line))
|
2018-12-28 01:38:48 +02:00
|
|
|
line = []rune{r}
|
2018-08-15 17:36:42 +10:00
|
|
|
widths = []float64{w}
|
|
|
|
lineWidth = w
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
} else {
|
2018-12-28 01:38:48 +02:00
|
|
|
line = append(line, r)
|
2017-07-05 23:10:57 +00:00
|
|
|
lineWidth += w
|
|
|
|
widths = append(widths, w)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(line) > 0 {
|
|
|
|
p.textLines = append(p.textLines, string(line))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-15 17:36:42 +10:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2018-08-15 09:32:13 +10:00
|
|
|
// GeneratePageBlocks generates the page blocks. Multiple blocks are generated if the contents wrap
|
|
|
|
// over multiple pages. Implements the Drawable interface.
|
2017-07-31 13:07:02 +00:00
|
|
|
func (p *Paragraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
|
2017-07-05 23:10:57 +00:00
|
|
|
origContext := ctx
|
2018-12-09 19:28:50 +02:00
|
|
|
var blocks []*Block
|
2017-07-05 23:10:57 +00:00
|
|
|
|
2017-07-06 16:26:22 +00:00
|
|
|
blk := NewBlock(ctx.PageWidth, ctx.PageHeight)
|
2017-07-05 23:10:57 +00:00
|
|
|
if p.positioning.isRelative() {
|
2018-08-17 08:41:35 +10:00
|
|
|
// Account for Paragraph margins.
|
2017-07-05 23:10:57 +00:00
|
|
|
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 {
|
2018-08-15 09:32:13 +10:00
|
|
|
// Goes out of the bounds. Write on a new template instead and create a new context at
|
|
|
|
// upper left corner.
|
2018-12-09 21:37:27 +02:00
|
|
|
// TODO: Handle case when Paragraph is larger than the Page...
|
2017-07-05 23:10:57 +00:00
|
|
|
// Should be fine if we just break on the paragraph, i.e. splitting it up over 2+ pages
|
|
|
|
|
|
|
|
blocks = append(blocks, blk)
|
2017-07-06 16:26:22 +00:00
|
|
|
blk = NewBlock(ctx.PageWidth, ctx.PageHeight)
|
2017-07-05 23:10:57 +00:00
|
|
|
|
2017-07-06 16:26:22 +00:00
|
|
|
// New Page.
|
|
|
|
ctx.Page++
|
2017-07-05 23:10:57 +00:00
|
|
|
newContext := ctx
|
2017-07-06 16:26:22 +00:00
|
|
|
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
|
2017-07-05 23:10:57 +00:00
|
|
|
ctx = newContext
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Absolute.
|
2018-10-07 18:45:26 +03:00
|
|
|
if int(p.wrapWidth) <= 0 {
|
2017-07-05 23:10:57 +00:00
|
|
|
// Use necessary space.
|
|
|
|
p.SetWidth(p.getTextWidth())
|
|
|
|
}
|
|
|
|
ctx.X = p.xPos
|
|
|
|
ctx.Y = p.yPos
|
|
|
|
}
|
|
|
|
|
2017-07-31 13:07:02 +00:00
|
|
|
// Place the Paragraph on the template at position (x,y) based on the ctx.
|
2017-07-05 23:10:57 +00:00
|
|
|
ctx, err := drawParagraphOnBlock(blk, p, ctx)
|
|
|
|
if err != nil {
|
|
|
|
common.Log.Debug("ERROR: %v", err)
|
|
|
|
return nil, ctx, err
|
|
|
|
}
|
|
|
|
|
|
|
|
blocks = append(blocks, blk)
|
|
|
|
if p.positioning.isRelative() {
|
2017-07-13 15:35:54 +00:00
|
|
|
ctx.X -= p.margins.left // Move back.
|
2017-07-26 16:11:28 +00:00
|
|
|
ctx.Width = origContext.Width
|
2017-07-05 23:10:57 +00:00
|
|
|
return blocks, ctx, nil
|
|
|
|
}
|
2018-07-26 14:57:57 +00:00
|
|
|
// Absolute: not changing the context.
|
|
|
|
return blocks, origContext, nil
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
2018-08-15 17:36:42 +10:00
|
|
|
// drawParagraphOnBlock draws Paragraph `p` on Block `blk` at the specified location on the page,
|
|
|
|
// adding it to the content stream.
|
2017-07-31 13:07:02 +00:00
|
|
|
func drawParagraphOnBlock(blk *Block, p *Paragraph, ctx DrawContext) (DrawContext, error) {
|
2017-07-05 23:10:57 +00:00
|
|
|
// Find a free name for the font.
|
|
|
|
num := 1
|
2018-12-26 18:42:38 +02:00
|
|
|
fontName := core.PdfObjectName("Font" + strconv.Itoa(num))
|
2017-07-05 23:10:57 +00:00
|
|
|
for blk.resources.HasFontByName(fontName) {
|
|
|
|
num++
|
2018-12-26 18:42:38 +02:00
|
|
|
fontName = core.PdfObjectName("Font" + strconv.Itoa(num))
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
2017-07-06 16:26:22 +00:00
|
|
|
// Add to the Page resources.
|
2017-07-05 23:10:57 +00:00
|
|
|
err := blk.resources.SetFontByName(fontName, p.textFont.ToPdfObject())
|
|
|
|
if err != nil {
|
|
|
|
return ctx, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wrap the text into lines.
|
|
|
|
p.wrapText()
|
|
|
|
|
|
|
|
// Create the content stream.
|
|
|
|
cc := contentstream.NewContentCreator()
|
|
|
|
cc.Add_q()
|
|
|
|
|
2017-07-06 16:26:22 +00:00
|
|
|
yPos := ctx.PageHeight - ctx.Y - p.fontSize*p.lineHeight
|
2017-07-05 23:10:57 +00:00
|
|
|
|
|
|
|
cc.Translate(ctx.X, yPos)
|
|
|
|
if p.angle != 0 {
|
|
|
|
cc.RotateDeg(p.angle)
|
|
|
|
}
|
|
|
|
|
|
|
|
cc.Add_BT().
|
|
|
|
Add_rg(p.color.R(), p.color.G(), p.color.B()).
|
|
|
|
Add_Tf(fontName, p.fontSize).
|
|
|
|
Add_TL(p.fontSize * p.lineHeight)
|
|
|
|
|
|
|
|
for idx, line := range p.textLines {
|
|
|
|
if idx != 0 {
|
|
|
|
// Move to next line if not first.
|
|
|
|
cc.Add_Tstar()
|
|
|
|
}
|
|
|
|
|
|
|
|
runes := []rune(line)
|
|
|
|
|
|
|
|
// Get width of the line (excluding spaces).
|
2018-08-15 09:32:13 +10:00
|
|
|
w := 0.0
|
2017-07-05 23:10:57 +00:00
|
|
|
spaces := 0
|
2018-08-21 12:43:51 +10:00
|
|
|
for i, r := range runes {
|
2018-12-28 01:38:48 +02:00
|
|
|
if r == ' ' {
|
2017-07-05 23:10:57 +00:00
|
|
|
spaces++
|
|
|
|
continue
|
|
|
|
}
|
2018-12-28 01:38:48 +02:00
|
|
|
if r == '\u000A' { // LF
|
2018-07-18 11:24:24 +00:00
|
|
|
continue
|
|
|
|
}
|
2018-12-28 01:38:48 +02:00
|
|
|
metrics, found := p.textFont.GetRuneMetrics(r)
|
2017-07-05 23:10:57 +00:00
|
|
|
if !found {
|
2018-12-28 01:38:48 +02:00
|
|
|
common.Log.Debug("Unsupported rune i=%d rune=0x%04x=%c in font %s %s",
|
|
|
|
i, r, r,
|
2018-08-17 08:41:35 +10:00
|
|
|
p.textFont.BaseFont(), p.textFont.Subtype())
|
2018-12-08 19:16:52 +02:00
|
|
|
return ctx, errors.New("unsupported text glyph")
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
w += p.fontSize * metrics.Wx
|
|
|
|
}
|
|
|
|
|
2018-12-09 19:28:50 +02:00
|
|
|
var objs []core.PdfObject
|
2017-07-05 23:10:57 +00:00
|
|
|
|
2018-12-28 01:38:48 +02:00
|
|
|
spaceMetrics, found := p.textFont.GetRuneMetrics(' ')
|
2017-07-05 23:10:57 +00:00
|
|
|
if !found {
|
2018-12-08 19:16:52 +02:00
|
|
|
return ctx, errors.New("the font does not have a space glyph")
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
spaceWidth := spaceMetrics.Wx
|
2018-08-21 12:43:51 +10:00
|
|
|
switch p.alignment {
|
|
|
|
case TextAlignmentJustify:
|
2017-07-05 23:10:57 +00:00
|
|
|
if spaces > 0 && idx < len(p.textLines)-1 { // Not to justify last line.
|
|
|
|
spaceWidth = (p.wrapWidth*1000.0 - w) / float64(spaces) / p.fontSize
|
|
|
|
}
|
2018-08-21 12:43:51 +10:00
|
|
|
case TextAlignmentCenter:
|
2017-07-05 23:10:57 +00:00
|
|
|
// Start with a shift.
|
|
|
|
textWidth := w + float64(spaces)*spaceWidth*p.fontSize
|
|
|
|
shift := (p.wrapWidth*1000.0 - textWidth) / 2 / p.fontSize
|
|
|
|
objs = append(objs, core.MakeFloat(-shift))
|
2018-08-21 12:43:51 +10:00
|
|
|
case TextAlignmentRight:
|
2017-07-05 23:10:57 +00:00
|
|
|
textWidth := w + float64(spaces)*spaceWidth*p.fontSize
|
|
|
|
shift := (p.wrapWidth*1000.0 - textWidth) / p.fontSize
|
|
|
|
objs = append(objs, core.MakeFloat(-shift))
|
|
|
|
}
|
2019-01-01 22:15:22 +02:00
|
|
|
enc := p.textFont.Encoder()
|
2017-07-05 23:10:57 +00:00
|
|
|
|
2018-12-09 19:28:50 +02:00
|
|
|
var encoded []byte
|
2018-09-03 10:48:31 +10:00
|
|
|
isCID := p.textFont.IsCID()
|
2018-08-21 12:43:51 +10:00
|
|
|
for _, r := range runes {
|
2019-01-01 22:15:22 +02:00
|
|
|
if r == ' ' { // TODO: What about \t and other spaces.
|
2018-08-21 12:43:51 +10:00
|
|
|
if len(encoded) > 0 {
|
|
|
|
objs = append(objs, core.MakeStringFromBytes(encoded))
|
2019-01-01 22:15:22 +02:00
|
|
|
encoded = nil
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
objs = append(objs, core.MakeFloat(-spaceWidth))
|
|
|
|
} else {
|
2019-01-01 22:15:22 +02:00
|
|
|
code, ok := enc.RuneToCharcode(r)
|
|
|
|
if !ok {
|
|
|
|
err := fmt.Errorf("unsupported rune in text encoding: %#x (%c)", r, r)
|
|
|
|
common.Log.Debug("%s", err)
|
|
|
|
return ctx, err
|
|
|
|
}
|
|
|
|
// TODO(dennwc): this should not be done manually; encoder should do this
|
|
|
|
if isCID {
|
|
|
|
hi, lo := code>>8, code&0xff
|
|
|
|
encoded = append(encoded, byte(hi), byte(lo))
|
|
|
|
} else {
|
|
|
|
encoded = append(encoded, byte(code))
|
2018-08-21 12:43:51 +10:00
|
|
|
}
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
}
|
2018-08-21 12:43:51 +10:00
|
|
|
if len(encoded) > 0 {
|
|
|
|
objs = append(objs, core.MakeStringFromBytes(encoded))
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cc.Add_TJ(objs...)
|
|
|
|
}
|
|
|
|
cc.Add_ET()
|
|
|
|
cc.Add_Q()
|
|
|
|
|
|
|
|
ops := cc.Operations()
|
|
|
|
ops.WrapIfNeeded()
|
|
|
|
|
|
|
|
blk.addContents(ops)
|
|
|
|
|
|
|
|
if p.positioning.isRelative() {
|
2018-09-17 21:33:17 +03:00
|
|
|
pHeight := p.Height() + p.margins.bottom
|
|
|
|
ctx.Y += pHeight
|
|
|
|
ctx.Height -= pHeight
|
2018-09-17 22:11:25 +03:00
|
|
|
|
2018-09-18 19:33:02 +03:00
|
|
|
// If the division is inline, calculate context new X coordinate.
|
2018-09-17 22:11:25 +03:00
|
|
|
if ctx.Inline {
|
|
|
|
ctx.X += p.Width() + p.margins.right
|
|
|
|
}
|
2017-07-05 23:10:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ctx, nil
|
|
|
|
}
|