github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/creator/table.go (about)

     1  /*
     2   * This file is subject to the terms and conditions defined in
     3   * file 'LICENSE.md', which is part of this source code package.
     4   */
     5  
     6  package creator
     7  
     8  import (
     9  	"errors"
    10  
    11  	"github.com/unidoc/unidoc/common"
    12  	"github.com/unidoc/unidoc/pdf/model"
    13  )
    14  
    15  // Table allows organizing content in an rows X columns matrix, which can spawn across multiple pages.
    16  type Table struct {
    17  	// Number of rows and columns.
    18  	rows int
    19  	cols int
    20  
    21  	// Current cell.  Current cell in the table.
    22  	// For 4x4 table, if in the 2nd row, 3rd column, then
    23  	// curCell = 4+3 = 7
    24  	curCell int
    25  
    26  	// Column width fractions: should add up to 1.
    27  	colWidths []float64
    28  
    29  	// Row heights.
    30  	rowHeights []float64
    31  
    32  	// Default row height.
    33  	defaultRowHeight float64
    34  
    35  	// Content cells.
    36  	cells []*TableCell
    37  
    38  	// Positioning: relative / absolute.
    39  	positioning positioning
    40  
    41  	// Absolute coordinates (when in absolute mode).
    42  	xPos, yPos float64
    43  
    44  	// Margins to be applied around the block when drawing on Page.
    45  	margins margins
    46  }
    47  
    48  // NewTable create a new Table with a specified number of columns.
    49  func NewTable(cols int) *Table {
    50  	t := &Table{}
    51  	t.rows = 0
    52  	t.cols = cols
    53  
    54  	t.curCell = 0
    55  
    56  	// Initialize column widths as all equal.
    57  	t.colWidths = []float64{}
    58  	colWidth := float64(1.0) / float64(cols)
    59  	for i := 0; i < cols; i++ {
    60  		t.colWidths = append(t.colWidths, colWidth)
    61  	}
    62  
    63  	t.rowHeights = []float64{}
    64  
    65  	// Default row height
    66  	// XXX/TODO: Base on contents instead?
    67  	t.defaultRowHeight = 10.0
    68  
    69  	t.cells = []*TableCell{}
    70  
    71  	return t
    72  }
    73  
    74  // SetColumnWidths sets the fractional column widths.
    75  // Each width should be in the range 0-1 and is a fraction of the table width.
    76  // The number of width inputs must match number of columns, otherwise an error is returned.
    77  func (table *Table) SetColumnWidths(widths ...float64) error {
    78  	if len(widths) != table.cols {
    79  		common.Log.Debug("Mismatching number of widths and columns")
    80  		return errors.New("Range check error")
    81  	}
    82  
    83  	table.colWidths = widths
    84  
    85  	return nil
    86  }
    87  
    88  // Height returns the total height of all rows.
    89  func (table *Table) Height() float64 {
    90  	sum := float64(0.0)
    91  	for _, h := range table.rowHeights {
    92  		sum += h
    93  	}
    94  
    95  	return sum
    96  }
    97  
    98  // SetMargins sets the Table's left, right, top, bottom margins.
    99  func (table *Table) SetMargins(left, right, top, bottom float64) {
   100  	table.margins.left = left
   101  	table.margins.right = right
   102  	table.margins.top = top
   103  	table.margins.bottom = bottom
   104  }
   105  
   106  // GetMargins returns the left, right, top, bottom Margins.
   107  func (table *Table) GetMargins() (float64, float64, float64, float64) {
   108  	return table.margins.left, table.margins.right, table.margins.top, table.margins.bottom
   109  }
   110  
   111  // SetRowHeight sets the height for a specified row.
   112  func (table *Table) SetRowHeight(row int, h float64) error {
   113  	if row < 1 || row > len(table.rowHeights) {
   114  		return errors.New("Range check error")
   115  	}
   116  
   117  	table.rowHeights[row-1] = h
   118  	return nil
   119  }
   120  
   121  // CurRow returns the currently active cell's row number.
   122  func (table *Table) CurRow() int {
   123  	curRow := (table.curCell-1)/table.cols + 1
   124  	return curRow
   125  }
   126  
   127  // CurCol returns the currently active cell's column number.
   128  func (table *Table) CurCol() int {
   129  	curCol := (table.curCell-1)%(table.cols) + 1
   130  	return curCol
   131  }
   132  
   133  // SetPos sets the Table's positioning to absolute mode and specifies the upper-left corner coordinates as (x,y).
   134  // Note that this is only sensible to use when the table does not wrap over multiple pages.
   135  // TODO: Should be able to set width too (not just based on context/relative positioning mode).
   136  func (table *Table) SetPos(x, y float64) {
   137  	table.positioning = positionAbsolute
   138  	table.xPos = x
   139  	table.yPos = y
   140  }
   141  
   142  // GeneratePageBlocks generate the page blocks.  Multiple blocks are generated if the contents wrap over multiple pages.
   143  // Implements the Drawable interface.
   144  func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
   145  	blocks := []*Block{}
   146  	block := NewBlock(ctx.PageWidth, ctx.PageHeight)
   147  
   148  	origCtx := ctx
   149  	if table.positioning.isAbsolute() {
   150  		ctx.X = table.xPos
   151  		ctx.Y = table.yPos
   152  	} else {
   153  		// Relative mode: add margins.
   154  		ctx.X += table.margins.left
   155  		ctx.Y += table.margins.top
   156  		ctx.Width -= table.margins.left + table.margins.right
   157  		ctx.Height -= table.margins.bottom + table.margins.top
   158  	}
   159  	tableWidth := ctx.Width
   160  
   161  	// Store table's upper left corner.
   162  	ulX := ctx.X
   163  	ulY := ctx.Y
   164  
   165  	ctx.Height = ctx.PageHeight - ctx.Y - ctx.Margins.bottom
   166  	origHeight := ctx.Height
   167  
   168  	// Start row keeps track of starting row (wraps to 0 on new page).
   169  	startrow := 0
   170  
   171  	// Prepare for drawing: Calculate cell dimensions, row, cell heights.
   172  	for _, cell := range table.cells {
   173  		// Get total width fraction
   174  		wf := float64(0.0)
   175  		for i := 0; i < cell.colspan; i++ {
   176  			wf += table.colWidths[cell.col+i-1]
   177  		}
   178  		// Get x pos relative to table upper left corner.
   179  		xrel := float64(0.0)
   180  		for i := 0; i < cell.col-1; i++ {
   181  			xrel += table.colWidths[i] * tableWidth
   182  		}
   183  		// Get y pos relative to table upper left corner.
   184  		yrel := float64(0.0)
   185  		for i := startrow; i < cell.row-1; i++ {
   186  			yrel += table.rowHeights[i]
   187  		}
   188  
   189  		// Calculate the width out of available width.
   190  		w := wf * tableWidth
   191  
   192  		// Get total height.
   193  		h := float64(0.0)
   194  		for i := 0; i < cell.rowspan; i++ {
   195  			h += table.rowHeights[cell.row+i-1]
   196  		}
   197  
   198  		// For text: Calculate width, height, wrapping within available space if specified.
   199  		switch t := cell.content.(type) {
   200  		case *Paragraph:
   201  			p := t
   202  			if p.enableWrap {
   203  				p.SetWidth(w - cell.indent)
   204  			}
   205  
   206  			newh := p.Height() + p.margins.bottom + p.margins.bottom
   207  			newh += 0.5 * p.fontSize * p.lineHeight // TODO: Make the top margin configurable?
   208  			if newh > h {
   209  				diffh := newh - h
   210  				// Add diff to last row.
   211  				table.rowHeights[cell.row+cell.rowspan-2] += diffh
   212  			}
   213  		case *StyledParagraph:
   214  			sp := t
   215  			if sp.enableWrap {
   216  				sp.SetWidth(w - cell.indent)
   217  			}
   218  
   219  			newh := sp.Height() + sp.margins.top + sp.margins.bottom
   220  			newh += 0.5 * sp.getTextHeight() // TODO: Make the top margin configurable?
   221  			if newh > h {
   222  				diffh := newh - h
   223  				// Add diff to last row.
   224  				table.rowHeights[cell.row+cell.rowspan-2] += diffh
   225  			}
   226  		case *Image:
   227  			img := t
   228  			newh := img.Height() + img.margins.top + img.margins.bottom
   229  			if newh > h {
   230  				diffh := newh - h
   231  				// Add diff to last row.
   232  				table.rowHeights[cell.row+cell.rowspan-2] += diffh
   233  			}
   234  		case *Division:
   235  			div := t
   236  
   237  			ctx := DrawContext{
   238  				X:     xrel,
   239  				Y:     yrel,
   240  				Width: w,
   241  			}
   242  
   243  			// Mock call to generate page blocks.
   244  			divBlocks, updCtx, err := div.GeneratePageBlocks(ctx)
   245  			if err != nil {
   246  				return nil, ctx, err
   247  			}
   248  
   249  			if len(divBlocks) > 1 {
   250  				// Wraps across page, make cell reach all the way to bottom of current page.
   251  				newh := ctx.Height - h
   252  				if newh > h {
   253  					diffh := newh - h
   254  					// Add diff to last row.
   255  					table.rowHeights[cell.row+cell.rowspan-2] += diffh
   256  				}
   257  			}
   258  
   259  			newh := div.Height() + div.margins.top + div.margins.bottom
   260  			_ = updCtx
   261  
   262  			// Get available width and height.
   263  			if newh > h {
   264  				diffh := newh - h
   265  				// Add diff to last row.
   266  				table.rowHeights[cell.row+cell.rowspan-2] += diffh
   267  			}
   268  		}
   269  
   270  	}
   271  
   272  	// Draw cells.
   273  	// row height, cell height
   274  	for _, cell := range table.cells {
   275  		// Get total width fraction
   276  		wf := float64(0.0)
   277  		for i := 0; i < cell.colspan; i++ {
   278  			wf += table.colWidths[cell.col+i-1]
   279  		}
   280  		// Get x pos relative to table upper left corner.
   281  		xrel := float64(0.0)
   282  		for i := 0; i < cell.col-1; i++ {
   283  			xrel += table.colWidths[i] * tableWidth
   284  		}
   285  		// Get y pos relative to table upper left corner.
   286  		yrel := float64(0.0)
   287  		for i := startrow; i < cell.row-1; i++ {
   288  			yrel += table.rowHeights[i]
   289  		}
   290  
   291  		// Calculate the width out of available width.
   292  		w := wf * tableWidth
   293  
   294  		// Get total height.
   295  		h := float64(0.0)
   296  		for i := 0; i < cell.rowspan; i++ {
   297  			h += table.rowHeights[cell.row+i-1]
   298  		}
   299  
   300  		ctx.Height = origHeight - yrel
   301  
   302  		if h > ctx.Height {
   303  			// Go to next page.
   304  			blocks = append(blocks, block)
   305  			block = NewBlock(ctx.PageWidth, ctx.PageHeight)
   306  			ulX = ctx.Margins.left
   307  			ulY = ctx.Margins.top
   308  			ctx.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom
   309  
   310  			startrow = cell.row - 1
   311  			yrel = 0
   312  		}
   313  
   314  		// Height should be how much space there is left of the page.
   315  		ctx.Width = w
   316  		ctx.X = ulX + xrel
   317  		ctx.Y = ulY + yrel
   318  
   319  		if cell.backgroundColor != nil {
   320  			// Draw background (fill)
   321  			rect := NewRectangle(ctx.X, ctx.Y, w, h)
   322  			r := cell.backgroundColor.R()
   323  			g := cell.backgroundColor.G()
   324  			b := cell.backgroundColor.B()
   325  			rect.SetFillColor(ColorRGBFromArithmetic(r, g, b))
   326  			if cell.borderStyle != CellBorderStyleNone {
   327  				// and border.
   328  				rect.SetBorderWidth(cell.borderWidth)
   329  				r := cell.borderColor.R()
   330  				g := cell.borderColor.G()
   331  				b := cell.borderColor.B()
   332  				rect.SetBorderColor(ColorRGBFromArithmetic(r, g, b))
   333  			} else {
   334  				rect.SetBorderWidth(0)
   335  			}
   336  			err := block.Draw(rect)
   337  			if err != nil {
   338  				common.Log.Debug("Error: %v\n", err)
   339  			}
   340  		} else if cell.borderStyle != CellBorderStyleNone {
   341  			// Draw border (no fill).
   342  			rect := NewRectangle(ctx.X, ctx.Y, w, h)
   343  			rect.SetBorderWidth(cell.borderWidth)
   344  			r := cell.borderColor.R()
   345  			g := cell.borderColor.G()
   346  			b := cell.borderColor.B()
   347  			rect.SetBorderColor(ColorRGBFromArithmetic(r, g, b))
   348  			err := block.Draw(rect)
   349  			if err != nil {
   350  				common.Log.Debug("Error: %v\n", err)
   351  			}
   352  		}
   353  
   354  		if cell.content != nil {
   355  			// Account for horizontal alignment:
   356  			cw := cell.content.Width() // content width.
   357  			switch cell.horizontalAlignment {
   358  			case CellHorizontalAlignmentLeft:
   359  				// Account for indent.
   360  				ctx.X += cell.indent
   361  				ctx.Width -= cell.indent
   362  			case CellHorizontalAlignmentCenter:
   363  				// Difference between available space and content space.
   364  				dw := w - cw
   365  				if dw > 0 {
   366  					ctx.X += dw / 2
   367  					ctx.Width -= dw / 2
   368  				}
   369  			case CellHorizontalAlignmentRight:
   370  				if w > cw {
   371  					ctx.X = ctx.X + w - cw - cell.indent
   372  					ctx.Width = cw
   373  				}
   374  			}
   375  
   376  			// Account for vertical alignment.
   377  			ch := cell.content.Height() // content height.
   378  			switch cell.verticalAlignment {
   379  			case CellVerticalAlignmentTop:
   380  				// Default: do nothing.
   381  			case CellVerticalAlignmentMiddle:
   382  				dh := h - ch
   383  				if dh > 0 {
   384  					ctx.Y += dh / 2
   385  					ctx.Height -= dh / 2
   386  				}
   387  			case CellVerticalAlignmentBottom:
   388  				if h > ch {
   389  					ctx.Y = ctx.Y + h - ch
   390  					ctx.Height = ch
   391  				}
   392  			}
   393  
   394  			err := block.DrawWithContext(cell.content, ctx)
   395  			if err != nil {
   396  				common.Log.Debug("Error: %v\n", err)
   397  			}
   398  		}
   399  
   400  		ctx.Y += h
   401  	}
   402  	blocks = append(blocks, block)
   403  
   404  	if table.positioning.isAbsolute() {
   405  		return blocks, origCtx, nil
   406  	}
   407  	// Relative mode.
   408  	// Move back X after.
   409  	ctx.X = origCtx.X
   410  	// Return original width.
   411  	ctx.Width = origCtx.Width
   412  	// Add the bottom margin.
   413  	ctx.Y += table.margins.bottom
   414  
   415  	return blocks, ctx, nil
   416  }
   417  
   418  // CellBorderStyle defines the table cell's border style.
   419  type CellBorderStyle int
   420  
   421  // Currently supported table styles are: None (no border) and boxed (line along each side).
   422  const (
   423  	// No border
   424  	CellBorderStyleNone CellBorderStyle = iota
   425  
   426  	// Borders along all sides (boxed).
   427  	CellBorderStyleBox
   428  )
   429  
   430  // CellHorizontalAlignment defines the table cell's horizontal alignment.
   431  type CellHorizontalAlignment int
   432  
   433  // Table cells have three horizontal alignment modes: left, center and right.
   434  const (
   435  	// Align cell content on the left (with specified indent); unused space on the right.
   436  	CellHorizontalAlignmentLeft CellHorizontalAlignment = iota
   437  
   438  	// Align cell content in the middle (unused space divided equally on the left/right).
   439  	CellHorizontalAlignmentCenter
   440  
   441  	// Align the cell content on the right; unsued space on the left.
   442  	CellHorizontalAlignmentRight
   443  )
   444  
   445  // CellVerticalAlignment defines the table cell's vertical alignment.
   446  type CellVerticalAlignment int
   447  
   448  // Table cells have three vertical alignment modes: top, middle and bottom.
   449  const (
   450  	// Align cell content vertically to the top; unused space below.
   451  	CellVerticalAlignmentTop CellVerticalAlignment = iota
   452  
   453  	// Align cell content in the middle; unused space divided equally above and below.
   454  	CellVerticalAlignmentMiddle
   455  
   456  	// Align cell content on the bottom; unused space above.
   457  	CellVerticalAlignmentBottom
   458  )
   459  
   460  // TableCell defines a table cell which can contain a Drawable as content.
   461  type TableCell struct {
   462  	// Background
   463  	backgroundColor *model.PdfColorDeviceRGB
   464  
   465  	// Border
   466  	borderStyle CellBorderStyle
   467  	borderColor *model.PdfColorDeviceRGB
   468  	borderWidth float64
   469  
   470  	// The row and column which the cell starts from.
   471  	row, col int
   472  
   473  	// Row, column span.
   474  	rowspan int
   475  	colspan int
   476  
   477  	// Each cell can contain 1 drawable.
   478  	content VectorDrawable
   479  
   480  	// Alignment
   481  	horizontalAlignment CellHorizontalAlignment
   482  	verticalAlignment   CellVerticalAlignment
   483  
   484  	// Left indent.
   485  	indent float64
   486  
   487  	// Table reference
   488  	table *Table
   489  }
   490  
   491  // NewCell makes a new cell and inserts into the table at current position in the table.
   492  func (table *Table) NewCell() *TableCell {
   493  	table.curCell++
   494  
   495  	curRow := (table.curCell-1)/table.cols + 1
   496  	for curRow > table.rows {
   497  		table.rows++
   498  		table.rowHeights = append(table.rowHeights, table.defaultRowHeight)
   499  	}
   500  	curCol := (table.curCell-1)%(table.cols) + 1
   501  
   502  	cell := &TableCell{}
   503  	cell.row = curRow
   504  	cell.col = curCol
   505  
   506  	// Default left indent
   507  	cell.indent = 5
   508  
   509  	cell.borderStyle = CellBorderStyleNone
   510  	cell.borderColor = model.NewPdfColorDeviceRGB(0, 0, 0)
   511  
   512  	// Alignment defaults.
   513  	cell.horizontalAlignment = CellHorizontalAlignmentLeft
   514  	cell.verticalAlignment = CellVerticalAlignmentTop
   515  
   516  	cell.rowspan = 1
   517  	cell.colspan = 1
   518  
   519  	table.cells = append(table.cells, cell)
   520  
   521  	// Keep reference to the table.
   522  	cell.table = table
   523  
   524  	return cell
   525  }
   526  
   527  // SkipCells skips over a specified number of cells in the table.
   528  func (table *Table) SkipCells(num int) {
   529  	if num < 0 {
   530  		common.Log.Debug("Table: cannot skip back to previous cells")
   531  		return
   532  	}
   533  	table.curCell += num
   534  }
   535  
   536  // SkipRows skips over a specified number of rows in the table.
   537  func (table *Table) SkipRows(num int) {
   538  	ncells := num*table.cols - 1
   539  	if ncells < 0 {
   540  		common.Log.Debug("Table: cannot skip back to previous cells")
   541  		return
   542  	}
   543  	table.curCell += ncells
   544  }
   545  
   546  // SkipOver skips over a specified number of rows and cols.
   547  func (table *Table) SkipOver(rows, cols int) {
   548  	ncells := rows*table.cols + cols - 1
   549  	if ncells < 0 {
   550  		common.Log.Debug("Table: cannot skip back to previous cells")
   551  		return
   552  	}
   553  	table.curCell += ncells
   554  }
   555  
   556  // SetIndent sets the cell's left indent.
   557  func (cell *TableCell) SetIndent(indent float64) {
   558  	cell.indent = indent
   559  }
   560  
   561  // SetHorizontalAlignment sets the cell's horizontal alignment of content.
   562  // Can be one of:
   563  // - CellHorizontalAlignmentLeft
   564  // - CellHorizontalAlignmentCenter
   565  // - CellHorizontalAlignmentRight
   566  func (cell *TableCell) SetHorizontalAlignment(halign CellHorizontalAlignment) {
   567  	cell.horizontalAlignment = halign
   568  }
   569  
   570  // SetVerticalAlignment set the cell's vertical alignment of content.
   571  // Can be one of:
   572  // - CellHorizontalAlignmentTop
   573  // - CellHorizontalAlignmentMiddle
   574  // - CellHorizontalAlignmentBottom
   575  func (cell *TableCell) SetVerticalAlignment(valign CellVerticalAlignment) {
   576  	cell.verticalAlignment = valign
   577  }
   578  
   579  // SetBorder sets the cell's border style.
   580  func (cell *TableCell) SetBorder(style CellBorderStyle, width float64) {
   581  	cell.borderStyle = style
   582  	cell.borderWidth = width
   583  }
   584  
   585  // SetBorderColor sets the cell's border color.
   586  func (cell *TableCell) SetBorderColor(col Color) {
   587  	cell.borderColor = model.NewPdfColorDeviceRGB(col.ToRGB())
   588  }
   589  
   590  // SetBackgroundColor sets the cell's background color.
   591  func (cell *TableCell) SetBackgroundColor(col Color) {
   592  	cell.backgroundColor = model.NewPdfColorDeviceRGB(col.ToRGB())
   593  }
   594  
   595  // Width returns the cell's width based on the input draw context.
   596  func (cell *TableCell) Width(ctx DrawContext) float64 {
   597  	fraction := float64(0.0)
   598  	for j := 0; j < cell.colspan; j++ {
   599  		fraction += cell.table.colWidths[cell.col+j-1]
   600  	}
   601  	w := ctx.Width * fraction
   602  	return w
   603  }
   604  
   605  // SetContent sets the cell's content.  The content is a VectorDrawable, i.e. a Drawable with a known height and width.
   606  // The currently supported VectorDrawable is: *Paragraph, *StyledParagraph.
   607  func (cell *TableCell) SetContent(vd VectorDrawable) error {
   608  	switch t := vd.(type) {
   609  	case *Paragraph:
   610  		if t.defaultWrap {
   611  			// Default paragraph settings in table: no wrapping.
   612  			t.enableWrap = false // No wrapping.
   613  		}
   614  
   615  		cell.content = vd
   616  	case *StyledParagraph:
   617  		if t.defaultWrap {
   618  			// Default styled paragraph settings in table: no wrapping.
   619  			t.enableWrap = false // No wrapping.
   620  		}
   621  
   622  		cell.content = vd
   623  	case *Image:
   624  		cell.content = vd
   625  	case *Division:
   626  		cell.content = vd
   627  	default:
   628  		common.Log.Debug("Error: unsupported cell content type %T\n", vd)
   629  		return errors.New("Type check error")
   630  	}
   631  
   632  	return nil
   633  }