diff --git a/pdf/creator/styled_paragraph.go b/pdf/creator/styled_paragraph.go index e9cebbab..0c21f6db 100644 --- a/pdf/creator/styled_paragraph.go +++ b/pdf/creator/styled_paragraph.go @@ -63,6 +63,9 @@ type StyledParagraph struct { // 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. @@ -104,6 +107,23 @@ func (p *StyledParagraph) Append(text string, style TextStyle) { p.wrapText() } +// Insert adds a new text chunk at the specified position in the paragraph. +func (p *StyledParagraph) Insert(index uint, text string, style TextStyle) { + l := uint(len(p.chunks)) + if index > l { + index = l + } + + chunk := TextChunk{ + Text: text, + Style: style, + } + chunk.Style.Font.SetEncoder(p.encoder) + + p.chunks = append(p.chunks[:index], append([]TextChunk{chunk}, p.chunks[index:]...)...) + 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) { @@ -238,6 +258,41 @@ func (p *StyledParagraph) getTextWidth() float64 { return width } +// getTextLineWidth calculates the text width of a provided collection of text chunks +func (p *StyledParagraph) getTextLineWidth(line []TextChunk) float64 { + var width float64 + for _, chunk := range line { + 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 @@ -415,6 +470,10 @@ func (p *StyledParagraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawCon 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 { diff --git a/pdf/creator/text_chunk.go b/pdf/creator/text_chunk.go new file mode 100644 index 00000000..061d3174 --- /dev/null +++ b/pdf/creator/text_chunk.go @@ -0,0 +1,15 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package creator + +// 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 +} diff --git a/pdf/creator/text_style.go b/pdf/creator/text_style.go index c5016de2..e831e367 100644 --- a/pdf/creator/text_style.go +++ b/pdf/creator/text_style.go @@ -35,9 +35,3 @@ func NewTextStyle() TextStyle { FontSize: 10, } } - -// TextChunk represents a chunk of text along with a particular style. -type TextChunk struct { - Text string - Style TextStyle -} diff --git a/pdf/creator/toc_line.go b/pdf/creator/toc_line.go new file mode 100644 index 00000000..6c47166e --- /dev/null +++ b/pdf/creator/toc_line.go @@ -0,0 +1,129 @@ +/* + * 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 ( + "strings" +) + +type TOCLine struct { + sp *StyledParagraph + + number TextChunk + title TextChunk + page TextChunk + separator TextChunk + + level uint + levelOffset float64 +} + +func NewTOCLine(number, title, page string, level uint) *TOCLine { + style := NewTextStyle() + + return NewStyledTOCLine( + TextChunk{Text: number, Style: style}, + TextChunk{Text: title, Style: style}, + TextChunk{Text: page, Style: style}, + level, + ) +} + +func NewStyledTOCLine(number, title, page TextChunk, level uint) *TOCLine { + style := NewTextStyle() + + sp := NewStyledParagraph("", style) + sp.SetEnableWrap(true) + sp.SetTextAlignment(TextAlignmentLeft) + + tl := &TOCLine{ + sp: sp, + number: number, + title: title, + page: page, + separator: TextChunk{ + Text: ".", + Style: style, + }, + level: level, + levelOffset: 10, + } + + sp.margins.left = float64(level) * tl.levelOffset + sp.beforeRender = tl.prepareParagraph + return tl +} + +func (tl *TOCLine) prepareParagraph(sp *StyledParagraph, ctx DrawContext) { + if tl.number.Text != "" { + tl.title.Text = " " + tl.title.Text + } + tl.title.Text += " " + + if tl.page.Text != "" { + tl.page.Text = " " + tl.page.Text + } + + sp.chunks = []TextChunk{ + tl.number, + tl.title, + tl.page, + } + sp.SetEncoder(sp.encoder) + sp.wrapText() + + l := len(sp.lines) + if l == 0 { + return + } + + // Insert separator + availWidth := ctx.Width*1000 - sp.getTextLineWidth(sp.lines[l-1]) + sepWidth := sp.getTextLineWidth([]TextChunk{tl.separator}) + sepCount := int(availWidth / sepWidth) + sepText := strings.Repeat(tl.separator.Text, sepCount) + + sp.Insert(2, sepText, tl.separator.Style) + + // Push page numbers to the end of the line + availWidth = availWidth - float64(sepCount)*sepWidth + if availWidth > 500 { + spaceMetrics, found := tl.separator.Style.Font.GetGlyphCharMetrics("space") + if found && availWidth > spaceMetrics.Wx { + spaces := int(availWidth / spaceMetrics.Wx) + if spaces > 0 { + style := tl.separator.Style + style.FontSize = 1 + sp.Insert(2, strings.Repeat(" ", spaces), style) + } + } + } +} + +func (tl *TOCLine) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + origCtx := ctx + + blocks, ctx, err := tl.sp.GeneratePageBlocks(ctx) + if err != nil { + return blocks, ctx, err + } + if len(blocks) > 1 { + // Did not fit, moved to new Page block. + ctx.Page++ + } + + if tl.sp.positioning.isRelative() { + // Move back X to same start of line. + ctx.X = origCtx.X + } + + if tl.sp.positioning.isAbsolute() { + // If absolute: return original context. + return blocks, origCtx, nil + } + + return blocks, ctx, nil +}