mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-09 19:29:34 +08:00
Add table headers support (#390)
* Add table headers support with a testcase.
This commit is contained in:
parent
95b4e1cfc0
commit
54264e343e
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user