Merge pull request #213 from adrg/paragraph-styles

Multi-style paragraphs
This commit is contained in:
Gunnsteinn Hall 2018-09-25 20:43:36 +00:00 committed by GitHub
commit f74acfdea0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 988 additions and 16 deletions

View File

@ -15,7 +15,7 @@ import (
// It can contain multiple Drawable components (currently supporting Paragraph and Image).
//
// The component stacking behavior is vertical, where the Drawables are drawn on top of each other.
// TODO: Add inline mode (horizontal stacking).
// Also supports horizontal stacking by activating the inline mode.
type Division struct {
components []VectorDrawable
@ -47,13 +47,15 @@ func (div *Division) SetInline(inline bool) {
}
// Add adds a VectorDrawable to the Division container.
// Currently supported VectorDrawables: *Paragraph, *Image.
// Currently supported VectorDrawables: *Paragraph, *StyledParagraph, *Image.
func (div *Division) Add(d VectorDrawable) error {
supported := false
switch d.(type) {
case *Paragraph:
supported = true
case *StyledParagraph:
supported = true
case *Image:
supported = true
}

View File

@ -58,6 +58,17 @@ func TestDivVertical(t *testing.T) {
p.SetFont(fontBold)
div.Add(p)
// Add styled paragraph
style := NewTextStyle()
style.Color = ColorRGBFrom8bit(0, 0, 255)
s := NewStyledParagraph("Not even with a styled ", style)
style.Color = ColorRGBFrom8bit(255, 0, 0)
s.Append("paragraph", style)
div.Add(s)
err = c.Draw(div)
if err != nil {
t.Fatalf("Error drawing: %v", err)
@ -110,11 +121,26 @@ func TestDivInline(t *testing.T) {
p.SetFont(fontRegular)
div.Add(p)
p = NewParagraph("This one did not fit in the available line space")
p = NewParagraph("This one did not fit in the available line space. ")
p.SetEnableWrap(false)
p.SetFont(fontBold)
div.Add(p)
// Add styled paragraph
style := NewTextStyle()
style.Color = ColorRGBFrom8bit(0, 0, 255)
s := NewStyledParagraph("This styled paragraph should ", style)
style.Color = ColorRGBFrom8bit(255, 0, 0)
s.Append("fit", style)
style.Color = ColorRGBFrom8bit(0, 255, 0)
style.Font = fontBold
s.Append(" right in.", style)
div.Add(s)
err = c.Draw(div)
if err != nil {
t.Fatalf("Error drawing: %v", err)
@ -199,26 +225,43 @@ func TestDivRandomSequences(t *testing.T) {
div := NewDivision()
div.SetInline(true)
for i := 0; i < 250; i++ {
style := NewTextStyle()
for i := 0; i < 350; i++ {
r := byte(seed.Intn(200))
g := byte(seed.Intn(200))
b := byte(seed.Intn(200))
word := RandString(seed.Intn(10)+5) + " "
fontSize := float64(14 + seed.Intn(3))
p := NewParagraph(word)
p.SetEnableWrap(false)
p.SetColor(ColorRGBFrom8bit(r, g, b))
p.SetFontSize(fontSize)
fontSize := float64(11 + seed.Intn(3))
if seed.Intn(2)%2 == 0 {
p.SetFont(fontBold)
} else {
p.SetFont(fontRegular)
}
p := NewParagraph(word)
p.SetEnableWrap(false)
p.SetColor(ColorRGBFrom8bit(r, g, b))
p.SetFontSize(fontSize)
div.Add(p)
if seed.Intn(2)%2 == 0 {
p.SetFont(fontBold)
} else {
p.SetFont(fontRegular)
}
div.Add(p)
} else {
style.Color = ColorRGBFrom8bit(r, g, b)
style.FontSize = fontSize
if seed.Intn(2)%2 == 0 {
style.Font = fontBold
} else {
style.Font = fontRegular
}
s := NewStyledParagraph(word, style)
s.SetEnableWrap(false)
div.Add(s)
}
}
err = c.Draw(div)

View File

@ -0,0 +1,642 @@
/*
* 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/unidoc/common"
"github.com/unidoc/unidoc/pdf/contentstream"
"github.com/unidoc/unidoc/pdf/core"
"github.com/unidoc/unidoc/pdf/model/textencoding"
)
// 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
// The text encoder which can convert the text (as runes) into a series of glyphs and get character metrics.
encoder textencoding.TextEncoder
// 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
}
// NewStyledParagraph creates a new styled paragraph.
// Uses default parameters: Helvetica, WinAnsiEncoding and wrap enabled
// with a wrap width of 100 points.
func NewStyledParagraph(text string, style TextStyle) *StyledParagraph {
// TODO: Can we wrap intellectually, only if given width is known?
p := &StyledParagraph{
chunks: []TextChunk{
TextChunk{
Text: text,
Style: style,
},
},
defaultStyle: NewTextStyle(),
lineHeight: 1.0,
alignment: TextAlignmentLeft,
enableWrap: true,
defaultWrap: true,
angle: 0,
scaleX: 1,
scaleY: 1,
positioning: positionRelative,
}
p.SetEncoder(textencoding.NewWinAnsiTextEncoder())
return p
}
// Append adds a new text chunk with a specified style to the paragraph.
func (p *StyledParagraph) Append(text string, style TextStyle) {
chunk := TextChunk{
Text: text,
Style: style,
}
chunk.Style.Font.SetEncoder(p.encoder)
p.chunks = append(p.chunks, chunk)
p.wrapText()
}
// Reset sets the entire text and also the style of the paragraph
// to those specified. It behaves as if the paragraph was a new one.
func (p *StyledParagraph) Reset(text string, style TextStyle) {
p.chunks = []TextChunk{}
p.Append(text, style)
}
// SetTextAlignment sets the horizontal alignment of the text within the space provided.
func (p *StyledParagraph) SetTextAlignment(align TextAlignment) {
p.alignment = align
}
// SetEncoder sets the text encoding.
func (p *StyledParagraph) SetEncoder(encoder textencoding.TextEncoder) {
p.encoder = encoder
p.defaultStyle.Font.SetEncoder(encoder)
// Sync with the text font too.
// XXX/FIXME: Keep in 1 place only.
for _, chunk := range p.chunks {
chunk.Style.Font.SetEncoder(encoder)
}
}
// 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 {
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
}
// getTextWidth calculates the text width as if all in one line (not taking wrapping into account).
func (p *StyledParagraph) getTextWidth() float64 {
var width float64
for _, chunk := range p.chunks {
style := &chunk.Style
for _, rune := range chunk.Text {
glyph, found := p.encoder.RuneToGlyph(rune)
if !found {
common.Log.Debug("Error! Glyph not found for rune: %s\n", rune)
// XXX/FIXME: return error.
return -1
}
// Ignore newline for this.. Handles as if all in one line.
if glyph == "controlLF" {
continue
}
metrics, found := style.Font.GetGlyphCharMetrics(glyph)
if !found {
common.Log.Debug("Glyph char metrics not found! %s\n", glyph)
// XXX/FIXME: return error.
return -1
}
width += style.FontSize * metrics.Wx
}
}
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.
// XXX/TODO: Consider the Knuth/Plass algorithm or an alternative.
func (p *StyledParagraph) wrapText() error {
if !p.enableWrap {
p.lines = [][]TextChunk{p.chunks}
return nil
}
p.lines = [][]TextChunk{}
var line []TextChunk
var lineWidth float64
for _, chunk := range p.chunks {
style := chunk.Style
var part []rune
var glyphs []string
var widths []float64
for _, r := range chunk.Text {
glyph, found := p.encoder.RuneToGlyph(r)
if !found {
common.Log.Debug("Error! Glyph not found for rune: %v\n", r)
// XXX/FIXME: return error.
return errors.New("Glyph not found for rune")
}
// newline wrapping.
if glyph == "controllf" {
// moves to next line.
line = append(line, TextChunk{
Text: strings.TrimRightFunc(string(part), unicode.IsSpace),
Style: style,
})
p.lines = append(p.lines, line)
line = []TextChunk{}
lineWidth = 0
part = []rune{}
widths = []float64{}
glyphs = []string{}
continue
}
metrics, found := style.Font.GetGlyphCharMetrics(glyph)
if !found {
common.Log.Debug("Glyph char metrics not found! %s\n", glyph)
// XXX/FIXME: return error.
return errors.New("Glyph char metrics missing")
}
w := style.FontSize * metrics.Wx
if lineWidth+w > p.wrapWidth*1000.0 {
// Goes out of bounds: Wrap.
// Breaks on the character.
// XXX/TODO: when goes outside: back up to next space,
// otherwise break on the character.
idx := -1
for j := len(glyphs) - 1; j >= 0; j-- {
if glyphs[j] == "space" {
idx = j
break
}
}
text := string(part)
if idx >= 0 {
text = string(part[0 : idx+1])
part = part[idx+1:]
part = append(part, r)
glyphs = glyphs[idx+1:]
glyphs = append(glyphs, glyph)
widths = widths[idx+1:]
widths = append(widths, w)
lineWidth = 0
for _, width := range widths {
lineWidth += width
}
} else {
lineWidth = w
part = []rune{r}
glyphs = []string{glyph}
widths = []float64{w}
}
line = append(line, TextChunk{
Text: strings.TrimRightFunc(string(text), unicode.IsSpace),
Style: style,
})
p.lines = append(p.lines, line)
line = []TextChunk{}
} else {
lineWidth += w
part = append(part, r)
glyphs = append(glyphs, glyph)
widths = append(widths, w)
}
}
if len(part) > 0 {
line = append(line, TextChunk{
Text: string(part),
Style: style,
})
}
}
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
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.
// XXX/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 p.wrapWidth == 0 {
// Use necessary space.
p.SetWidth(p.getTextWidth())
}
ctx.X = p.xPos
ctx.Y = p.yPos
}
// 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
fonts := [][]core.PdfObjectName{}
for _, line := range p.lines {
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()
for idx, line := range p.lines {
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
var spaceWidth float64
var spaces uint
for _, chunk := range line {
style := &chunk.Style
spaceMetrics, found := style.Font.GetGlyphCharMetrics("space")
if !found {
return ctx, errors.New("The font does not have a space glyph")
}
var chunkSpaces uint
for _, r := range chunk.Text {
glyph, found := p.encoder.RuneToGlyph(r)
if !found {
common.Log.Debug("Rune 0x%x not supported by text encoder", r)
return ctx, errors.New("Unsupported rune in text encoding")
}
if glyph == "space" {
chunkSpaces++
continue
}
if glyph == "controlLF" {
continue
}
metrics, found := style.Font.GetGlyphCharMetrics(glyph)
if !found {
common.Log.Debug("Unsupported glyph %s in font\n", glyph)
return ctx, errors.New("Unsupported text glyph")
}
width += style.FontSize * metrics.Wx
}
spaceWidth += float64(chunkSpaces) * spaceMetrics.Wx * style.FontSize
spaces += chunkSpaces
}
// Add line shifts
objs := []core.PdfObject{}
if p.alignment == TextAlignmentJustify {
// Not to justify last line.
if spaces > 0 && !isLastLine {
spaceWidth = (p.wrapWidth*1000.0 - width) / float64(spaces) / defaultFontSize
}
} else if p.alignment == TextAlignmentCenter {
// Start with a shift.
shift := (p.wrapWidth*1000.0 - width - spaceWidth) / 2 / defaultFontSize
objs = append(objs, core.MakeFloat(-shift))
} else if p.alignment == TextAlignmentRight {
shift := (p.wrapWidth*1000.0 - width - spaceWidth) / defaultFontSize
objs = append(objs, core.MakeFloat(-shift))
}
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
if p.alignment != TextAlignmentJustify || isLastLine {
spaceMetrics, found := style.Font.GetGlyphCharMetrics("space")
if !found {
return ctx, errors.New("The font does not have a space glyph")
}
fontName = fonts[idx][k]
fontSize = style.FontSize
spaceWidth = spaceMetrics.Wx
}
encStr := ""
for _, rn := range chunk.Text {
glyph, found := p.encoder.RuneToGlyph(rn)
if !found {
common.Log.Debug("Rune 0x%x not supported by text encoder", r)
return ctx, errors.New("Unsupported rune in text encoding")
}
if glyph == "space" {
if !found {
common.Log.Debug("Unsupported glyph %s in font\n", glyph)
return ctx, errors.New("Unsupported text glyph")
}
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.MakeString(encStr)}...)
encStr = ""
}
cc.Add_Tf(fontName, fontSize).
Add_TL(fontSize * p.lineHeight).
Add_TJ([]core.PdfObject{core.MakeFloat(-spaceWidth)}...)
} else {
encStr += p.encoder.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.MakeString(encStr)}...)
}
}
}
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
}

View File

@ -0,0 +1,222 @@
/*
* 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 (
"testing"
"github.com/unidoc/unidoc/pdf/model/fonts"
)
func TestParagraphRegularVsStyled(t *testing.T) {
fontRegular := fonts.NewFontHelvetica()
fontBold := fonts.NewFontHelveticaBold()
c := New()
c.NewPage()
// Draw section title.
p := NewParagraph("Regular paragraph vs styled paragraph (should be identical)")
p.SetMargins(0, 0, 20, 10)
p.SetFont(fontBold)
err := c.Draw(p)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
table := NewTable(2)
table.SetColumnWidths(0.5, 0.5)
// Add regular paragraph to table.
p = NewParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.")
p.SetMargins(10, 10, 5, 10)
p.SetFont(fontBold)
p.SetEnableWrap(true)
p.SetTextAlignment(TextAlignmentLeft)
cell := table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(p)
// Add styled paragraph to table.
style := NewTextStyle()
style.Font = fontBold
s := NewStyledParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.", style)
s.SetMargins(10, 10, 5, 10)
s.SetEnableWrap(true)
s.SetTextAlignment(TextAlignmentLeft)
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(s)
// Add regular paragraph to table.
p = NewParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.")
p.SetMargins(10, 10, 5, 10)
p.SetFont(fontRegular)
p.SetEnableWrap(true)
p.SetTextAlignment(TextAlignmentJustify)
p.SetColor(ColorRGBFrom8bit(0, 0, 255))
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(p)
// Add styled paragraph to table.
style.Font = fontRegular
style.Color = ColorRGBFrom8bit(0, 0, 255)
s = NewStyledParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.", style)
s.SetMargins(10, 10, 5, 10)
s.SetEnableWrap(true)
s.SetTextAlignment(TextAlignmentJustify)
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(s)
// Add regular paragraph to table.
p = NewParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.")
p.SetMargins(10, 10, 5, 10)
p.SetFont(fontRegular)
p.SetEnableWrap(true)
p.SetTextAlignment(TextAlignmentRight)
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(p)
// Add styled paragraph to table.
style.Font = fontRegular
style.Color = ColorRGBFrom8bit(0, 0, 0)
s = NewStyledParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.", style)
s.SetMargins(10, 10, 5, 10)
s.SetEnableWrap(true)
s.SetTextAlignment(TextAlignmentRight)
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(s)
// Add regular paragraph to table.
p = NewParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.")
p.SetMargins(10, 10, 5, 10)
p.SetFont(fontBold)
p.SetEnableWrap(true)
p.SetTextAlignment(TextAlignmentCenter)
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(p)
// Add styled paragraph to table.
style.Font = fontBold
style.Color = ColorRGBFrom8bit(0, 0, 0)
s = NewStyledParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar.", style)
s.SetMargins(10, 10, 5, 10)
s.SetEnableWrap(true)
s.SetTextAlignment(TextAlignmentCenter)
cell = table.NewCell()
cell.SetBorder(CellBorderStyleBox, 1)
cell.SetContent(s)
// Draw table.
err = c.Draw(table)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
// Write output file.
err = c.WriteToFile("/tmp/paragraphs_regular_vs_styled.pdf")
if err != nil {
t.Fatalf("Fail: %v\n", err)
}
}
func TestStyledParagraph(t *testing.T) {
fontRegular := fonts.NewFontCourier()
fontBold := fonts.NewFontCourierBold()
fontHelvetica := fonts.NewFontHelvetica()
c := New()
c.NewPage()
// Draw section title.
p := NewParagraph("Styled paragraph")
p.SetMargins(0, 0, 20, 10)
p.SetFont(fontBold)
err := c.Draw(p)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
style := NewTextStyle()
style.Font = fontRegular
s := NewStyledParagraph("This is a paragraph ", style)
s.SetEnableWrap(true)
s.SetTextAlignment(TextAlignmentJustify)
s.SetMargins(0, 0, 10, 0)
style.Color = ColorRGBFrom8bit(255, 0, 0)
s.Append("with different colors ", style)
style.Color = ColorRGBFrom8bit(0, 0, 0)
style.FontSize = 14
s.Append("and with different font sizes ", style)
style.FontSize = 10
style.Font = fontBold
s.Append("and with different font styles ", style)
style.Font = fontHelvetica
style.FontSize = 13
s.Append("and with different fonts ", style)
style.Font = fontBold
style.Color = ColorRGBFrom8bit(0, 0, 255)
style.FontSize = 15
s.Append("and with the changed properties all at once. ", style)
style.Color = ColorRGBFrom8bit(127, 255, 0)
style.FontSize = 12
style.Font = fontHelvetica
s.Append("And maybe try a different color again.", style)
err = c.Draw(s)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
// Test the reset function and also pagination
style.Color = ColorRGBFrom8bit(255, 0, 0)
style.Font = fontRegular
s.Reset("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt. Dis parturient montes nascetur ridiculus mus. Pharetra diam sit amet nisl suscipit adipiscing. Proin fermentum leo vel orci porta. Id diam vel quam elementum pulvinar. ", style)
s.SetTextAlignment(TextAlignmentJustify)
style.Color = ColorRGBFrom8bit(0, 0, 255)
style.FontSize = 15
style.Font = fontHelvetica
s.Append("And maybe try a different color again.", style)
err = c.Draw(s)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
// Write output file.
err = c.WriteToFile("/tmp/styled_paragraph.pdf")
if err != nil {
t.Fatalf("Fail: %v\n", err)
}
}

View File

@ -210,6 +210,19 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
// Add diff to last row.
table.rowHeights[cell.row+cell.rowspan-2] += diffh
}
case *StyledParagraph:
sp := t
if sp.enableWrap {
sp.SetWidth(w - cell.indent)
}
newh := sp.Height() + sp.margins.top + sp.margins.bottom
newh += 0.5 * sp.getTextHeight() // TODO: Make the top margin configurable?
if newh > h {
diffh := newh - h
// Add diff to last row.
table.rowHeights[cell.row+cell.rowspan-2] += diffh
}
case *Image:
img := t
newh := img.Height() + img.margins.top + img.margins.bottom
@ -590,7 +603,7 @@ func (cell *TableCell) Width(ctx DrawContext) float64 {
}
// SetContent sets the cell's content. The content is a VectorDrawable, i.e. a Drawable with a known height and width.
// The currently supported VectorDrawable is: *Paragraph.
// The currently supported VectorDrawable is: *Paragraph, *StyledParagraph.
func (cell *TableCell) SetContent(vd VectorDrawable) error {
switch t := vd.(type) {
case *Paragraph:
@ -599,6 +612,13 @@ func (cell *TableCell) SetContent(vd VectorDrawable) error {
t.enableWrap = false // No wrapping.
}
cell.content = vd
case *StyledParagraph:
if t.defaultWrap {
// Default styled paragraph settings in table: no wrapping.
t.enableWrap = false // No wrapping.
}
cell.content = vd
case *Image:
cell.content = vd

43
pdf/creator/text_style.go Normal file
View File

@ -0,0 +1,43 @@
/*
* 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 (
"github.com/unidoc/unidoc/pdf/model/fonts"
"github.com/unidoc/unidoc/pdf/model/textencoding"
)
// TextStyle is a collection of properties that can be assigned to a chunk of text.
type TextStyle struct {
// The color of the text.
Color Color
// The font the text will use.
Font fonts.Font
// The size of the font.
FontSize float64
}
// NewTextStyle creates a new text style object which can be used with chunks
// of text. Uses default parameters: Helvetica, WinAnsiEncoding and wrap
// enabled with a wrap width of 100 points.
func NewTextStyle() TextStyle {
font := fonts.NewFontHelvetica()
font.SetEncoder(textencoding.NewWinAnsiTextEncoder())
return TextStyle{
Color: ColorRGBFrom8bit(0, 0, 0),
Font: font,
FontSize: 10,
}
}
// TextChunk represents a chunk of text along with a particular style.
type TextChunk struct {
Text string
Style TextStyle
}