fix wrong git ignore

This commit is contained in:
Adrian Zürcher
2025-12-15 17:44:00 +01:00
parent ed9f31bb96
commit 8f313c00f0
126 changed files with 70589 additions and 1 deletions

View File

@@ -0,0 +1,543 @@
package creator
import (
"errors"
"fmt"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/core"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// Block contains a portion of PDF Page contents. It has a width and a position and can
// be placed anywhere on a Page. It can even contain a whole Page, and is used in the creator
// where each Drawable object can output one or more blocks, each representing content for separate pages
// (typically needed when Page breaks occur).
type Block struct {
// Block contents and resources.
contents *contentstream.ContentStreamOperations
resources *model.PdfPageResources
// Positioning: relative / absolute.
positioning positioning
// Absolute coordinates (when in absolute mode).
xPos, yPos float64
// The bounding box for the block.
width float64
height float64
// Rotation angle.
angle float64
// Margins to be applied around the block when drawing on Page.
margins margins
}
// NewBlock creates a new Block with specified width and height.
func NewBlock(width float64, height float64) *Block {
b := &Block{}
b.contents = &contentstream.ContentStreamOperations{}
b.resources = model.NewPdfPageResources()
b.width = width
b.height = height
return b
}
// NewBlockFromPage creates a Block from a PDF Page. Useful for loading template pages as blocks from a PDF document
// and additional content with the creator.
func NewBlockFromPage(page *model.PdfPage) (*Block, error) {
b := &Block{}
content, err := page.GetAllContentStreams()
if err != nil {
return nil, err
}
contentParser := contentstream.NewContentStreamParser(content)
operations, err := contentParser.Parse()
if err != nil {
return nil, err
}
operations.WrapIfNeeded()
b.contents = operations
if page.Resources != nil {
b.resources = page.Resources
} else {
b.resources = model.NewPdfPageResources()
}
mbox, err := page.GetMediaBox()
if err != nil {
return nil, err
}
if mbox.Llx != 0 || mbox.Lly != 0 {
// Account for media box offset if any.
b.translate(-mbox.Llx, mbox.Lly)
}
b.width = mbox.Urx - mbox.Llx
b.height = mbox.Ury - mbox.Lly
return b, nil
}
// SetAngle sets the rotation angle in degrees.
func (blk *Block) SetAngle(angleDeg float64) {
blk.angle = angleDeg
}
// duplicate duplicates the block with a new copy of the operations list.
func (blk *Block) duplicate() *Block {
dup := &Block{}
// Copy over.
*dup = *blk
dupContents := contentstream.ContentStreamOperations{}
dupContents = append(dupContents, *blk.contents...)
dup.contents = &dupContents
return dup
}
// GeneratePageBlocks draws the block contents on a template Page block.
// Implements the Drawable interface.
func (blk *Block) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
blocks := []*Block{}
if blk.positioning.isRelative() {
// Draw at current ctx.X, ctx.Y position
dup := blk.duplicate()
cc := contentstream.NewContentCreator()
cc.Translate(ctx.X, ctx.PageHeight-ctx.Y-blk.height)
if blk.angle != 0 {
// Make the rotation about the upper left corner.
// XXX/TODO: Account for rotation origin. (Consider).
cc.Translate(0, blk.Height())
cc.RotateDeg(blk.angle)
cc.Translate(0, -blk.Height())
}
contents := append(*cc.Operations(), *dup.contents...)
contents.WrapIfNeeded()
dup.contents = &contents
blocks = append(blocks, dup)
ctx.Y += blk.height
} else {
// Absolute. Draw at blk.xPos, blk.yPos position
dup := blk.duplicate()
cc := contentstream.NewContentCreator()
cc.Translate(blk.xPos, ctx.PageHeight-blk.yPos-blk.height)
if blk.angle != 0 {
// Make the rotation about the upper left corner.
// XXX/TODO: Consider supporting specification of rotation origin.
cc.Translate(0, blk.Height())
cc.RotateDeg(blk.angle)
cc.Translate(0, -blk.Height())
}
contents := append(*cc.Operations(), *dup.contents...)
contents.WrapIfNeeded()
dup.contents = &contents
blocks = append(blocks, dup)
}
return blocks, ctx, nil
}
// Height returns the Block's height.
func (blk *Block) Height() float64 {
return blk.height
}
// Width returns the Block's width.
func (blk *Block) Width() float64 {
return blk.width
}
// addContents adds contents to a block. Wrap both existing and new contents to ensure
// independence of content operations.
func (blk *Block) addContents(operations *contentstream.ContentStreamOperations) {
blk.contents.WrapIfNeeded()
operations.WrapIfNeeded()
*blk.contents = append(*blk.contents, *operations...)
}
// addContentsByString adds contents to a block by contents string.
func (blk *Block) addContentsByString(contents string) error {
cc := contentstream.NewContentStreamParser(contents)
operations, err := cc.Parse()
if err != nil {
return err
}
blk.contents.WrapIfNeeded()
operations.WrapIfNeeded()
*blk.contents = append(*blk.contents, *operations...)
return nil
}
// SetMargins sets the Block's left, right, top, bottom, margins.
func (blk *Block) SetMargins(left, right, top, bottom float64) {
blk.margins.left = left
blk.margins.right = right
blk.margins.top = top
blk.margins.bottom = bottom
}
// GetMargins returns the Block's margins: left, right, top, bottom.
func (blk *Block) GetMargins() (float64, float64, float64, float64) {
return blk.margins.left, blk.margins.right, blk.margins.top, blk.margins.bottom
}
// SetPos sets the Block's positioning to absolute mode with the specified coordinates.
func (blk *Block) SetPos(x, y float64) {
blk.positioning = positionAbsolute
blk.xPos = x
blk.yPos = y
}
// Scale block by specified factors in the x and y directions.
func (blk *Block) Scale(sx, sy float64) {
ops := contentstream.NewContentCreator().
Scale(sx, sy).
Operations()
*blk.contents = append(*ops, *blk.contents...)
blk.contents.WrapIfNeeded()
blk.width *= sx
blk.height *= sy
}
// ScaleToWidth scales the Block to a specified width, maintaining the same aspect ratio.
func (blk *Block) ScaleToWidth(w float64) {
ratio := w / blk.width
blk.Scale(ratio, ratio)
}
// ScaleToHeight scales the Block to a specified height, maintaining the same aspect ratio.
func (blk *Block) ScaleToHeight(h float64) {
ratio := h / blk.height
blk.Scale(ratio, ratio)
}
// translate translates the block, moving block contents on the PDF. For internal use.
func (blk *Block) translate(tx, ty float64) {
ops := contentstream.NewContentCreator().
Translate(tx, -ty).
Operations()
*blk.contents = append(*ops, *blk.contents...)
blk.contents.WrapIfNeeded()
}
// drawToPage draws the block on a PdfPage. Generates the content streams and appends to the PdfPage's content
// stream and links needed resources.
func (blk *Block) drawToPage(page *model.PdfPage) error {
// Check if Page contents are wrapped - if not wrap it.
content, err := page.GetAllContentStreams()
if err != nil {
return err
}
contentParser := contentstream.NewContentStreamParser(content)
ops, err := contentParser.Parse()
if err != nil {
return err
}
ops.WrapIfNeeded()
// Ensure resource dictionaries are available.
if page.Resources == nil {
page.Resources = model.NewPdfPageResources()
}
// Merge the contents into ops.
err = mergeContents(ops, page.Resources, blk.contents, blk.resources)
if err != nil {
return err
}
err = page.SetContentStreams([]string{string(ops.Bytes())}, core.NewFlateEncoder())
if err != nil {
return err
}
return nil
}
// Draw draws the drawable d on the block.
// Note that the drawable must not wrap, i.e. only return one block. Otherwise an error is returned.
func (blk *Block) Draw(d Drawable) error {
ctx := DrawContext{}
ctx.Width = blk.width
ctx.Height = blk.height
ctx.PageWidth = blk.width
ctx.PageHeight = blk.height
ctx.X = 0 // Upper left corner of block
ctx.Y = 0
blocks, _, err := d.GeneratePageBlocks(ctx)
if err != nil {
return err
}
if len(blocks) != 1 {
return errors.New("too many output blocks")
}
for _, newBlock := range blocks {
err := mergeContents(blk.contents, blk.resources, newBlock.contents, newBlock.resources)
if err != nil {
return err
}
}
return nil
}
// DrawWithContext draws the Block using the specified drawing context.
func (blk *Block) DrawWithContext(d Drawable, ctx DrawContext) error {
blocks, _, err := d.GeneratePageBlocks(ctx)
if err != nil {
return err
}
if len(blocks) != 1 {
return errors.New("too many output blocks")
}
for _, newBlock := range blocks {
err := mergeContents(blk.contents, blk.resources, newBlock.contents, newBlock.resources)
if err != nil {
return err
}
}
return nil
}
// mergeBlocks appends another block onto the block.
func (blk *Block) mergeBlocks(toAdd *Block) error {
err := mergeContents(blk.contents, blk.resources, toAdd.contents, toAdd.resources)
return err
}
// mergeContents merges contents and content streams.
// Active in the sense that it modified the input contents and resources.
func mergeContents(contents *contentstream.ContentStreamOperations, resources *model.PdfPageResources,
contentsToAdd *contentstream.ContentStreamOperations, resourcesToAdd *model.PdfPageResources) error {
// To properly add contents from a block, we need to handle the resources that the block is
// using and make sure it is accessible in the modified Page.
//
// Currently supporting: Font, XObject, Colormap, Pattern, Shading, GState resources
// from the block.
//
xobjectMap := map[core.PdfObjectName]core.PdfObjectName{}
fontMap := map[core.PdfObjectName]core.PdfObjectName{}
csMap := map[core.PdfObjectName]core.PdfObjectName{}
patternMap := map[core.PdfObjectName]core.PdfObjectName{}
shadingMap := map[core.PdfObjectName]core.PdfObjectName{}
gstateMap := map[core.PdfObjectName]core.PdfObjectName{}
for _, op := range *contentsToAdd {
switch op.Operand {
case "Do":
// XObject.
if len(op.Params) == 1 {
if name, ok := op.Params[0].(*core.PdfObjectName); ok {
if _, processed := xobjectMap[*name]; !processed {
var useName core.PdfObjectName
// Process if not already processed..
obj, _ := resourcesToAdd.GetXObjectByName(*name)
if obj != nil {
useName = *name
for {
obj2, _ := resources.GetXObjectByName(useName)
if obj2 == nil || obj2 == obj {
break
}
// If there is a conflict... then append "0" to the name..
useName = useName + "0"
}
}
resources.SetXObjectByName(useName, obj)
xobjectMap[*name] = useName
}
useName := xobjectMap[*name]
op.Params[0] = &useName
}
}
case "Tf":
// Font.
if len(op.Params) == 2 {
if name, ok := op.Params[0].(*core.PdfObjectName); ok {
if _, processed := fontMap[*name]; !processed {
var useName core.PdfObjectName
// Process if not already processed.
obj, found := resourcesToAdd.GetFontByName(*name)
if found {
useName = *name
for {
obj2, found := resources.GetFontByName(useName)
if !found || obj2 == obj {
break
}
useName = useName + "0"
}
}
resources.SetFontByName(useName, obj)
fontMap[*name] = useName
}
useName := fontMap[*name]
op.Params[0] = &useName
}
}
case "CS", "cs":
// Colorspace.
if len(op.Params) == 1 {
if name, ok := op.Params[0].(*core.PdfObjectName); ok {
if _, processed := csMap[*name]; !processed {
var useName core.PdfObjectName
// Process if not already processed.
cs, found := resourcesToAdd.GetColorspaceByName(*name)
if found {
useName = *name
for {
cs2, found := resources.GetColorspaceByName(useName)
if !found || cs == cs2 {
break
}
useName = useName + "0"
}
resources.SetColorspaceByName(useName, cs)
csMap[*name] = useName
} else {
common.Log.Debug("Colorspace not found")
}
}
if useName, has := csMap[*name]; has {
op.Params[0] = &useName
} else {
common.Log.Debug("error: Colorspace %s not found", *name)
}
}
}
case "SCN", "scn":
if len(op.Params) == 1 {
if name, ok := op.Params[0].(*core.PdfObjectName); ok {
if _, processed := patternMap[*name]; !processed {
var useName core.PdfObjectName
p, found := resourcesToAdd.GetPatternByName(*name)
if found {
useName = *name
for {
p2, found := resources.GetPatternByName(useName)
if !found || p2 == p {
break
}
useName = useName + "0"
}
err := resources.SetPatternByName(useName, p.ToPdfObject())
if err != nil {
return err
}
patternMap[*name] = useName
}
}
if useName, has := patternMap[*name]; has {
op.Params[0] = &useName
}
}
}
case "sh":
// Shading.
if len(op.Params) == 1 {
if name, ok := op.Params[0].(*core.PdfObjectName); ok {
if _, processed := shadingMap[*name]; !processed {
var useName core.PdfObjectName
// Process if not already processed.
sh, found := resourcesToAdd.GetShadingByName(*name)
if found {
useName = *name
for {
sh2, found := resources.GetShadingByName(useName)
if !found || sh == sh2 {
break
}
useName = useName + "0"
}
err := resources.SetShadingByName(useName, sh.ToPdfObject())
if err != nil {
common.Log.Debug("error Set shading: %v", err)
return err
}
shadingMap[*name] = useName
} else {
common.Log.Debug("Shading not found")
}
}
if useName, has := shadingMap[*name]; has {
op.Params[0] = &useName
} else {
common.Log.Debug("error: Shading %s not found", *name)
}
}
}
case "gs":
// ExtGState.
if len(op.Params) == 1 {
if name, ok := op.Params[0].(*core.PdfObjectName); ok {
if _, processed := gstateMap[*name]; !processed {
var useName core.PdfObjectName
// Process if not already processed.
gs, found := resourcesToAdd.GetExtGState(*name)
if found {
useName = *name
i := 1
for {
gs2, found := resources.GetExtGState(useName)
if !found || gs == gs2 {
break
}
useName = core.PdfObjectName(fmt.Sprintf("GS%d", i))
i++
}
}
resources.AddExtGState(useName, gs)
gstateMap[*name] = useName
}
useName := gstateMap[*name]
op.Params[0] = &useName
}
}
}
*contents = append(*contents, op)
}
return nil
}

View File

@@ -0,0 +1,174 @@
package creator
import (
"errors"
"fmt"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/fonts"
)
// Chapter is used to arrange multiple drawables (paragraphs, images, etc) into a single section. The concept is
// the same as a book or a report chapter.
type Chapter struct {
number int
title string
heading *Paragraph
subchapters int
contents []Drawable
// Show chapter numbering
showNumbering bool
// Include in TOC.
includeInTOC bool
// Positioning: relative / absolute.
positioning positioning
// Margins to be applied around the block when drawing on Page.
margins margins
// Reference to the creator's TOC.
toc *TableOfContents
}
// NewChapter creates a new chapter with the specified title as the heading.
func (c *Creator) NewChapter(title string) *Chapter {
chap := &Chapter{}
c.chapters++
chap.number = c.chapters
chap.title = title
chap.showNumbering = true
chap.includeInTOC = true
heading := fmt.Sprintf("%d. %s", c.chapters, title)
p := NewParagraph(heading)
p.SetFontSize(16)
p.SetFont(fonts.NewFontHelvetica()) // bold?
chap.heading = p
chap.contents = []Drawable{}
// Keep a reference for toc.
chap.toc = c.toc
return chap
}
// SetShowNumbering sets a flag to indicate whether or not to show chapter numbers as part of title.
func (chap *Chapter) SetShowNumbering(show bool) {
if show {
heading := fmt.Sprintf("%d. %s", chap.number, chap.title)
chap.heading.SetText(heading)
} else {
heading := chap.title
chap.heading.SetText(heading)
}
chap.showNumbering = show
}
// SetIncludeInTOC sets a flag to indicate whether or not to include in tOC.
func (chap *Chapter) SetIncludeInTOC(includeInTOC bool) {
chap.includeInTOC = includeInTOC
}
// GetHeading returns the chapter heading paragraph. Used to give access to address style: font, sizing etc.
func (chap *Chapter) GetHeading() *Paragraph {
return chap.heading
}
// SetMargins sets the Chapter margins: left, right, top, bottom.
// Typically not needed as the creator's page margins are used.
func (chap *Chapter) SetMargins(left, right, top, bottom float64) {
chap.margins.left = left
chap.margins.right = right
chap.margins.top = top
chap.margins.bottom = bottom
}
// GetMargins returns the Chapter's margin: left, right, top, bottom.
func (chap *Chapter) GetMargins() (float64, float64, float64, float64) {
return chap.margins.left, chap.margins.right, chap.margins.top, chap.margins.bottom
}
// Add adds a new Drawable to the chapter.
func (chap *Chapter) Add(d Drawable) error {
if Drawable(chap) == d {
common.Log.Debug("error: Cannot add itself")
return errors.New("range check error")
}
switch d.(type) {
case *Chapter:
common.Log.Debug("error: Cannot add chapter to a chapter")
return errors.New("type check error")
case *Paragraph, *Image, *Block, *Subchapter, *Table, *PageBreak:
chap.contents = append(chap.contents, d)
default:
common.Log.Debug("Unsupported: %T", d)
return errors.New("type check error")
}
return nil
}
// GeneratePageBlocks generate the Page blocks. Multiple blocks are generated if the contents wrap over
// multiple pages.
func (chap *Chapter) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
origCtx := ctx
if chap.positioning.isRelative() {
// Update context.
ctx.X += chap.margins.left
ctx.Y += chap.margins.top
ctx.Width -= chap.margins.left + chap.margins.right
ctx.Height -= chap.margins.top
}
blocks, ctx, err := chap.heading.GeneratePageBlocks(ctx)
if err != nil {
return blocks, ctx, err
}
if len(blocks) > 1 {
ctx.Page++ // Did not fit, moved to new Page block.
}
if chap.includeInTOC {
// Add to TOC.
chap.toc.add(chap.title, chap.number, 0, ctx.Page)
}
for _, d := range chap.contents {
newBlocks, c, err := d.GeneratePageBlocks(ctx)
if err != nil {
return blocks, ctx, err
}
if len(newBlocks) < 1 {
continue
}
// The first block is always appended to the last..
blocks[len(blocks)-1].mergeBlocks(newBlocks[0])
blocks = append(blocks, newBlocks[1:]...)
ctx = c
}
if chap.positioning.isRelative() {
// Move back X to same start of line.
ctx.X = origCtx.X
}
if chap.positioning.isAbsolute() {
// If absolute: return original context.
return blocks, origCtx, nil
}
return blocks, ctx, nil
}

View File

@@ -0,0 +1,115 @@
package creator
import (
"fmt"
"math"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
)
// Color interface represents colors in the PDF creator.
type Color interface {
ToRGB() (float64, float64, float64)
}
// Represents RGB color values.
type rgbColor struct {
// Arithmetic representation of r,g,b (range 0-1).
r, g, b float64
}
func (col rgbColor) ToRGB() (float64, float64, float64) {
return col.r, col.g, col.b
}
// Commonly used colors.
var (
ColorBlack = ColorRGBFromArithmetic(0, 0, 0)
ColorWhite = ColorRGBFromArithmetic(1, 1, 1)
ColorRed = ColorRGBFromArithmetic(1, 0, 0)
ColorGreen = ColorRGBFromArithmetic(0, 1, 0)
ColorBlue = ColorRGBFromArithmetic(0, 0, 1)
ColorYellow = ColorRGBFromArithmetic(1, 1, 0)
)
// ColorRGBFromHex converts color hex code to rgb color for using with creator.
// NOTE: If there is a problem interpreting the string, then will use black color and log a debug message.
// Example hex code: #ffffff -> (1,1,1) white.
func ColorRGBFromHex(hexStr string) Color {
color := rgbColor{}
if (len(hexStr) != 4 && len(hexStr) != 7) || hexStr[0] != '#' {
common.Log.Debug("invalid hex code: %s", hexStr)
return color
}
var r, g, b int
if len(hexStr) == 4 {
// Special case: 4 digits: #abc ; where r = a*16+a, e.g. #ffffff -> #fff
var tmp1, tmp2, tmp3 int
n, err := fmt.Sscanf(hexStr, "#%1x%1x%1x", &tmp1, &tmp2, &tmp3)
if err != nil {
common.Log.Debug("invalid hex code: %s, error: %v", hexStr, err)
return color
}
if n != 3 {
common.Log.Debug("invalid hex code: %s", hexStr)
return color
}
r = tmp1*16 + tmp1
g = tmp2*16 + tmp2
b = tmp3*16 + tmp3
} else {
// Default case: 7 digits: #rrggbb
n, err := fmt.Sscanf(hexStr, "#%2x%2x%2x", &r, &g, &b)
if err != nil {
common.Log.Debug("invalid hex code: %s", hexStr)
return color
}
if n != 3 {
common.Log.Debug("invalid hex code: %s, n != 3 (%d)", hexStr, n)
return color
}
}
rNorm := float64(r) / 255.0
gNorm := float64(g) / 255.0
bNorm := float64(b) / 255.0
color.r = rNorm
color.g = gNorm
color.b = bNorm
return color
}
// ColorRGBFrom8bit creates a Color from 8bit (0-255) r,g,b values.
// Example:
//
// red := ColorRGBFrom8Bit(255, 0, 0)
func ColorRGBFrom8bit(r, g, b byte) Color {
color := rgbColor{}
color.r = float64(r) / 255.0
color.g = float64(g) / 255.0
color.b = float64(b) / 255.0
return color
}
// ColorRGBFromArithmetic creates a Color from arithmetic (0-1.0) color values.
// Example:
//
// green := ColorRGBFromArithmetic(0, 1.0, 0)
func ColorRGBFromArithmetic(r, g, b float64) Color {
// Ensure is in the range 0-1:
r = math.Max(math.Min(r, 1.0), 0.0)
g = math.Max(math.Min(g, 1.0), 0.0)
b = math.Max(math.Min(b, 1.0), 0.0)
color := rgbColor{}
color.r = r
color.g = g
color.b = b
return color
}

View File

@@ -0,0 +1,49 @@
package creator
// PageSize represents the page size as a 2 element array representing the width and height in PDF document units (points).
type PageSize [2]float64
// PPI specifies the default PDF resolution in points/inch.
var PPI float64 = 72 // Points per inch. (Default resolution).
// PPMM specifies the default PDF resolution in points/mm.
var PPMM float64 = 72 * 1.0 / 25.4 // Points per mm. (Default resolution).
// Commonly used page sizes
var (
PageSizeA3 = PageSize{297 * PPMM, 420 * PPMM}
PageSizeA4 = PageSize{210 * PPMM, 297 * PPMM}
PageSizeA5 = PageSize{148 * PPMM, 210 * PPMM}
PageSizeLetter = PageSize{8.5 * PPI, 11 * PPI}
PageSizeLegal = PageSize{8.5 * PPI, 14 * PPI}
)
// TextAlignment options for paragraph.
type TextAlignment int
// The options supported for text alignment are:
// left - TextAlignmentLeft
// right - TextAlignmentRight
// center - TextAlignmentCenter
// justify - TextAlignmentJustify
const (
TextAlignmentLeft TextAlignment = iota
TextAlignmentRight
TextAlignmentCenter
TextAlignmentJustify
)
// Relative and absolute positioning types.
type positioning int
const (
positionRelative positioning = iota
positionAbsolute
)
func (p positioning) isRelative() bool {
return p == positionRelative
}
func (p positioning) isAbsolute() bool {
return p == positionAbsolute
}

View File

@@ -0,0 +1,524 @@
package creator
import (
"errors"
"io"
"os"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// Creator is a wrapper around functionality for creating PDF reports and/or adding new
// content onto imported PDF pages, etc.
type Creator struct {
pages []*model.PdfPage
activePage *model.PdfPage
pagesize PageSize
context DrawContext
pageMargins margins
pageWidth, pageHeight float64
// Keep track of number of chapters for indexing.
chapters int
// Hooks.
genFrontPageFunc func(args FrontpageFunctionArgs)
genTableOfContentFunc func(toc *TableOfContents) (*Chapter, error)
drawHeaderFunc func(header *Block, args HeaderFunctionArgs)
drawFooterFunc func(footer *Block, args FooterFunctionArgs)
pdfWriterAccessFunc func(writer *model.PdfWriter) error
finalized bool
toc *TableOfContents
// Forms.
acroForm *model.PdfAcroForm
}
// SetForms Add Acroforms to a PDF file. Sets the specified form for writing.
func (c *Creator) SetForms(form *model.PdfAcroForm) error {
c.acroForm = form
return nil
}
// FrontpageFunctionArgs holds the input arguments to a front page drawing function.
// It is designed as a struct, so additional parameters can be added in the future with backwards compatibility.
type FrontpageFunctionArgs struct {
PageNum int
TotalPages int
}
// HeaderFunctionArgs holds the input arguments to a header drawing function.
// It is designed as a struct, so additional parameters can be added in the future with backwards compatibility.
type HeaderFunctionArgs struct {
PageNum int
TotalPages int
}
// FooterFunctionArgs holds the input arguments to a footer drawing function.
// It is designed as a struct, so additional parameters can be added in the future with backwards compatibility.
type FooterFunctionArgs struct {
PageNum int
TotalPages int
}
// Margins. Can be page margins, or margins around an element.
type margins struct {
left float64
right float64
top float64
bottom float64
}
// New creates a new instance of the PDF Creator.
func New() *Creator {
c := &Creator{}
c.pages = []*model.PdfPage{}
c.SetPageSize(PageSizeLetter)
m := 0.1 * c.pageWidth
c.pageMargins.left = m
c.pageMargins.right = m
c.pageMargins.top = m
c.pageMargins.bottom = m
c.toc = newTableOfContents()
return c
}
// SetPageMargins sets the page margins: left, right, top, bottom.
// The default page margins are 10% of document width.
func (c *Creator) SetPageMargins(left, right, top, bottom float64) {
c.pageMargins.left = left
c.pageMargins.right = right
c.pageMargins.top = top
c.pageMargins.bottom = bottom
}
// Width returns the current page width.
func (c *Creator) Width() float64 {
return c.pageWidth
}
// Height returns the current page height.
func (c *Creator) Height() float64 {
return c.pageHeight
}
func (c *Creator) setActivePage(p *model.PdfPage) {
c.activePage = p
}
func (c *Creator) getActivePage() *model.PdfPage {
if c.activePage == nil {
if len(c.pages) == 0 {
return nil
}
return c.pages[len(c.pages)-1]
}
return c.activePage
}
// SetPageSize sets the Creator's page size. Pages that are added after this will be created with this Page size.
// Does not affect pages already created.
//
// Common page sizes are defined as constants.
// Examples:
// 1. c.SetPageSize(creator.PageSizeA4)
// 2. c.SetPageSize(creator.PageSizeA3)
// 3. c.SetPageSize(creator.PageSizeLegal)
// 4. c.SetPageSize(creator.PageSizeLetter)
//
// For custom sizes: Use the PPMM (points per mm) and PPI (points per inch) when defining those based on
// physical page sizes:
//
// Examples:
// 1. 10x15 sq. mm: SetPageSize(PageSize{10*creator.PPMM, 15*creator.PPMM}) where PPMM is points per mm.
// 2. 3x2 sq. inches: SetPageSize(PageSize{3*creator.PPI, 2*creator.PPI}) where PPI is points per inch.
func (c *Creator) SetPageSize(size PageSize) {
c.pagesize = size
c.pageWidth = size[0]
c.pageHeight = size[1]
// Update default margins to 10% of width.
m := 0.1 * c.pageWidth
c.pageMargins.left = m
c.pageMargins.right = m
c.pageMargins.top = m
c.pageMargins.bottom = m
}
// DrawHeader sets a function to draw a header on created output pages.
func (c *Creator) DrawHeader(drawHeaderFunc func(header *Block, args HeaderFunctionArgs)) {
c.drawHeaderFunc = drawHeaderFunc
}
// DrawFooter sets a function to draw a footer on created output pages.
func (c *Creator) DrawFooter(drawFooterFunc func(footer *Block, args FooterFunctionArgs)) {
c.drawFooterFunc = drawFooterFunc
}
// CreateFrontPage sets a function to generate a front Page.
func (c *Creator) CreateFrontPage(genFrontPageFunc func(args FrontpageFunctionArgs)) {
c.genFrontPageFunc = genFrontPageFunc
}
// CreateTableOfContents sets a function to generate table of contents.
func (c *Creator) CreateTableOfContents(genTOCFunc func(toc *TableOfContents) (*Chapter, error)) {
c.genTableOfContentFunc = genTOCFunc
}
// Create a new Page with current parameters.
func (c *Creator) newPage() *model.PdfPage {
page := model.NewPdfPage()
width := c.pagesize[0]
height := c.pagesize[1]
bbox := model.PdfRectangle{Llx: 0, Lly: 0, Urx: width, Ury: height}
page.MediaBox = &bbox
c.pageWidth = width
c.pageHeight = height
c.initContext()
return page
}
// Initialize the drawing context, moving to upper left corner.
func (c *Creator) initContext() {
// Update context, move to upper left corner.
c.context.X = c.pageMargins.left
c.context.Y = c.pageMargins.top
c.context.Width = c.pageWidth - c.pageMargins.right - c.pageMargins.left
c.context.Height = c.pageHeight - c.pageMargins.bottom - c.pageMargins.top
c.context.PageHeight = c.pageHeight
c.context.PageWidth = c.pageWidth
c.context.Margins = c.pageMargins
}
// NewPage adds a new Page to the Creator and sets as the active Page.
func (c *Creator) NewPage() {
page := c.newPage()
c.pages = append(c.pages, page)
c.context.Page++
}
// AddPage adds the specified page to the creator.
func (c *Creator) AddPage(page *model.PdfPage) error {
mbox, err := page.GetMediaBox()
if err != nil {
common.Log.Debug("Failed to get page mediabox: %v", err)
return err
}
c.context.X = mbox.Llx + c.pageMargins.left
c.context.Y = c.pageMargins.top
c.context.PageHeight = mbox.Ury - mbox.Lly
c.context.PageWidth = mbox.Urx - mbox.Llx
c.pages = append(c.pages, page)
c.context.Page++
return nil
}
// RotateDeg rotates the current active page by angle degrees. An error is returned on failure, which can be
// if there is no currently active page, or the angleDeg is not a multiple of 90 degrees.
func (c *Creator) RotateDeg(angleDeg int64) error {
page := c.getActivePage()
if page == nil {
common.Log.Debug("Fail to rotate: no page currently active")
return errors.New("no page active")
}
if angleDeg%90 != 0 {
common.Log.Debug("error: Page rotation angle not a multiple of 90")
return errors.New("range check error")
}
// Do the rotation.
var rotation int64 = 0
if page.Rotate != nil {
rotation = *(page.Rotate)
}
rotation += angleDeg // Rotate by angleDeg degrees.
page.Rotate = &rotation
return nil
}
// Context returns the current drawing context.
func (c *Creator) Context() DrawContext {
return c.context
}
// Call before writing out. Takes care of adding headers and footers, as well as generating front Page and
// table of contents.
func (c *Creator) finalize() error {
totPages := len(c.pages)
// Estimate number of additional generated pages and update TOC.
genpages := 0
if c.genFrontPageFunc != nil {
genpages++
}
if c.genTableOfContentFunc != nil {
c.initContext()
c.context.Page = genpages + 1
ch, err := c.genTableOfContentFunc(c.toc)
if err != nil {
return err
}
// Make an estimate of the number of pages.
blocks, _, err := ch.GeneratePageBlocks(c.context)
if err != nil {
common.Log.Debug("Failed to generate blocks: %v", err)
return err
}
genpages += len(blocks)
// Update the table of content Page numbers, accounting for front Page and TOC.
for idx := range c.toc.entries {
c.toc.entries[idx].PageNumber += genpages
}
// Remove the TOC chapter entry.
c.toc.entries = c.toc.entries[:len(c.toc.entries)-1]
}
hasFrontPage := false
// Generate the front Page.
if c.genFrontPageFunc != nil {
totPages++
p := c.newPage()
// Place at front.
c.pages = append([]*model.PdfPage{p}, c.pages...)
c.setActivePage(p)
args := FrontpageFunctionArgs{
PageNum: 1,
TotalPages: totPages,
}
c.genFrontPageFunc(args)
hasFrontPage = true
}
if c.genTableOfContentFunc != nil {
c.initContext()
ch, err := c.genTableOfContentFunc(c.toc)
if err != nil {
common.Log.Debug("error generating TOC: %v", err)
return err
}
ch.SetShowNumbering(false)
ch.SetIncludeInTOC(false)
blocks, _, _ := ch.GeneratePageBlocks(c.context)
tocpages := []*model.PdfPage{}
for _, block := range blocks {
block.SetPos(0, 0)
totPages++
p := c.newPage()
// Place at front.
tocpages = append(tocpages, p)
c.setActivePage(p)
c.Draw(block)
}
if hasFrontPage {
front := c.pages[0]
rest := c.pages[1:]
c.pages = append([]*model.PdfPage{front}, tocpages...)
c.pages = append(c.pages, rest...)
} else {
c.pages = append(tocpages, c.pages...)
}
}
for idx, page := range c.pages {
c.setActivePage(page)
if c.drawHeaderFunc != nil {
// Prepare a block to draw on.
// Header is drawn on the top of the page. Has width of the page, but height limited to the page
// margin top height.
headerBlock := NewBlock(c.pageWidth, c.pageMargins.top)
args := HeaderFunctionArgs{
PageNum: idx + 1,
TotalPages: totPages,
}
c.drawHeaderFunc(headerBlock, args)
headerBlock.SetPos(0, 0)
err := c.Draw(headerBlock)
if err != nil {
common.Log.Debug("error drawing header: %v", err)
return err
}
}
if c.drawFooterFunc != nil {
// Prepare a block to draw on.
// Footer is drawn on the bottom of the page. Has width of the page, but height limited to the page
// margin bottom height.
footerBlock := NewBlock(c.pageWidth, c.pageMargins.bottom)
args := FooterFunctionArgs{
PageNum: idx + 1,
TotalPages: totPages,
}
c.drawFooterFunc(footerBlock, args)
footerBlock.SetPos(0, c.pageHeight-footerBlock.height)
err := c.Draw(footerBlock)
if err != nil {
common.Log.Debug("error drawing footer: %v", err)
return err
}
}
}
c.finalized = true
return nil
}
// MoveTo moves the drawing context to absolute coordinates (x, y).
func (c *Creator) MoveTo(x, y float64) {
c.context.X = x
c.context.Y = y
}
// MoveX moves the drawing context to absolute position x.
func (c *Creator) MoveX(x float64) {
c.context.X = x
}
// MoveY moves the drawing context to absolute position y.
func (c *Creator) MoveY(y float64) {
c.context.Y = y
}
// MoveRight moves the drawing context right by relative displacement dx (negative goes left).
func (c *Creator) MoveRight(dx float64) {
c.context.X += dx
}
// MoveDown moves the drawing context down by relative displacement dy (negative goes up).
func (c *Creator) MoveDown(dy float64) {
c.context.Y += dy
}
// Draw draws the Drawable widget to the document. This can span over 1 or more pages. Additional pages are added if
// the contents go over the current Page.
func (c *Creator) Draw(d Drawable) error {
if c.getActivePage() == nil {
// Add a new Page if none added already.
c.NewPage()
}
blocks, ctx, err := d.GeneratePageBlocks(c.context)
if err != nil {
return err
}
for idx, blk := range blocks {
if idx > 0 {
c.NewPage()
}
p := c.getActivePage()
err := blk.drawToPage(p)
if err != nil {
return err
}
}
// Inner elements can affect X, Y position and available height.
c.context.X = ctx.X
c.context.Y = ctx.Y
c.context.Height = ctx.PageHeight - ctx.Y - ctx.Margins.bottom
return nil
}
// Write output of creator to io.WriteSeeker interface.
func (c *Creator) Write(ws io.WriteSeeker) error {
if !c.finalized {
c.finalize()
}
pdfWriter := model.NewPdfWriter()
// Form fields.
if c.acroForm != nil {
errF := pdfWriter.SetForms(c.acroForm)
if errF != nil {
common.Log.Debug("Failure: %v", errF)
return errF
}
}
// Pdf Writer access hook. Can be used to encrypt, etc. via the PdfWriter instance.
if c.pdfWriterAccessFunc != nil {
err := c.pdfWriterAccessFunc(&pdfWriter)
if err != nil {
common.Log.Debug("Failure: %v", err)
return err
}
}
for _, page := range c.pages {
err := pdfWriter.AddPage(page)
if err != nil {
common.Log.Error("Failed to add Page: %s", err)
return err
}
}
err := pdfWriter.Write(ws)
if err != nil {
return err
}
return nil
}
// SetPdfWriterAccessFunc sets a PdfWriter access function/hook.
// Exposes the PdfWriter just prior to writing the PDF. Can be used to encrypt the output PDF, etc.
//
// Example of encrypting with a user/owner password "password"
// Prior to calling c.WriteFile():
//
// c.SetPdfWriterAccessFunc(func(w *model.PdfWriter) error {
// userPass := []byte("password")
// ownerPass := []byte("password")
// err := w.Encrypt(userPass, ownerPass, nil)
// return err
// })
func (c *Creator) SetPdfWriterAccessFunc(pdfWriterAccessFunc func(writer *model.PdfWriter) error) {
c.pdfWriterAccessFunc = pdfWriterAccessFunc
}
// WriteToFile writes the Creator output to file specified by path.
func (c *Creator) WriteToFile(outputPath string) error {
fWrite, err := os.Create(outputPath)
if err != nil {
return err
}
defer fWrite.Close()
err = c.Write(fWrite)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,66 @@
package creator
import (
"fmt"
"strings"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// NewCurve returns new instance of Curve between points (x1,y1) and (x2, y2) with control point (cx,cy).
func NewCurve(x1, y1, cx, cy, x2, y2 float64) *Curve {
c := &Curve{}
c.x1 = x1
c.y1 = y1
c.cx = cx
c.cy = cy
c.x2 = x2
c.y2 = y2
c.lineColor = model.NewPdfColorDeviceRGB(0, 0, 0)
c.lineWidth = 1.0
return c
}
// Curve represents a cubic Bezier curve with a control point.
type Curve struct {
x1 float64
y1 float64
cx float64 // control point
cy float64
x2 float64
y2 float64
lineColor *model.PdfColorDeviceRGB
lineWidth float64
}
// SetWidth sets line width.
func (c *Curve) SetWidth(width float64) {
c.lineWidth = width
}
// SetColor sets the line color.
func (c *Curve) SetColor(col Color) {
c.lineColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// GeneratePageBlocks draws the curve onto page blocks.
func (c *Curve) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
block := NewBlock(ctx.PageWidth, ctx.PageHeight)
var ops []string
ops = append(ops, fmt.Sprintf("%.2f w", c.lineWidth)) // line widtdh
ops = append(ops, fmt.Sprintf("%.3f %.3f %.3f RG", c.lineColor[0], c.lineColor[1], c.lineColor[2])) // line color
ops = append(ops, fmt.Sprintf("%.2f %.2f m", c.x1, ctx.PageHeight-c.y1)) // move to
ops = append(ops, fmt.Sprintf("%.5f %.5f %.5f %.5f v S", c.cx, ctx.PageHeight-c.cy, c.x2, ctx.PageHeight-c.y2))
err := block.addContentsByString(strings.Join(ops, "\n"))
if err != nil {
return nil, ctx, err
}
return []*Block{block}, ctx, nil
}

View File

@@ -0,0 +1,189 @@
package creator
import (
"errors"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
)
// Division is a container component which can wrap across multiple pages (unlike Block).
// 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.
// Also supports horizontal stacking by activating the inline mode.
type Division struct {
components []VectorDrawable
// Positioning: relative / absolute.
positioning positioning
// Margins to be applied around the block when drawing on Page.
margins margins
// Controls whether the components are stacked horizontally
inline bool
}
// NewDivision returns a new Division container component.
func NewDivision() *Division {
return &Division{
components: []VectorDrawable{},
}
}
// Inline returns whether the inline mode of the division is active.
func (div *Division) Inline() bool {
return div.inline
}
// SetInline sets the inline mode of the division.
func (div *Division) SetInline(inline bool) {
div.inline = inline
}
// Add adds a VectorDrawable to the Division container.
// 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
}
if !supported {
return errors.New("unsupported type in Division")
}
div.components = append(div.components, d)
return nil
}
// Height returns the height for the Division component assuming all stacked on top of each other.
func (div *Division) Height() float64 {
y := 0.0
yMax := 0.0
for _, component := range div.components {
compWidth, compHeight := component.Width(), component.Height()
switch t := component.(type) {
case *Paragraph:
p := t
compWidth += p.margins.left + p.margins.right
compHeight += p.margins.top + p.margins.bottom
}
// Vertical stacking.
y += compHeight
yMax = y
}
return yMax
}
// Width is not used. Not used as a Division element is designed to fill into available width depending on
// context. Returns 0.
func (div *Division) Width() float64 {
return 0
}
// GeneratePageBlocks generates the page blocks for the Division component.
// Multiple blocks are generated if the contents wrap over multiple pages.
func (div *Division) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
pageblocks := []*Block{}
origCtx := ctx
if div.positioning.isRelative() {
// Update context.
ctx.X += div.margins.left
ctx.Y += div.margins.top
ctx.Width -= div.margins.left + div.margins.right
ctx.Height -= div.margins.top
}
// Set the inline mode of the division to the context.
ctx.Inline = div.inline
// Draw.
divCtx := ctx
tmpCtx := ctx
var lineHeight float64
for _, component := range div.components {
if ctx.Inline {
// Check whether the component fits on the current line.
if (ctx.X-divCtx.X)+component.Width() <= ctx.Width {
ctx.Y = tmpCtx.Y
ctx.Height = tmpCtx.Height
} else {
ctx.X = divCtx.X
ctx.Width = divCtx.Width
tmpCtx.Y += lineHeight
tmpCtx.Height -= lineHeight
lineHeight = 0
}
}
newblocks, updCtx, err := component.GeneratePageBlocks(ctx)
if err != nil {
common.Log.Debug("error generating page blocks: %v", err)
return nil, ctx, err
}
if len(newblocks) < 1 {
continue
}
if len(pageblocks) > 0 {
// If there are pageblocks already in place.
// merge the first block in with current Block and append the rest.
pageblocks[len(pageblocks)-1].mergeBlocks(newblocks[0])
pageblocks = append(pageblocks, newblocks[1:]...)
} else {
pageblocks = append(pageblocks, newblocks[0:]...)
}
// Apply padding/margins.
if ctx.Inline {
// Recalculate positions on page change.
if ctx.Page != updCtx.Page {
divCtx.Y = ctx.Margins.top
divCtx.Height = ctx.PageHeight - ctx.Margins.top
tmpCtx.Y = divCtx.Y
tmpCtx.Height = divCtx.Height
lineHeight = updCtx.Height - divCtx.Height
} else {
// Calculate current line max height.
if dl := ctx.Height - updCtx.Height; dl > lineHeight {
lineHeight = dl
}
}
} else {
updCtx.X = ctx.X
}
ctx = updCtx
}
// Restore the original inline mode of the context.
ctx.Inline = origCtx.Inline
if div.positioning.isRelative() {
// Move back X to same start of line.
ctx.X = origCtx.X
}
if div.positioning.isAbsolute() {
// If absolute: return original context.
return pageblocks, origCtx, nil
}
return pageblocks, ctx, nil
}

View File

@@ -0,0 +1,44 @@
package creator
// Drawable is a widget that can be used to draw with the Creator.
type Drawable interface {
// Draw onto blocks representing Page contents. As the content can wrap over many pages, multiple
// templates are returned, one per Page. The function also takes a draw context containing information
// where to draw (if relative positioning) and the available height to draw on accounting for Margins etc.
GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error)
}
// VectorDrawable is a Drawable with a specified width and height.
type VectorDrawable interface {
Drawable
// Width returns the width of the Drawable.
Width() float64
// Height returns the height of the Drawable.
Height() float64
}
// DrawContext defines the drawing context. The DrawContext is continuously used and updated when drawing the page
// contents in relative mode. Keeps track of current X, Y position, available height as well as other page parameters
// such as margins and dimensions.
type DrawContext struct {
// Current page number.
Page int
// Current position. In a relative positioning mode, a drawable will be placed at these coordinates.
X, Y float64
// Context dimensions. Available width and height (on current page).
Width, Height float64
// Page Margins.
Margins margins
// Absolute Page size, widths and height.
PageWidth float64
PageHeight float64
// Controls whether the components are stacked horizontally
Inline bool
}

View File

@@ -0,0 +1,89 @@
package creator
import (
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream/draw"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// Ellipse defines an ellipse with a center at (xc,yc) and a specified width and height. The ellipse can have a colored
// fill and/or border with a specified width.
// Implements the Drawable interface and can be drawn on PDF using the Creator.
type Ellipse struct {
xc float64
yc float64
width float64
height float64
fillColor *model.PdfColorDeviceRGB
borderColor *model.PdfColorDeviceRGB
borderWidth float64
}
// NewEllipse creates a new ellipse centered at (xc,yc) with a width and height specified.
func NewEllipse(xc, yc, width, height float64) *Ellipse {
ell := &Ellipse{}
ell.xc = xc
ell.yc = yc
ell.width = width
ell.height = height
ell.borderColor = model.NewPdfColorDeviceRGB(0, 0, 0)
ell.borderWidth = 1.0
return ell
}
// GetCoords returns the coordinates of the Ellipse's center (xc,yc).
func (ell *Ellipse) GetCoords() (float64, float64) {
return ell.xc, ell.yc
}
// SetBorderWidth sets the border width.
func (ell *Ellipse) SetBorderWidth(bw float64) {
ell.borderWidth = bw
}
// SetBorderColor sets the border color.
func (ell *Ellipse) SetBorderColor(col Color) {
ell.borderColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// SetFillColor sets the fill color.
func (ell *Ellipse) SetFillColor(col Color) {
ell.fillColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// GeneratePageBlocks draws the rectangle on a new block representing the page.
func (ell *Ellipse) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
block := NewBlock(ctx.PageWidth, ctx.PageHeight)
drawell := draw.Circle{
X: ell.xc - ell.width/2,
Y: ctx.PageHeight - ell.yc - ell.height/2,
Width: ell.width,
Height: ell.height,
Opacity: 1.0,
BorderWidth: ell.borderWidth,
}
if ell.fillColor != nil {
drawell.FillEnabled = true
drawell.FillColor = ell.fillColor
}
if ell.borderColor != nil {
drawell.BorderEnabled = true
drawell.BorderColor = ell.borderColor
drawell.BorderWidth = ell.borderWidth
}
contents, _, err := drawell.Draw("")
if err != nil {
return nil, ctx, err
}
err = block.addContentsByString(string(contents))
if err != nil {
return nil, ctx, err
}
return []*Block{block}, ctx, nil
}

View File

@@ -0,0 +1,107 @@
package creator
import (
pdfcontent "gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream/draw"
pdfcore "gitea.tecamino.com/paadi/pdfmerge/internal/pdf/core"
pdf "gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// FilledCurve represents a closed path of Bezier curves with a border and fill.
type FilledCurve struct {
curves []draw.CubicBezierCurve
FillEnabled bool // Show fill?
fillColor *pdf.PdfColorDeviceRGB
BorderEnabled bool // Show border?
BorderWidth float64
borderColor *pdf.PdfColorDeviceRGB
}
// NewFilledCurve returns a instance of filled curve.
func NewFilledCurve() *FilledCurve {
curve := FilledCurve{}
curve.curves = []draw.CubicBezierCurve{}
return &curve
}
// AppendCurve appends a Bezier curve to the filled curve.
func (fc *FilledCurve) AppendCurve(curve draw.CubicBezierCurve) *FilledCurve {
fc.curves = append(fc.curves, curve)
return fc
}
// SetFillColor sets the fill color for the path.
func (fc *FilledCurve) SetFillColor(color Color) {
fc.fillColor = pdf.NewPdfColorDeviceRGB(color.ToRGB())
}
// SetBorderColor sets the border color for the path.
func (fc *FilledCurve) SetBorderColor(color Color) {
fc.borderColor = pdf.NewPdfColorDeviceRGB(color.ToRGB())
}
// draw draws the filled curve. Can specify a graphics state (gsName) for setting opacity etc. Otherwise leave empty ("").
// Returns the content stream as a byte array, the bounding box and an error on failure.
func (fc *FilledCurve) draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
bpath := draw.NewCubicBezierPath()
for _, c := range fc.curves {
bpath = bpath.AppendCurve(c)
}
creator := pdfcontent.NewContentCreator()
creator.Add_q()
if fc.FillEnabled {
creator.Add_rg(fc.fillColor.R(), fc.fillColor.G(), fc.fillColor.B())
}
if fc.BorderEnabled {
creator.Add_RG(fc.borderColor.R(), fc.borderColor.G(), fc.borderColor.B())
creator.Add_w(fc.BorderWidth)
}
if len(gsName) > 1 {
// If a graphics state is provided, use it. (can support transparency).
creator.Add_gs(pdfcore.PdfObjectName(gsName))
}
draw.DrawBezierPathWithCreator(bpath, creator)
creator.Add_h() // Close the path.
if fc.FillEnabled && fc.BorderEnabled {
creator.Add_B() // fill and stroke.
} else if fc.FillEnabled {
creator.Add_f() // Fill.
} else if fc.BorderEnabled {
creator.Add_S() // Stroke.
}
creator.Add_Q()
// Get bounding box.
pathBbox := bpath.GetBoundingBox()
if fc.BorderEnabled {
// Account for stroke width.
pathBbox.Height += fc.BorderWidth
pathBbox.Width += fc.BorderWidth
pathBbox.X -= fc.BorderWidth / 2
pathBbox.Y -= fc.BorderWidth / 2
}
// Bounding box - global coordinate system.
bbox := &pdf.PdfRectangle{}
bbox.Llx = pathBbox.X
bbox.Lly = pathBbox.Y
bbox.Urx = pathBbox.X + pathBbox.Width
bbox.Ury = pathBbox.Y + pathBbox.Height
return creator.Bytes(), bbox, nil
}
// GeneratePageBlocks draws the filled curve on page blocks.
func (fc *FilledCurve) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
block := NewBlock(ctx.PageWidth, ctx.PageHeight)
contents, _, _ := fc.draw("")
err := block.addContentsByString(string(contents))
if err != nil {
return nil, ctx, err
}
return []*Block{block}, ctx, nil
}

View File

@@ -0,0 +1,329 @@
package creator
import (
"bytes"
"fmt"
goimage "image"
"os"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/core"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// The Image type is used to draw an image onto PDF.
type Image struct {
xobj *model.XObjectImage
img *model.Image
// Rotation angle.
angle float64
// The dimensions of the image. As to be placed on the PDF.
width, height float64
// The original dimensions of the image (pixel based).
origWidth, origHeight float64
// Positioning: relative / absolute.
positioning positioning
// Absolute coordinates (when in absolute mode).
xPos float64
yPos float64
// Opacity (alpha value).
opacity float64
// Margins to be applied around the block when drawing on Page.
margins margins
// Encoder
encoder core.StreamEncoder
}
// NewImage create a new image from a image (model.Image).
func NewImage(img *model.Image) (*Image, error) {
image := &Image{}
image.img = img
// Image original size in points = pixel size.
image.origWidth = float64(img.Width)
image.origHeight = float64(img.Height)
image.width = image.origWidth
image.height = image.origHeight
image.angle = 0
image.opacity = 1.0
image.positioning = positionRelative
return image, nil
}
// NewImageFromData creates an Image from image data.
func NewImageFromData(data []byte) (*Image, error) {
imgReader := bytes.NewReader(data)
// Load the image with default handler.
img, err := model.ImageHandling.Read(imgReader)
if err != nil {
common.Log.Error("error loading image: %s", err)
return nil, err
}
return NewImage(img)
}
// NewImageFromFile creates an Image from a file.
func NewImageFromFile(path string) (*Image, error) {
imgData, err := os.ReadFile(path)
if err != nil {
return nil, err
}
img, err := NewImageFromData(imgData)
if err != nil {
return nil, err
}
return img, nil
}
// NewImageFromGoImage creates an Image from a go image.Image datastructure.
func NewImageFromGoImage(goimg goimage.Image) (*Image, error) {
img, err := model.ImageHandling.NewImageFromGoImage(goimg)
if err != nil {
return nil, err
}
return NewImage(img)
}
// SetEncoder sets the encoding/compression mechanism for the image.
func (img *Image) SetEncoder(encoder core.StreamEncoder) {
img.encoder = encoder
}
// Height returns Image's document height.
func (img *Image) Height() float64 {
return img.height
}
// Width returns Image's document width.
func (img *Image) Width() float64 {
return img.width
}
// SetOpacity sets opacity for Image.
func (img *Image) SetOpacity(opacity float64) {
img.opacity = opacity
}
// SetMargins sets the margins for the Image (in relative mode): left, right, top, bottom.
func (img *Image) SetMargins(left, right, top, bottom float64) {
img.margins.left = left
img.margins.right = right
img.margins.top = top
img.margins.bottom = bottom
}
// GetMargins returns the Image's margins: left, right, top, bottom.
func (img *Image) GetMargins() (float64, float64, float64, float64) {
return img.margins.left, img.margins.right, img.margins.top, img.margins.bottom
}
// makeXObject makes the encoded XObject Image that will be used in the PDF.
func (img *Image) makeXObject() error {
encoder := img.encoder
if encoder == nil {
// Default: Use flate encoder.
encoder = core.NewFlateEncoder()
}
// Create the XObject image.
ximg, err := model.NewXObjectImageFromImage(img.img, nil, encoder)
if err != nil {
common.Log.Error("Failed to create xobject image: %s", err)
return err
}
img.xobj = ximg
return nil
}
// GeneratePageBlocks generate the Page blocks. Draws the Image on a block, implementing the Drawable interface.
func (img *Image) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
if img.xobj == nil {
// Build the XObject Image if not already prepared.
img.makeXObject()
}
blocks := []*Block{}
origCtx := ctx
blk := NewBlock(ctx.PageWidth, ctx.PageHeight)
if img.positioning.isRelative() {
if img.height > ctx.Height {
// Goes out of the bounds. Write on a new template instead and create a new context at upper
// left corner.
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 + img.margins.left
newContext.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom - img.margins.bottom
newContext.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right - img.margins.left - img.margins.right
ctx = newContext
} else {
ctx.Y += img.margins.top
ctx.Height -= img.margins.top + img.margins.bottom
ctx.X += img.margins.left
ctx.Width -= img.margins.left + img.margins.right
}
} else {
// Absolute.
ctx.X = img.xPos
ctx.Y = img.yPos
}
// Place the Image on the template at position (x,y) based on the ctx.
ctx, err := drawImageOnBlock(blk, img, ctx)
if err != nil {
return nil, ctx, err
}
blocks = append(blocks, blk)
if img.positioning.isAbsolute() {
// Absolute drawing should not affect context.
ctx = origCtx
} else {
// XXX/TODO: Use projected height.
ctx.Y += img.margins.bottom
ctx.Height -= img.margins.bottom
}
return blocks, ctx, nil
}
// SetPos sets the absolute position. Changes object positioning to absolute.
func (img *Image) SetPos(x, y float64) {
img.positioning = positionAbsolute
img.xPos = x
img.yPos = y
}
// Scale scales Image by a constant factor, both width and height.
func (img *Image) Scale(xFactor, yFactor float64) {
img.width = xFactor * img.width
img.height = yFactor * img.height
}
// ScaleToWidth scale Image to a specified width w, maintaining the aspect ratio.
func (img *Image) ScaleToWidth(w float64) {
ratio := img.height / img.width
img.width = w
img.height = w * ratio
}
// ScaleToHeight scale Image to a specified height h, maintaining the aspect ratio.
func (img *Image) ScaleToHeight(h float64) {
ratio := img.width / img.height
img.height = h
img.width = h * ratio
}
// SetWidth set the Image's document width to specified w. This does not change the raw image data, i.e.
// no actual scaling of data is performed. That is handled by the PDF viewer.
func (img *Image) SetWidth(w float64) {
img.width = w
}
// SetHeight sets the Image's document height to specified h.
func (img *Image) SetHeight(h float64) {
img.height = h
}
// SetAngle sets Image rotation angle in degrees.
func (img *Image) SetAngle(angle float64) {
img.angle = angle
}
// Draw the image onto the specified blk.
func drawImageOnBlock(blk *Block, img *Image, ctx DrawContext) (DrawContext, error) {
origCtx := ctx
// Find a free name for the image.
num := 1
imgName := core.PdfObjectName(fmt.Sprintf("Img%d", num))
for blk.resources.HasXObjectByName(imgName) {
num++
imgName = core.PdfObjectName(fmt.Sprintf("Img%d", num))
}
// Add to the Page resources.
err := blk.resources.SetXObjectImageByName(imgName, img.xobj)
if err != nil {
return ctx, err
}
// Find an available GS name.
i := 0
gsName := core.PdfObjectName(fmt.Sprintf("GS%d", i))
for blk.resources.HasExtGState(gsName) {
i++
gsName = core.PdfObjectName(fmt.Sprintf("GS%d", i))
}
// Graphics state with normal blend mode.
gs0 := core.MakeDict()
gs0.Set("BM", core.MakeName("Normal"))
if img.opacity < 1.0 {
gs0.Set("CA", core.MakeFloat(img.opacity))
gs0.Set("ca", core.MakeFloat(img.opacity))
}
err = blk.resources.AddExtGState(gsName, core.MakeIndirectObject(gs0))
if err != nil {
return ctx, err
}
xPos := ctx.X
yPos := ctx.PageHeight - ctx.Y - img.Height()
angle := img.angle
// Create content stream to add to the Page contents.
contentCreator := contentstream.NewContentCreator()
contentCreator.Add_gs(gsName) // Set graphics state.
contentCreator.Translate(xPos, yPos)
if angle != 0 {
// Make the rotation about the upper left corner.
contentCreator.Translate(0, img.Height())
contentCreator.RotateDeg(angle)
contentCreator.Translate(0, -img.Height())
}
contentCreator.
Scale(img.Width(), img.Height()).
Add_Do(imgName) // Draw the image.
ops := contentCreator.Operations()
ops.WrapIfNeeded()
blk.addContents(ops)
if img.positioning.isRelative() {
ctx.Y += img.Height()
ctx.Height -= img.Height()
return ctx, nil
}
// Absolute positioning - return original context.
return origCtx, nil
}

View File

@@ -0,0 +1,85 @@
package creator
import (
"math"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream/draw"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// Line defines a line between point 1 (X1,Y1) and point 2 (X2,Y2). The line ending styles can be none (regular line),
// or arrows at either end. The line also has a specified width, color and opacity.
// Implements the Drawable interface and can be drawn on PDF using the Creator.
type Line struct {
x1 float64
y1 float64
x2 float64
y2 float64
lineColor *model.PdfColorDeviceRGB
lineWidth float64
}
// NewLine creates a new Line with default parameters between (x1,y1) to (x2,y2).
func NewLine(x1, y1, x2, y2 float64) *Line {
l := &Line{}
l.x1 = x1
l.y1 = y1
l.x2 = x2
l.y2 = y2
l.lineColor = model.NewPdfColorDeviceRGB(0, 0, 0)
l.lineWidth = 1.0
return l
}
// GetCoords returns the (x1, y1), (x2, y2) points defining the Line.
func (l *Line) GetCoords() (float64, float64, float64, float64) {
return l.x1, l.y1, l.x2, l.y2
}
// SetLineWidth sets the line width.
func (l *Line) SetLineWidth(lw float64) {
l.lineWidth = lw
}
// SetColor sets the line color.
// Use ColorRGBFromHex, ColorRGBFrom8bit or ColorRGBFromArithmetic to make the color object.
func (l *Line) SetColor(col Color) {
l.lineColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// Length calculates and returns the line length.
func (l *Line) Length() float64 {
return math.Sqrt(math.Pow(l.x2-l.x1, 2.0) + math.Pow(l.y2-l.y1, 2.0))
}
// GeneratePageBlocks draws the line on a new block representing the page. Implements the Drawable interface.
func (l *Line) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
block := NewBlock(ctx.PageWidth, ctx.PageHeight)
drawline := draw.Line{
LineWidth: l.lineWidth,
Opacity: 1.0,
LineColor: l.lineColor,
LineEndingStyle1: draw.LineEndingStyleNone,
LineEndingStyle2: draw.LineEndingStyleNone,
X1: l.x1,
Y1: ctx.PageHeight - l.y1,
X2: l.x2,
Y2: ctx.PageHeight - l.y2,
}
contents, _, err := drawline.Draw("")
if err != nil {
return nil, ctx, err
}
err = block.addContentsByString(string(contents))
if err != nil {
return nil, ctx, err
}
return []*Block{block}, ctx, nil
}

View File

@@ -0,0 +1,31 @@
package creator
// PageBreak represents a page break for a chapter.
type PageBreak struct {
}
// NewPageBreak create a new page break.
func NewPageBreak() *PageBreak {
return &PageBreak{}
}
// GeneratePageBlocks generates a page break block.
func (p *PageBreak) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
// Return two empty blocks. First one simply means that there is nothing more to add at the current page.
// The second one starts a new page.
blocks := []*Block{
NewBlock(ctx.PageWidth, ctx.PageHeight-ctx.Y),
NewBlock(ctx.PageWidth, ctx.PageHeight),
}
// New Page. Place context in upper left corner (with margins).
ctx.Page++
newContext := ctx
newContext.Y = ctx.Margins.top
newContext.X = ctx.Margins.left
newContext.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom
newContext.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right
ctx = newContext
return blocks, ctx, nil
}

View File

@@ -0,0 +1,525 @@
package creator
import (
"errors"
"fmt"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/core"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/fonts"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/textencoding"
)
// Paragraph 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 Paragraph struct {
// The input utf-8 text as a string (series of runes).
text string
// The text encoder which can convert the text (as runes) into a series of glyphs and get character metrics.
encoder textencoding.TextEncoder
// The font to be used to draw the text.
textFont fonts.Font
// 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
// 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, scaleY float64
// Text lines after wrapping to available width.
textLines []string
}
// NewParagraph create a new text paragraph. Uses default parameters: Helvetica, WinAnsiEncoding and wrap enabled
// with a wrap width of 100 points.
func NewParagraph(text string) *Paragraph {
p := &Paragraph{}
p.text = text
p.textFont = fonts.NewFontHelvetica()
p.SetEncoder(textencoding.NewWinAnsiTextEncoder())
p.fontSize = 10
p.lineHeight = 1.0
// TODO: Can we wrap intellectually, only if given width is known?
p.enableWrap = true
p.defaultWrap = true
p.SetColor(ColorRGBFrom8bit(0, 0, 0))
p.alignment = TextAlignmentLeft
p.angle = 0
p.scaleX = 1
p.scaleY = 1
p.positioning = positionRelative
return p
}
// SetFont sets the Paragraph's font.
func (p *Paragraph) SetFont(font fonts.Font) {
p.textFont = font
}
// SetFontSize sets the font size in document units (points).
func (p *Paragraph) SetFontSize(fontSize float64) {
p.fontSize = fontSize
}
// SetTextAlignment sets the horizontal alignment of the text within the space provided.
func (p *Paragraph) SetTextAlignment(align TextAlignment) {
p.alignment = align
}
// SetEncoder sets the text encoding.
func (p *Paragraph) SetEncoder(encoder textencoding.TextEncoder) {
p.encoder = encoder
// Sync with the text font too.
// XXX/FIXME: Keep in 1 place only.
p.textFont.SetEncoder(encoder)
}
// SetLineHeight sets the line height (1.0 default).
func (p *Paragraph) SetLineHeight(lineheight float64) {
p.lineHeight = lineheight
}
// SetText sets the text content of the Paragraph.
func (p *Paragraph) SetText(text string) {
p.text = text
}
// Text sets the text content of the Paragraph.
func (p *Paragraph) Text() string {
return p.text
}
// SetEnableWrap sets the line wrapping enabled flag.
func (p *Paragraph) SetEnableWrap(enableWrap bool) {
p.enableWrap = enableWrap
p.defaultWrap = false
}
// SetColor set the color of the Paragraph text.
//
// Example:
//
// 1. p := NewParagraph("Red paragraph")
// // Set to red color with a hex code:
// p.SetColor(creator.ColorRGBFromHex("#ff0000"))
//
// 2. Make Paragraph green with 8-bit rgb values (0-255 each component)
// p.SetColor(creator.ColorRGBFrom8bit(0, 255, 0)
//
// 3. Make Paragraph blue with arithmetic (0-1) rgb components.
// p.SetColor(creator.ColorRGBFromArithmetic(0, 0, 1.0)
func (p *Paragraph) SetColor(col Color) {
pdfColor := model.NewPdfColorDeviceRGB(col.ToRGB())
p.color = *pdfColor
}
// SetPos sets absolute positioning with specified coordinates.
func (p *Paragraph) SetPos(x, y float64) {
p.positioning = positionAbsolute
p.xPos = x
p.yPos = y
}
// SetAngle sets the rotation angle of the text.
func (p *Paragraph) SetAngle(angle float64) {
p.angle = angle
}
// SetMargins sets the Paragraph's margins.
func (p *Paragraph) 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 *Paragraph) 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 *Paragraph) SetWidth(width float64) {
p.wrapWidth = width
p.wrapText()
}
// Width returns the width of the Paragraph.
func (p *Paragraph) 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 *Paragraph) Height() float64 {
if len(p.textLines) == 0 {
p.wrapText()
}
h := float64(len(p.textLines)) * p.lineHeight * p.fontSize
return h
}
// getTextWidth calculates the text width as if all in one line (not taking wrapping into account).
func (p *Paragraph) getTextWidth() float64 {
w := float64(0.0)
for _, rune := range p.text {
glyph, found := p.encoder.RuneToGlyph(rune)
if !found {
common.Log.Debug("error! Glyph not found for rune: %s\n", rune)
return -1 // XXX/FIXME: return error.
}
// Ignore newline for this.. Handles as if all in one line.
if glyph == "controlLF" {
continue
}
metrics, found := p.textFont.GetGlyphCharMetrics(glyph)
if !found {
common.Log.Debug("Glyph char metrics not found! %s\n", glyph)
return -1 // XXX/FIXME: return error.
}
w += p.fontSize * metrics.Wx
}
return w
}
// Simple algorithm to wrap the text into lines (greedy algorithm - fill the lines).
// XXX/TODO: Consider the Knuth/Plass algorithm or an alternative.
func (p *Paragraph) wrapText() error {
if !p.enableWrap {
p.textLines = []string{p.text}
return nil
}
line := []rune{}
lineWidth := float64(0.0)
p.textLines = []string{}
runes := []rune(p.text)
glyphs := []string{}
widths := []float64{}
for _, val := range runes {
glyph, found := p.encoder.RuneToGlyph(val)
if !found {
common.Log.Debug("error! Glyph not found for rune: %v\n", val)
return errors.New("glyph not found for rune") // XXX/FIXME: return error.
}
// Newline wrapping.
if glyph == "controlLF" {
// Moves to next line.
p.textLines = append(p.textLines, string(line))
line = []rune{}
lineWidth = 0
widths = []float64{}
glyphs = []string{}
continue
}
metrics, found := p.textFont.GetGlyphCharMetrics(glyph)
if !found {
common.Log.Debug("Glyph char metrics not found! %s\n", glyph)
return errors.New("glyph char metrics missing") // XXX/FIXME: return error.
}
w := p.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 i := len(glyphs) - 1; i >= 0; i-- {
if glyphs[i] == "space" {
idx = i
break
}
}
if idx > 0 {
p.textLines = append(p.textLines, string(line[0:idx+1]))
line = line[idx+1:]
line = append(line, val)
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 {
p.textLines = append(p.textLines, string(line))
line = []rune{val}
lineWidth = w
widths = []float64{w}
glyphs = []string{glyph}
}
} else {
line = append(line, val)
lineWidth += w
glyphs = append(glyphs, glyph)
widths = append(widths, w)
}
}
if len(line) > 0 {
p.textLines = append(p.textLines, string(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 *Paragraph) 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 := 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() {
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 drawParagraphOnBlock(blk *Block, p *Paragraph, ctx DrawContext) (DrawContext, error) {
// Find a free name for the font.
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 to the Page resources.
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()
yPos := ctx.PageHeight - ctx.Y - p.fontSize*p.lineHeight
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).
w := float64(0)
spaces := 0
for _, runeVal := range runes {
glyph, found := p.encoder.RuneToGlyph(runeVal)
if !found {
common.Log.Debug("Rune 0x%x not supported by text encoder", runeVal)
return ctx, errors.New("unsupported rune in text encoding")
}
if glyph == "space" {
spaces++
continue
}
if glyph == "controlLF" {
continue
}
metrics, found := p.textFont.GetGlyphCharMetrics(glyph)
if !found {
common.Log.Debug("Unsupported glyph %s in font\n", glyph)
return ctx, errors.New("unsupported text glyph")
}
w += p.fontSize * metrics.Wx
}
objs := []core.PdfObject{}
spaceMetrics, found := p.textFont.GetGlyphCharMetrics("space")
if !found {
return ctx, errors.New("the font does not have a space glyph")
}
spaceWidth := spaceMetrics.Wx
switch p.alignment {
case TextAlignmentJustify:
if spaces > 0 && idx < len(p.textLines)-1 { // Not to justify last line.
spaceWidth = (p.wrapWidth*1000.0 - w) / float64(spaces) / p.fontSize
}
case TextAlignmentCenter:
// 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))
case TextAlignmentRight:
textWidth := w + float64(spaces)*spaceWidth*p.fontSize
shift := (p.wrapWidth*1000.0 - textWidth) / p.fontSize
objs = append(objs, core.MakeFloat(-shift))
}
encStr := ""
for _, runeVal := range runes {
//creator.Add_Tj(core.PdfObjectString(tb.Encoder.Encode(line)))
glyph, found := p.encoder.RuneToGlyph(runeVal)
if !found {
common.Log.Debug("Rune 0x%x not supported by text encoder", runeVal)
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 {
objs = append(objs, core.MakeString(encStr))
encStr = ""
}
objs = append(objs, core.MakeFloat(-spaceWidth))
} else {
encStr += string(p.encoder.Encode(string(runeVal)))
}
}
if len(encStr) > 0 {
objs = append(objs, core.MakeString(encStr))
}
cc.Add_TJ(objs...)
}
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,88 @@
package creator
import (
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream/draw"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// Rectangle defines a rectangle with upper left corner at (x,y) and a specified width and height. The rectangle
// can have a colored fill and/or border with a specified width.
// Implements the Drawable interface and can be drawn on PDF using the Creator.
type Rectangle struct {
x float64 // Upper left corner
y float64
width float64
height float64
fillColor *model.PdfColorDeviceRGB
borderColor *model.PdfColorDeviceRGB
borderWidth float64
}
// NewRectangle creates a new Rectangle with default parameters with left corner at (x,y) and width, height as specified.
func NewRectangle(x, y, width, height float64) *Rectangle {
rect := &Rectangle{}
rect.x = x
rect.y = y
rect.width = width
rect.height = height
rect.borderColor = model.NewPdfColorDeviceRGB(0, 0, 0)
rect.borderWidth = 1.0
return rect
}
// GetCoords returns coordinates of the Rectangle's upper left corner (x,y).
func (rect *Rectangle) GetCoords() (float64, float64) {
return rect.x, rect.y
}
// SetBorderWidth sets the border width.
func (rect *Rectangle) SetBorderWidth(bw float64) {
rect.borderWidth = bw
}
// SetBorderColor sets border color.
func (rect *Rectangle) SetBorderColor(col Color) {
rect.borderColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// SetFillColor sets the fill color.
func (rect *Rectangle) SetFillColor(col Color) {
rect.fillColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// GeneratePageBlocks draws the rectangle on a new block representing the page. Implements the Drawable interface.
func (rect *Rectangle) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
block := NewBlock(ctx.PageWidth, ctx.PageHeight)
drawrect := draw.Rectangle{
Opacity: 1.0,
X: rect.x,
Y: ctx.PageHeight - rect.y - rect.height,
Height: rect.height,
Width: rect.width,
}
if rect.fillColor != nil {
drawrect.FillEnabled = true
drawrect.FillColor = rect.fillColor
}
if rect.borderColor != nil && rect.borderWidth > 0 {
drawrect.BorderEnabled = true
drawrect.BorderColor = rect.borderColor
drawrect.BorderWidth = rect.borderWidth
}
contents, _, err := drawrect.Draw("")
if err != nil {
return nil, ctx, err
}
err = block.addContentsByString(string(contents))
if err != nil {
return nil, ctx, err
}
return []*Block{block}, ctx, nil
}

View File

@@ -0,0 +1,638 @@
package creator
import (
"errors"
"fmt"
"strings"
"unicode"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/contentstream"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/core"
"gitea.tecamino.com/paadi/pdfmerge/internal/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{
{
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 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{}
switch p.alignment {
case TextAlignmentJustify:
// Not to justify last line.
if spaces > 0 && !isLastLine {
spaceWidth = (p.wrapWidth*1000.0 - width) / float64(spaces) / defaultFontSize
}
case TextAlignmentCenter:
// Start with a shift.
shift := (p.wrapWidth*1000.0 - width - spaceWidth) / 2 / defaultFontSize
objs = append(objs, core.MakeFloat(-shift))
case 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,179 @@
package creator
import (
"fmt"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/fonts"
)
// Subchapter simply represents a sub chapter pertaining to a specific Chapter. It can contain multiple
// Drawables, just like a chapter.
type Subchapter struct {
chapterNum int
subchapterNum int
title string
heading *Paragraph
contents []Drawable
// Show chapter numbering
showNumbering bool
// Include in TOC.
includeInTOC bool
// Positioning: relative / absolute.
positioning positioning
// Margins to be applied around the block when drawing on Page.
margins margins
// Reference to the creator's TOC.
toc *TableOfContents
}
// NewSubchapter creates a new Subchapter under Chapter ch with specified title.
// All other parameters are set to their defaults.
func (c *Creator) NewSubchapter(ch *Chapter, title string) *Subchapter {
subchap := &Subchapter{}
ch.subchapters++
subchap.subchapterNum = ch.subchapters
subchap.chapterNum = ch.number
subchap.title = title
heading := fmt.Sprintf("%d.%d %s", subchap.chapterNum, subchap.subchapterNum, title)
p := NewParagraph(heading)
p.SetFontSize(14)
p.SetFont(fonts.NewFontHelvetica()) // bold?
subchap.showNumbering = true
subchap.includeInTOC = true
subchap.heading = p
subchap.contents = []Drawable{}
// Add subchapter to ch.
ch.Add(subchap)
// Keep a reference for toc.
subchap.toc = c.toc
return subchap
}
// SetShowNumbering sets a flag to indicate whether or not to show chapter numbers as part of title.
func (subchap *Subchapter) SetShowNumbering(show bool) {
if show {
heading := fmt.Sprintf("%d.%d. %s", subchap.chapterNum, subchap.subchapterNum, subchap.title)
subchap.heading.SetText(heading)
} else {
heading := subchap.title
subchap.heading.SetText(heading)
}
subchap.showNumbering = show
}
// SetIncludeInTOC sets a flag to indicate whether or not to include in the table of contents.
func (subchap *Subchapter) SetIncludeInTOC(includeInTOC bool) {
subchap.includeInTOC = includeInTOC
}
// GetHeading returns the Subchapter's heading Paragraph to address style (font type, size, etc).
func (subchap *Subchapter) GetHeading() *Paragraph {
return subchap.heading
}
// Set absolute coordinates.
/*
func (subchap *subchapter) SetPos(x, y float64) {
subchap.positioning = positionAbsolute
subchap.xPos = x
subchap.yPos = y
}
*/
// SetMargins sets the Subchapter's margins (left, right, top, bottom).
// These margins are typically not needed as the Creator's page margins are used preferably.
func (subchap *Subchapter) SetMargins(left, right, top, bottom float64) {
subchap.margins.left = left
subchap.margins.right = right
subchap.margins.top = top
subchap.margins.bottom = bottom
}
// GetMargins returns the Subchapter's margins: left, right, top, bottom.
func (subchap *Subchapter) GetMargins() (float64, float64, float64, float64) {
return subchap.margins.left, subchap.margins.right, subchap.margins.top, subchap.margins.bottom
}
// Add adds a new Drawable to the chapter.
// The currently supported Drawables are: *Paragraph, *Image, *Block, *Table.
func (subchap *Subchapter) Add(d Drawable) {
switch d.(type) {
case *Chapter, *Subchapter:
common.Log.Debug("error: Cannot add chapter or subchapter to a subchapter")
case *Paragraph, *Image, *Block, *Table, *PageBreak:
subchap.contents = append(subchap.contents, d)
default:
common.Log.Debug("Unsupported: %T", d)
}
}
// GeneratePageBlocks generates the page blocks. Multiple blocks are generated if the contents wrap over
// multiple pages. Implements the Drawable interface.
func (subchap *Subchapter) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
origCtx := ctx
if subchap.positioning.isRelative() {
// Update context.
ctx.X += subchap.margins.left
ctx.Y += subchap.margins.top
ctx.Width -= subchap.margins.left + subchap.margins.right
ctx.Height -= subchap.margins.top
}
blocks, ctx, err := subchap.heading.GeneratePageBlocks(ctx)
if err != nil {
return blocks, ctx, err
}
if len(blocks) > 1 {
ctx.Page++ // did not fit - moved to next Page.
}
if subchap.includeInTOC {
// Add to TOC.
subchap.toc.add(subchap.title, subchap.chapterNum, subchap.subchapterNum, ctx.Page)
}
for _, d := range subchap.contents {
newBlocks, c, err := d.GeneratePageBlocks(ctx)
if err != nil {
return blocks, ctx, err
}
if len(newBlocks) < 1 {
continue
}
// The first block is always appended to the last..
blocks[len(blocks)-1].mergeBlocks(newBlocks[0])
blocks = append(blocks, newBlocks[1:]...)
ctx = c
}
if subchap.positioning.isRelative() {
// Move back X to same start of line.
ctx.X = origCtx.X
}
if subchap.positioning.isAbsolute() {
// If absolute: return original context.
return blocks, origCtx, nil
}
return blocks, ctx, nil
}

View File

@@ -0,0 +1,628 @@
package creator
import (
"errors"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model"
)
// Table allows organizing content in an rows X columns matrix, which can spawn across multiple pages.
type Table struct {
// Number of rows and columns.
rows int
cols int
// Current cell. Current cell in the table.
// For 4x4 table, if in the 2nd row, 3rd column, then
// curCell = 4+3 = 7
curCell int
// Column width fractions: should add up to 1.
colWidths []float64
// Row heights.
rowHeights []float64
// Default row height.
defaultRowHeight float64
// Content cells.
cells []*TableCell
// Positioning: relative / absolute.
positioning positioning
// Absolute coordinates (when in absolute mode).
xPos, yPos float64
// Margins to be applied around the block when drawing on Page.
margins margins
}
// NewTable create a new Table with a specified number of columns.
func NewTable(cols int) *Table {
t := &Table{}
t.rows = 0
t.cols = cols
t.curCell = 0
// Initialize column widths as all equal.
t.colWidths = []float64{}
colWidth := float64(1.0) / float64(cols)
for i := 0; i < cols; i++ {
t.colWidths = append(t.colWidths, colWidth)
}
t.rowHeights = []float64{}
// Default row height
// XXX/TODO: Base on contents instead?
t.defaultRowHeight = 10.0
t.cells = []*TableCell{}
return t
}
// SetColumnWidths sets the fractional column widths.
// Each width should be in the range 0-1 and is a fraction of the table width.
// The number of width inputs must match number of columns, otherwise an error is returned.
func (table *Table) SetColumnWidths(widths ...float64) error {
if len(widths) != table.cols {
common.Log.Debug("Mismatching number of widths and columns")
return errors.New("range check error")
}
table.colWidths = widths
return nil
}
// Height returns the total height of all rows.
func (table *Table) Height() float64 {
sum := float64(0.0)
for _, h := range table.rowHeights {
sum += h
}
return sum
}
// SetMargins sets the Table's left, right, top, bottom margins.
func (table *Table) SetMargins(left, right, top, bottom float64) {
table.margins.left = left
table.margins.right = right
table.margins.top = top
table.margins.bottom = bottom
}
// GetMargins returns the left, right, top, bottom Margins.
func (table *Table) GetMargins() (float64, float64, float64, float64) {
return table.margins.left, table.margins.right, table.margins.top, table.margins.bottom
}
// SetRowHeight sets the height for a specified row.
func (table *Table) SetRowHeight(row int, h float64) error {
if row < 1 || row > len(table.rowHeights) {
return errors.New("range check error")
}
table.rowHeights[row-1] = h
return nil
}
// CurRow returns the currently active cell's row number.
func (table *Table) CurRow() int {
curRow := (table.curCell-1)/table.cols + 1
return curRow
}
// CurCol returns the currently active cell's column number.
func (table *Table) CurCol() int {
curCol := (table.curCell-1)%(table.cols) + 1
return curCol
}
// SetPos sets the Table's positioning to absolute mode and specifies the upper-left corner coordinates as (x,y).
// Note that this is only sensible to use when the table does not wrap over multiple pages.
// TODO: Should be able to set width too (not just based on context/relative positioning mode).
func (table *Table) SetPos(x, y float64) {
table.positioning = positionAbsolute
table.xPos = x
table.yPos = y
}
// GeneratePageBlocks generate the page blocks. Multiple blocks are generated if the contents wrap over multiple pages.
// Implements the Drawable interface.
func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
blocks := []*Block{}
block := NewBlock(ctx.PageWidth, ctx.PageHeight)
origCtx := ctx
if table.positioning.isAbsolute() {
ctx.X = table.xPos
ctx.Y = table.yPos
} else {
// Relative mode: add margins.
ctx.X += table.margins.left
ctx.Y += table.margins.top
ctx.Width -= table.margins.left + table.margins.right
ctx.Height -= table.margins.bottom + table.margins.top
}
tableWidth := ctx.Width
// Store table's upper left corner.
ulX := ctx.X
ulY := ctx.Y
ctx.Height = ctx.PageHeight - ctx.Y - ctx.Margins.bottom
origHeight := ctx.Height
// Start row keeps track of starting row (wraps to 0 on new page).
startrow := 0
// Prepare for drawing: Calculate cell dimensions, row, cell heights.
for _, cell := range table.cells {
// Get total width fraction
wf := float64(0.0)
for i := 0; i < cell.colspan; i++ {
wf += table.colWidths[cell.col+i-1]
}
// Get x pos relative to table upper left corner.
xrel := float64(0.0)
for i := 0; i < cell.col-1; i++ {
xrel += table.colWidths[i] * tableWidth
}
// Get y pos relative to table upper left corner.
yrel := float64(0.0)
for i := startrow; i < cell.row-1; i++ {
yrel += table.rowHeights[i]
}
// Calculate the width out of available width.
w := wf * tableWidth
// Get total height.
h := float64(0.0)
for i := 0; i < cell.rowspan; i++ {
h += table.rowHeights[cell.row+i-1]
}
// For text: Calculate width, height, wrapping within available space if specified.
switch t := cell.content.(type) {
case *Paragraph:
p := t
if p.enableWrap {
p.SetWidth(w - cell.indent)
}
newh := p.Height() + p.margins.bottom + p.margins.bottom
newh += 0.5 * p.fontSize * p.lineHeight // 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 *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
if newh > h {
diffh := newh - h
// Add diff to last row.
table.rowHeights[cell.row+cell.rowspan-2] += diffh
}
case *Division:
div := t
ctx := DrawContext{
X: xrel,
Y: yrel,
Width: w,
}
// Mock call to generate page blocks.
divBlocks, updCtx, err := div.GeneratePageBlocks(ctx)
if err != nil {
return nil, ctx, err
}
if len(divBlocks) > 1 {
// Wraps across page, make cell reach all the way to bottom of current page.
newh := ctx.Height - h
if newh > h {
diffh := newh - h
// Add diff to last row.
table.rowHeights[cell.row+cell.rowspan-2] += diffh
}
}
newh := div.Height() + div.margins.top + div.margins.bottom
_ = updCtx
// Get available width and height.
if newh > h {
diffh := newh - h
// Add diff to last row.
table.rowHeights[cell.row+cell.rowspan-2] += diffh
}
}
}
// Draw cells.
// row height, cell height
for _, cell := range table.cells {
// Get total width fraction
wf := float64(0.0)
for i := 0; i < cell.colspan; i++ {
wf += table.colWidths[cell.col+i-1]
}
// Get x pos relative to table upper left corner.
xrel := float64(0.0)
for i := 0; i < cell.col-1; i++ {
xrel += table.colWidths[i] * tableWidth
}
// Get y pos relative to table upper left corner.
yrel := float64(0.0)
for i := startrow; i < cell.row-1; i++ {
yrel += table.rowHeights[i]
}
// Calculate the width out of available width.
w := wf * tableWidth
// Get total height.
h := float64(0.0)
for i := 0; i < cell.rowspan; i++ {
h += table.rowHeights[cell.row+i-1]
}
ctx.Height = origHeight - yrel
if h > ctx.Height {
// Go to next page.
blocks = append(blocks, block)
block = NewBlock(ctx.PageWidth, ctx.PageHeight)
ulX = ctx.Margins.left
ulY = ctx.Margins.top
ctx.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom
startrow = cell.row - 1
yrel = 0
}
// Height should be how much space there is left of the page.
ctx.Width = w
ctx.X = ulX + xrel
ctx.Y = ulY + yrel
if cell.backgroundColor != nil {
// Draw background (fill)
rect := NewRectangle(ctx.X, ctx.Y, w, h)
r := cell.backgroundColor.R()
g := cell.backgroundColor.G()
b := cell.backgroundColor.B()
rect.SetFillColor(ColorRGBFromArithmetic(r, g, b))
if cell.borderStyle != CellBorderStyleNone {
// and border.
rect.SetBorderWidth(cell.borderWidth)
r := cell.borderColor.R()
g := cell.borderColor.G()
b := cell.borderColor.B()
rect.SetBorderColor(ColorRGBFromArithmetic(r, g, b))
} else {
rect.SetBorderWidth(0)
}
err := block.Draw(rect)
if err != nil {
common.Log.Debug("error: %v\n", err)
}
} else if cell.borderStyle != CellBorderStyleNone {
// Draw border (no fill).
rect := NewRectangle(ctx.X, ctx.Y, w, h)
rect.SetBorderWidth(cell.borderWidth)
r := cell.borderColor.R()
g := cell.borderColor.G()
b := cell.borderColor.B()
rect.SetBorderColor(ColorRGBFromArithmetic(r, g, b))
err := block.Draw(rect)
if err != nil {
common.Log.Debug("error: %v\n", err)
}
}
if cell.content != nil {
// Account for horizontal alignment:
cw := cell.content.Width() // content width.
switch cell.horizontalAlignment {
case CellHorizontalAlignmentLeft:
// Account for indent.
ctx.X += cell.indent
ctx.Width -= cell.indent
case CellHorizontalAlignmentCenter:
// Difference between available space and content space.
dw := w - cw
if dw > 0 {
ctx.X += dw / 2
ctx.Width -= dw / 2
}
case CellHorizontalAlignmentRight:
if w > cw {
ctx.X = ctx.X + w - cw - cell.indent
ctx.Width = cw
}
}
// Account for vertical alignment.
ch := cell.content.Height() // content height.
switch cell.verticalAlignment {
case CellVerticalAlignmentTop:
// Default: do nothing.
case CellVerticalAlignmentMiddle:
dh := h - ch
if dh > 0 {
ctx.Y += dh / 2
ctx.Height -= dh / 2
}
case CellVerticalAlignmentBottom:
if h > ch {
ctx.Y = ctx.Y + h - ch
ctx.Height = ch
}
}
err := block.DrawWithContext(cell.content, ctx)
if err != nil {
common.Log.Debug("error: %v\n", err)
}
}
ctx.Y += h
}
blocks = append(blocks, block)
if table.positioning.isAbsolute() {
return blocks, origCtx, nil
}
// Relative mode.
// Move back X after.
ctx.X = origCtx.X
// Return original width.
ctx.Width = origCtx.Width
// Add the bottom margin.
ctx.Y += table.margins.bottom
return blocks, ctx, nil
}
// CellBorderStyle defines the table cell's border style.
type CellBorderStyle int
// Currently supported table styles are: None (no border) and boxed (line along each side).
const (
// No border
CellBorderStyleNone CellBorderStyle = iota
// Borders along all sides (boxed).
CellBorderStyleBox
)
// CellHorizontalAlignment defines the table cell's horizontal alignment.
type CellHorizontalAlignment int
// Table cells have three horizontal alignment modes: left, center and right.
const (
// Align cell content on the left (with specified indent); unused space on the right.
CellHorizontalAlignmentLeft CellHorizontalAlignment = iota
// Align cell content in the middle (unused space divided equally on the left/right).
CellHorizontalAlignmentCenter
// Align the cell content on the right; unsued space on the left.
CellHorizontalAlignmentRight
)
// CellVerticalAlignment defines the table cell's vertical alignment.
type CellVerticalAlignment int
// Table cells have three vertical alignment modes: top, middle and bottom.
const (
// Align cell content vertically to the top; unused space below.
CellVerticalAlignmentTop CellVerticalAlignment = iota
// Align cell content in the middle; unused space divided equally above and below.
CellVerticalAlignmentMiddle
// Align cell content on the bottom; unused space above.
CellVerticalAlignmentBottom
)
// TableCell defines a table cell which can contain a Drawable as content.
type TableCell struct {
// Background
backgroundColor *model.PdfColorDeviceRGB
// Border
borderStyle CellBorderStyle
borderColor *model.PdfColorDeviceRGB
borderWidth float64
// The row and column which the cell starts from.
row, col int
// Row, column span.
rowspan int
colspan int
// Each cell can contain 1 drawable.
content VectorDrawable
// Alignment
horizontalAlignment CellHorizontalAlignment
verticalAlignment CellVerticalAlignment
// Left indent.
indent float64
// Table reference
table *Table
}
// NewCell makes a new cell and inserts into the table at current position in the table.
func (table *Table) NewCell() *TableCell {
table.curCell++
curRow := (table.curCell-1)/table.cols + 1
for curRow > table.rows {
table.rows++
table.rowHeights = append(table.rowHeights, table.defaultRowHeight)
}
curCol := (table.curCell-1)%(table.cols) + 1
cell := &TableCell{}
cell.row = curRow
cell.col = curCol
// Default left indent
cell.indent = 5
cell.borderStyle = CellBorderStyleNone
cell.borderColor = model.NewPdfColorDeviceRGB(0, 0, 0)
// Alignment defaults.
cell.horizontalAlignment = CellHorizontalAlignmentLeft
cell.verticalAlignment = CellVerticalAlignmentTop
cell.rowspan = 1
cell.colspan = 1
table.cells = append(table.cells, cell)
// Keep reference to the table.
cell.table = table
return cell
}
// SkipCells skips over a specified number of cells in the table.
func (table *Table) SkipCells(num int) {
if num < 0 {
common.Log.Debug("Table: cannot skip back to previous cells")
return
}
table.curCell += num
}
// SkipRows skips over a specified number of rows in the table.
func (table *Table) SkipRows(num int) {
ncells := num*table.cols - 1
if ncells < 0 {
common.Log.Debug("Table: cannot skip back to previous cells")
return
}
table.curCell += ncells
}
// SkipOver skips over a specified number of rows and cols.
func (table *Table) SkipOver(rows, cols int) {
ncells := rows*table.cols + cols - 1
if ncells < 0 {
common.Log.Debug("Table: cannot skip back to previous cells")
return
}
table.curCell += ncells
}
// SetIndent sets the cell's left indent.
func (cell *TableCell) SetIndent(indent float64) {
cell.indent = indent
}
// SetHorizontalAlignment sets the cell's horizontal alignment of content.
// Can be one of:
// - CellHorizontalAlignmentLeft
// - CellHorizontalAlignmentCenter
// - CellHorizontalAlignmentRight
func (cell *TableCell) SetHorizontalAlignment(halign CellHorizontalAlignment) {
cell.horizontalAlignment = halign
}
// SetVerticalAlignment set the cell's vertical alignment of content.
// Can be one of:
// - CellHorizontalAlignmentTop
// - CellHorizontalAlignmentMiddle
// - CellHorizontalAlignmentBottom
func (cell *TableCell) SetVerticalAlignment(valign CellVerticalAlignment) {
cell.verticalAlignment = valign
}
// SetBorder sets the cell's border style.
func (cell *TableCell) SetBorder(style CellBorderStyle, width float64) {
cell.borderStyle = style
cell.borderWidth = width
}
// SetBorderColor sets the cell's border color.
func (cell *TableCell) SetBorderColor(col Color) {
cell.borderColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// SetBackgroundColor sets the cell's background color.
func (cell *TableCell) SetBackgroundColor(col Color) {
cell.backgroundColor = model.NewPdfColorDeviceRGB(col.ToRGB())
}
// Width returns the cell's width based on the input draw context.
func (cell *TableCell) Width(ctx DrawContext) float64 {
fraction := float64(0.0)
for j := 0; j < cell.colspan; j++ {
fraction += cell.table.colWidths[cell.col+j-1]
}
w := ctx.Width * fraction
return w
}
// 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, *StyledParagraph.
func (cell *TableCell) SetContent(vd VectorDrawable) error {
switch t := vd.(type) {
case *Paragraph:
if t.defaultWrap {
// Default paragraph settings in table: no wrapping.
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
case *Division:
cell.content = vd
default:
common.Log.Debug("error: unsupported cell content type %T\n", vd)
return errors.New("type check error")
}
return nil
}

View File

@@ -0,0 +1,38 @@
package creator
import (
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/fonts"
"gitea.tecamino.com/paadi/pdfmerge/internal/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
}

View File

@@ -0,0 +1,38 @@
package creator
// TableOfContents provides an overview over chapters and subchapters when creating a document with Creator.
type TableOfContents struct {
entries []TableOfContentsEntry
}
// Make a new table of contents.
func newTableOfContents() *TableOfContents {
toc := TableOfContents{}
toc.entries = []TableOfContentsEntry{}
return &toc
}
// Entries returns the table of content entries.
func (toc *TableOfContents) Entries() []TableOfContentsEntry {
return toc.entries
}
// Add a TOC entry.
func (toc *TableOfContents) add(title string, chapter, subchapter, pageNum int) {
entry := TableOfContentsEntry{}
entry.Title = title
entry.Chapter = chapter
entry.Subchapter = subchapter
entry.PageNumber = pageNum
toc.entries = append(toc.entries, entry)
}
// TableOfContentsEntry defines a single entry in the TableOfContents.
// Each entry has a title, chapter number, sub chapter (0 if chapter) and the page number.
type TableOfContentsEntry struct {
Title string
Chapter int
Subchapter int // 0 if chapter
PageNumber int // Page number
}