Merge pull request #360 from adrg/creator-chapter-hierarchy

Creator chapter hierarchy
This commit is contained in:
Gunnsteinn Hall 2019-03-06 23:25:11 +00:00 committed by GitHub
commit 0dc0219e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 309 deletions

View File

@ -17,14 +17,21 @@ import (
// 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
// The number of the chapter.
number int
// The title of the chapter.
title string
// The heading paragraph of the chapter.
heading *Paragraph
subchapters int
// The content components of the chapter.
contents []Drawable
// The number of subchapters the chapter has.
subchapters int
// Show chapter numbering
showNumbering bool
@ -40,6 +47,9 @@ type Chapter struct {
// Margins to be applied around the block when drawing on Page.
margins margins
// Reference to the parent chapter the current chapter belongs to.
parent *Chapter
// Reference to the TOC of the creator.
toc *TOC
@ -48,36 +58,53 @@ type Chapter struct {
// The item of the chapter in the outline.
outlineItem *model.OutlineItem
// The level of the chapter in the chapters hierarchy.
level uint
}
// newChapter creates a new chapter with the specified title as the heading.
func newChapter(toc *TOC, outline *model.Outline, title string, number int, style TextStyle) *Chapter {
p := newParagraph(fmt.Sprintf("%d. %s", number, title), style)
p.SetFont(style.Font)
p.SetFontSize(16)
func newChapter(parent *Chapter, toc *TOC, outline *model.Outline, title string, number int, style TextStyle) *Chapter {
var level uint = 1
if parent != nil {
level = parent.level + 1
}
return &Chapter{
chapter := &Chapter{
number: number,
title: title,
showNumbering: true,
includeInTOC: true,
parent: parent,
toc: toc,
outline: outline,
heading: p,
contents: []Drawable{},
level: level,
}
p := newParagraph(chapter.headingText(), style)
p.SetFont(style.Font)
p.SetFontSize(style.FontSize)
chapter.heading = p
return chapter
}
// NewSubchapter creates a new child chapter with the specified title.
func (chap *Chapter) NewSubchapter(title string) *Chapter {
style := newTextStyle(chap.heading.textFont)
style.FontSize = 14
chap.subchapters++
subchapter := newChapter(chap, chap.toc, chap.outline, title, chap.subchapters, style)
chap.Add(subchapter)
return subchapter
}
// 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 := fmt.Sprintf("%s", chap.title)
chap.heading.SetText(heading)
}
chap.showNumbering = show
chap.heading.SetText(chap.headingText())
}
// SetIncludeInTOC sets a flag to indicate whether or not to include in tOC.
@ -112,10 +139,7 @@ func (chap *Chapter) Add(d Drawable) 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:
case *Paragraph, *Image, *Block, *Table, *PageBreak, *Chapter:
chap.contents = append(chap.contents, d)
default:
common.Log.Debug("Unsupported: %T", d)
@ -125,6 +149,36 @@ func (chap *Chapter) Add(d Drawable) error {
return nil
}
// headingNumber returns the chapter heading number based on the chapter
// hierarchy and the showNumbering property.
func (chap *Chapter) headingNumber() string {
var chapNumber string
if chap.showNumbering {
if chap.number != 0 {
chapNumber = strconv.Itoa(chap.number) + "."
}
if chap.parent != nil {
parentChapNumber := chap.parent.headingNumber()
if parentChapNumber != "" {
chapNumber = parentChapNumber + chapNumber
}
}
}
return chapNumber
}
// headingText returns the chapter heading text content.
func (chap *Chapter) headingText() string {
heading := chap.title
if chapNumber := chap.headingNumber(); chapNumber != "" {
heading = fmt.Sprintf("%s %s", chapNumber, heading)
}
return heading
}
// 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) {
@ -138,37 +192,25 @@ func (chap *Chapter) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
ctx.Height -= chap.margins.top
}
origX := ctx.X
origY := ctx.Y
blocks, ctx, err := chap.heading.GeneratePageBlocks(ctx)
blocks, c, 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.
}
ctx = c
// Generate chapter title and number.
chapTitle := chap.title
var chapNumber string
posX := ctx.X
posY := ctx.Y - chap.heading.Height()
page := int64(ctx.Page)
if chap.showNumbering {
if chap.number != 0 {
chapNumber = strconv.Itoa(chap.number) + "."
}
}
if chapNumber != "" {
chapTitle = fmt.Sprintf("%s %s", chapNumber, chapTitle)
}
chapNumber := chap.headingNumber()
chapTitle := chap.headingText()
// Add to TOC.
if chap.includeInTOC {
line := chap.toc.Add(chapNumber, chap.title, strconv.FormatInt(page, 10), 1)
line := chap.toc.Add(chapNumber, chap.title, strconv.FormatInt(page, 10), chap.level)
if chap.toc.showLinks {
line.SetLink(page, origX, origY)
line.SetLink(page, posX, posY)
}
}
@ -176,14 +218,19 @@ func (chap *Chapter) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
if chap.outlineItem == nil {
chap.outlineItem = model.NewOutlineItem(
chapTitle,
model.NewOutlineDest(page-1, origX, origY),
model.NewOutlineDest(page-1, posX, posY),
)
chap.outline.Add(chap.outlineItem)
if chap.parent != nil {
chap.parent.outlineItem.Add(chap.outlineItem)
} else {
chap.outline.Add(chap.outlineItem)
}
} else {
outlineDest := &chap.outlineItem.Dest
outlineDest.Page = page - 1
outlineDest.X = origX
outlineDest.Y = origY
outlineDest.X = posX
outlineDest.Y = posY
}
for _, d := range chap.contents {

View File

@ -714,13 +714,10 @@ func (c *Creator) NewStyledTOCLine(number, title, page TextChunk, level uint, st
// NewChapter creates a new chapter with the specified title as the heading.
func (c *Creator) NewChapter(title string) *Chapter {
c.chapters++
return newChapter(c.toc, c.outline, title, c.chapters, c.NewTextStyle())
}
style := c.NewTextStyle()
style.FontSize = 16
// 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 {
return newSubchapter(ch, title, c.NewTextStyle())
return newChapter(nil, c.toc, c.outline, title, c.chapters, style)
}
// NewInvoice returns an instance of an empty invoice.

View File

@ -793,7 +793,7 @@ func TestSubchaptersSimple(t *testing.T) {
// Add chapters.
ch1 := c.NewChapter("Introduction")
subchap1 := c.NewSubchapter(ch1, "The fundamentals of the mastery of the most genious experiment of all times in modern world history. The story of the maker and the maker bot and the genius cow.")
subchap1 := ch1.NewSubchapter("The fundamentals of the mastery of the most genious experiment of all times in modern world history. The story of the maker and the maker bot and the genius cow.")
subchap1.SetMargins(0, 0, 5, 0)
//subCh1 := NewSubchapter(ch1, "Workflow")
@ -809,19 +809,19 @@ func TestSubchaptersSimple(t *testing.T) {
subchap1.Add(p)
}
subchap2 := c.NewSubchapter(ch1, "Mechanism")
subchap2 := ch1.NewSubchapter("Mechanism")
subchap2.SetMargins(0, 0, 5, 0)
for j := 0; j < 1; j++ {
subchap2.Add(p)
}
subchap3 := c.NewSubchapter(ch1, "Discussion")
subchap3 := ch1.NewSubchapter("Discussion")
subchap3.SetMargins(0, 0, 5, 0)
for j := 0; j < 1; j++ {
subchap3.Add(p)
}
subchap4 := c.NewSubchapter(ch1, "Conclusion")
subchap4 := ch1.NewSubchapter("Conclusion")
subchap4.SetMargins(0, 0, 5, 0)
for j := 0; j < 1; j++ {
subchap4.Add(p)
@ -895,11 +895,9 @@ func TestSubchapters(t *testing.T) {
// Add chapters.
ch1 := c.NewChapter("Introduction")
subchap1 := c.NewSubchapter(ch1, "The fundamentals")
subchap1 := ch1.NewSubchapter("The fundamentals")
subchap1.SetMargins(0, 0, 5, 0)
//subCh1 := NewSubchapter(ch1, "Workflow")
p := c.NewParagraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
"aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore " +
@ -911,19 +909,28 @@ func TestSubchapters(t *testing.T) {
subchap1.Add(p)
}
subchap2 := c.NewSubchapter(ch1, "Mechanism")
subchap2 := ch1.NewSubchapter("Mechanism")
subchap2.SetMargins(0, 0, 5, 0)
for j := 0; j < 15; j++ {
subchap2.Add(p)
}
subchap3 := c.NewSubchapter(ch1, "Discussion")
subchap3 := ch1.NewSubchapter("Discussion")
subchap3.SetMargins(0, 0, 5, 0)
for j := 0; j < 19; j++ {
subchap3.Add(p)
}
subchap4 := c.NewSubchapter(ch1, "Conclusion")
// Create multi-level subchapters.
subchap := subchap3
for i := 0; i < 5; i++ {
subchap = subchap.NewSubchapter(fmt.Sprintf("Discussion %d", i+1))
for j := 0; j < 5; j++ {
subchap.Add(p)
}
}
subchap4 := ch1.NewSubchapter("Conclusion")
subchap4.SetMargins(0, 0, 5, 0)
for j := 0; j < 23; j++ {
subchap4.Add(p)
@ -1808,7 +1815,7 @@ func TestTableInSubchapter(t *testing.T) {
ch.GetHeading().SetFontSize(18)
ch.GetHeading().SetColor(ColorRGBFrom8bit(72, 86, 95))
sc := c.NewSubchapter(ch, "Issuer details")
sc := ch.NewSubchapter("Issuer details")
sc.SetMargins(0, 0, 5, 0)
sc.GetHeading().SetFont(fontRegular)
sc.GetHeading().SetFontSize(18)
@ -1862,7 +1869,7 @@ func TestTableInSubchapter(t *testing.T) {
ch.Add(issuerTable)
sc = c.NewSubchapter(ch, "My Statement")
sc = ch.NewSubchapter("My Statement")
//sc.SetMargins(0, 0, 5, 0)
sc.GetHeading().SetFont(fontRegular)
sc.GetHeading().SetFontSize(18)
@ -2381,7 +2388,7 @@ func TestCombineDuplicateDirectObjects(t *testing.T) {
c.AddTOC = true
ch1 := c.NewChapter("Introduction")
subchap1 := c.NewSubchapter(ch1, "The fundamentals")
subchap1 := ch1.NewSubchapter("The fundamentals")
subchap1.SetMargins(0, 0, 5, 0)
//subCh1 := NewSubchapter(ch1, "Workflow")
@ -2398,19 +2405,19 @@ func TestCombineDuplicateDirectObjects(t *testing.T) {
subchap1.Add(p)
}
subchap2 := c.NewSubchapter(ch1, "Mechanism")
subchap2 := ch1.NewSubchapter("Mechanism")
subchap2.SetMargins(0, 0, 5, 0)
for j := 0; j < 3; j++ {
subchap2.Add(p)
}
subchap3 := c.NewSubchapter(ch1, "Discussion")
subchap3 := ch1.NewSubchapter("Discussion")
subchap3.SetMargins(0, 0, 5, 0)
for j := 0; j < 4; j++ {
subchap3.Add(p)
}
subchap4 := c.NewSubchapter(ch1, "Conclusion")
subchap4 := ch1.NewSubchapter("Conclusion")
subchap4.SetMargins(0, 0, 5, 0)
for j := 0; j < 3; j++ {
subchap4.Add(p)
@ -2588,7 +2595,7 @@ func TestCombineIdenticalIndirectObjects(t *testing.T) {
c.AddTOC = true
ch1 := c.NewChapter("Introduction")
subchap1 := c.NewSubchapter(ch1, "The fundamentals")
subchap1 := ch1.NewSubchapter("The fundamentals")
subchap1.SetMargins(0, 0, 5, 0)
//subCh1 := NewSubchapter(ch1, "Workflow")
@ -2604,19 +2611,19 @@ func TestCombineIdenticalIndirectObjects(t *testing.T) {
subchap1.Add(p)
}
subchap2 := c.NewSubchapter(ch1, "Mechanism")
subchap2 := ch1.NewSubchapter("Mechanism")
subchap2.SetMargins(0, 0, 5, 0)
for j := 0; j < 15; j++ {
subchap2.Add(p)
}
subchap3 := c.NewSubchapter(ch1, "Discussion")
subchap3 := ch1.NewSubchapter("Discussion")
subchap3.SetMargins(0, 0, 5, 0)
for j := 0; j < 19; j++ {
subchap3.Add(p)
}
subchap4 := c.NewSubchapter(ch1, "Conclusion")
subchap4 := ch1.NewSubchapter("Conclusion")
subchap4.SetMargins(0, 0, 5, 0)
for j := 0; j < 23; j++ {
subchap4.Add(p)
@ -2781,7 +2788,7 @@ func TestAllOptimizations(t *testing.T) {
c.AddTOC = true
ch1 := c.NewChapter("Introduction")
subchap1 := c.NewSubchapter(ch1, "The fundamentals")
subchap1 := ch1.NewSubchapter("The fundamentals")
subchap1.SetMargins(0, 0, 5, 0)
//subCh1 := NewSubchapter(ch1, "Workflow")
@ -2797,19 +2804,19 @@ func TestAllOptimizations(t *testing.T) {
subchap1.Add(p)
}
subchap2 := c.NewSubchapter(ch1, "Mechanism")
subchap2 := ch1.NewSubchapter("Mechanism")
subchap2.SetMargins(0, 0, 5, 0)
for j := 0; j < 15; j++ {
subchap2.Add(p)
}
subchap3 := c.NewSubchapter(ch1, "Discussion")
subchap3 := ch1.NewSubchapter("Discussion")
subchap3.SetMargins(0, 0, 5, 0)
for j := 0; j < 19; j++ {
subchap3.Add(p)
}
subchap4 := c.NewSubchapter(ch1, "Conclusion")
subchap4 := ch1.NewSubchapter("Conclusion")
subchap4.SetMargins(0, 0, 5, 0)
for j := 0; j < 23; j++ {
subchap4.Add(p)

View File

@ -1,233 +0,0 @@
/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/
package creator
import (
"fmt"
"strconv"
"github.com/unidoc/unidoc/common"
"github.com/unidoc/unidoc/pdf/model"
)
// Subchapter simply represents a sub chapter pertaining to a specific Chapter. It can contain
// multiple Drawables, just like a chapter.
type Subchapter struct {
subchapterNum int
title string
heading *Paragraph
contents []Drawable
// Show chapter numbering
showNumbering bool
// Include in TOC.
includeInTOC bool
// 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
// Reference to the creator's TOC.
toc *TOC
// Reference to the chapter the subchapter belongs to.
chapter *Chapter
// Reference to the outline of the creator.
outline *model.Outline
// The item of the subchapter in the outline.
outlineItem *model.OutlineItem
}
// newSubchapter creates a new Subchapter under Chapter ch with specified title.
// All other parameters are set to their defaults.
func newSubchapter(ch *Chapter, title string, style TextStyle) *Subchapter {
ch.subchapters++
p := newParagraph(fmt.Sprintf("%d.%d %s", ch.number, ch.subchapters, title), style)
p.SetFont(style.Font) // bold?
p.SetFontSize(14)
subchapter := &Subchapter{
subchapterNum: ch.subchapters,
title: title,
showNumbering: true,
includeInTOC: true,
heading: p,
contents: []Drawable{},
chapter: ch,
toc: ch.toc,
outline: ch.outline,
}
// Add subchapter to chapter.
ch.Add(subchapter)
return subchapter
}
// 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.chapter.number, subchap.subchapterNum, subchap.title)
subchap.heading.SetText(heading)
} else {
heading := fmt.Sprintf("%s", 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
}
origX := ctx.X
origY := ctx.Y
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.
}
// Generate subchapter title and number.
subchapTitle := subchap.title
var subchapNumber string
page := int64(ctx.Page)
if subchap.showNumbering {
if subchap.chapter.number != 0 {
subchapNumber = strconv.Itoa(subchap.chapter.number)
}
if subchap.subchapterNum != 0 {
if subchapNumber != "" {
subchapNumber += "."
}
subchapNumber += strconv.Itoa(subchap.subchapterNum) + "."
}
}
if subchapNumber != "" {
subchapTitle = fmt.Sprintf("%s %s", subchapNumber, subchapTitle)
}
// Add to TOC.
if subchap.includeInTOC {
line := subchap.toc.Add(subchapNumber, subchap.title, strconv.FormatInt(page, 10), 2)
if subchap.toc.showLinks {
line.SetLink(page, origX, origY)
}
}
// Add to outline.
if subchap.outlineItem == nil {
subchap.outlineItem = model.NewOutlineItem(
subchapTitle,
model.NewOutlineDest(page-1, origX, origY),
)
subchap.chapter.outlineItem.Add(subchap.outlineItem)
} else {
outlineDest := &subchap.outlineItem.Dest
outlineDest.Page = page - 1
outlineDest.X = origX
outlineDest.Y = origY
}
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
}