fix wrong git ignore
This commit is contained in:
543
internal/pdf/creator/block.go
Normal file
543
internal/pdf/creator/block.go
Normal 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
|
||||
}
|
||||
174
internal/pdf/creator/chapters.go
Normal file
174
internal/pdf/creator/chapters.go
Normal 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
|
||||
}
|
||||
115
internal/pdf/creator/color.go
Normal file
115
internal/pdf/creator/color.go
Normal 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
|
||||
}
|
||||
49
internal/pdf/creator/const.go
Normal file
49
internal/pdf/creator/const.go
Normal 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
|
||||
}
|
||||
524
internal/pdf/creator/creator.go
Normal file
524
internal/pdf/creator/creator.go
Normal 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
|
||||
}
|
||||
66
internal/pdf/creator/curve.go
Normal file
66
internal/pdf/creator/curve.go
Normal 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
|
||||
}
|
||||
189
internal/pdf/creator/division.go
Normal file
189
internal/pdf/creator/division.go
Normal 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
|
||||
}
|
||||
44
internal/pdf/creator/drawable.go
Normal file
44
internal/pdf/creator/drawable.go
Normal 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
|
||||
}
|
||||
89
internal/pdf/creator/ellipse.go
Normal file
89
internal/pdf/creator/ellipse.go
Normal 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
|
||||
}
|
||||
107
internal/pdf/creator/filled_curve.go
Normal file
107
internal/pdf/creator/filled_curve.go
Normal 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
|
||||
}
|
||||
329
internal/pdf/creator/image.go
Normal file
329
internal/pdf/creator/image.go
Normal 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
|
||||
}
|
||||
85
internal/pdf/creator/line.go
Normal file
85
internal/pdf/creator/line.go
Normal 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
|
||||
}
|
||||
31
internal/pdf/creator/pagebreak.go
Normal file
31
internal/pdf/creator/pagebreak.go
Normal 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
|
||||
}
|
||||
525
internal/pdf/creator/paragraph.go
Normal file
525
internal/pdf/creator/paragraph.go
Normal 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
|
||||
}
|
||||
88
internal/pdf/creator/rectangle.go
Normal file
88
internal/pdf/creator/rectangle.go
Normal 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
|
||||
}
|
||||
638
internal/pdf/creator/styled_paragraph.go
Normal file
638
internal/pdf/creator/styled_paragraph.go
Normal 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
|
||||
}
|
||||
179
internal/pdf/creator/subchapter.go
Normal file
179
internal/pdf/creator/subchapter.go
Normal 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
|
||||
}
|
||||
628
internal/pdf/creator/table.go
Normal file
628
internal/pdf/creator/table.go
Normal 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
|
||||
}
|
||||
38
internal/pdf/creator/text_style.go
Normal file
38
internal/pdf/creator/text_style.go
Normal 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
|
||||
}
|
||||
38
internal/pdf/creator/toc.go
Normal file
38
internal/pdf/creator/toc.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user