diff --git a/pdf/creator/chapters.go b/pdf/creator/chapters.go index bff39f00..383d1091 100644 --- a/pdf/creator/chapters.go +++ b/pdf/creator/chapters.go @@ -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 { diff --git a/pdf/creator/creator.go b/pdf/creator/creator.go index 6d0c55e5..a0a0e872 100644 --- a/pdf/creator/creator.go +++ b/pdf/creator/creator.go @@ -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. diff --git a/pdf/creator/creator_test.go b/pdf/creator/creator_test.go index d122581d..f363e092 100644 --- a/pdf/creator/creator_test.go +++ b/pdf/creator/creator_test.go @@ -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) diff --git a/pdf/creator/subchapter.go b/pdf/creator/subchapter.go deleted file mode 100644 index aaadfa93..00000000 --- a/pdf/creator/subchapter.go +++ /dev/null @@ -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 -}