Add table headers support (#390)

* Add table headers support with a testcase.
This commit is contained in:
Adrian-George Bostan 2019-03-30 11:56:37 +02:00 committed by Gunnsteinn Hall
parent 95b4e1cfc0
commit 54264e343e
2 changed files with 183 additions and 19 deletions

View File

@ -45,31 +45,31 @@ type Table struct {
// Margins to be applied around the block when drawing on Page.
margins margins
// Specifies whether the table has a header.
hasHeader bool
// Header rows.
headerStartRow int
headerEndRow int
}
// 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
t := &Table{
cols: cols,
defaultRowHeight: 10.0,
colWidths: []float64{},
rowHeights: []float64{},
cells: []*TableCell{},
}
// 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
// TODO: Base on contents instead?
t.defaultRowHeight = 10.0
t.cells = []*TableCell{}
return t
}
@ -83,7 +83,6 @@ func (table *Table) SetColumnWidths(widths ...float64) error {
}
table.colWidths = widths
return nil
}
@ -148,6 +147,25 @@ func (table *Table) SetPos(x, y float64) {
table.yPos = y
}
// SetHeaderRows turns the selected table rows into headers that are repeated
// for every page the table spans. startRow and endRow are inclusive.
func (table *Table) SetHeaderRows(startRow, endRow int) error {
if startRow <= 0 {
return errors.New("header start row must be greater than 0")
}
if endRow <= 0 {
return errors.New("header end row must be greater than 0")
}
if startRow > endRow {
return errors.New("header start row must be less than or equal to the end row")
}
table.hasHeader = true
table.headerStartRow = startRow
table.headerEndRow = endRow
return nil
}
// GeneratePageBlocks generate the page blocks. Multiple blocks are generated if the contents wrap
// over multiple pages.
// Implements the Drawable interface.
@ -178,8 +196,12 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
// Start row keeps track of starting row (wraps to 0 on new page).
startrow := 0
// Indices of the first and the last header cells.
startHeaderCell := -1
endHeaderCell := -1
// Prepare for drawing: Calculate cell dimensions, row, cell heights.
for _, cell := range table.cells {
for cellIdx, cell := range table.cells {
// Get total width fraction
wf := float64(0.0)
for i := 0; i < cell.colspan; i++ {
@ -205,6 +227,16 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
h += table.rowHeights[cell.row+i-1]
}
// Calculate header cell range.
if table.hasHeader {
if cell.row >= table.headerStartRow && cell.row <= table.headerEndRow {
if startHeaderCell < 0 {
startHeaderCell = cellIdx
}
endHeaderCell = cellIdx
}
}
// For text: Calculate width, height, wrapping within available space if specified.
switch t := cell.content.(type) {
case *Paragraph:
@ -292,22 +324,28 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
table.rowHeights[cell.row+cell.rowspan-2] += diffh
}
}
}
// Draw cells.
// row height, cell height
for _, cell := range table.cells {
var drawingHeaders bool
var resumeIdx, resumeStartRow int
for cellIdx := 0; cellIdx < len(table.cells); cellIdx++ {
cell := table.cells[cellIdx]
// 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++ {
@ -324,7 +362,6 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
}
ctx.Height = origHeight - yrel
if h > ctx.Height {
// Go to next page.
blocks = append(blocks, block)
@ -337,6 +374,18 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
startrow = cell.row - 1
yrel = 0
// Save state and jump back to the first header cell.
if table.hasHeader && startHeaderCell >= 0 {
resumeIdx = cellIdx
cellIdx = startHeaderCell - 1
resumeStartRow = startrow
startrow = table.headerStartRow - 1
drawingHeaders = true
continue
}
}
// Height should be how much space there is left of the page.
@ -448,6 +497,18 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
}
ctx.Y += h
// Resume previous state after headers have been rendered.
if drawingHeaders && cellIdx+1 > endHeaderCell {
// Account for the height of the rendered headers.
ulY += yrel + h
origHeight -= h + yrel
startrow = resumeStartRow
cellIdx = resumeIdx - 1
drawingHeaders = false
}
}
blocks = append(blocks, block)

View File

@ -303,3 +303,106 @@ func TestTableColSpan(t *testing.T) {
t.Fatalf("Fail: %v\n", err)
}
}
func TestTableHeaderTest(t *testing.T) {
c := New()
p := c.NewStyledParagraph()
p.Append("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" +
"eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " +
"mollit anim id est laborum.")
table := c.NewTable(4)
table.SetColumnWidths(0.25, 0.25, 0.25, 0.25)
table.SetHeaderRows(1, 3)
// Add header
for i := 0; i < 3; i++ {
p := c.NewParagraph(fmt.Sprintf("Table header %d", i+1))
p.SetColor(ColorRGBFrom8bit(
byte((i+1)*50), byte((i+1)*50), byte((i+1)*50),
))
cell := table.MultiColCell(1)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(p)
cell.SetBackgroundColor(ColorRGBFrom8bit(
byte(100*(i+1)), byte(100*(i+1)), byte(100*(i+1)),
))
for j := 0; j < 3; j++ {
p := c.NewParagraph(fmt.Sprintf("Header column %d-%d", i+1, j+1))
p.SetColor(ColorRGBFrom8bit(255, 255, 255))
cell = table.MultiColCell(1)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(p)
cell.SetBackgroundColor(ColorRGBFrom8bit(
byte(100*(i+1)), byte(50*(j+1)), byte(50*(i+j+1)),
))
}
}
// Add content
for i := 0; i < 50; i++ {
j := i * 4
// Colspan 4
cell := table.MultiColCell(4)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 1", j+1)))
// Colspan 1 + 1 + 1 + 1
cell = table.NewCell()
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 1", j+2)))
cell = table.NewCell()
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 2", j+2)))
cell = table.NewCell()
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 3", j+2)))
cell = table.NewCell()
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 4", j+2)))
// Colspan 2 + 2
cell = table.MultiColCell(2)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 1", j+3)))
cell = table.MultiColCell(2)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 2", j+3)))
// Colspan 3 + 1
cell = table.MultiColCell(3)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 1", j+4)))
cell = table.NewCell()
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(c.NewParagraph(fmt.Sprintf("Line %d - Col 2", j+4)))
if i > 0 && i%5 == 0 {
cell := table.MultiColCell(4)
cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1)
cell.SetContent(p)
}
}
err := c.Draw(table)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
err = c.WriteToFile(tempFile("table_headers.pdf"))
if err != nil {
t.Fatalf("Fail: %v\n", err)
}
}