/* * 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 ( "errors" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/contentstream/draw" "github.com/unidoc/unidoc/pdf/core" "github.com/unidoc/unidoc/pdf/model" ) // Table allows organizing content in an rows X columns matrix, which can spawn across multiple pages. type Table struct { // Number of rows and columns. rows int cols int // Current cell. Current cell in the table. // For 4x4 table, if in the 2nd row, 3rd column, then // curCell = 4+3 = 7 curCell int // Column width fractions: should add up to 1. colWidths []float64 // Row heights. rowHeights []float64 // Default row height. defaultRowHeight float64 // Content cells. cells []*TableCell // 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 } // 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 // 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 } // SetColumnWidths sets the fractional column widths. // Each width should be in the range 0-1 and is a fraction of the table width. // The number of width inputs must match number of columns, otherwise an error is returned. func (table *Table) SetColumnWidths(widths ...float64) error { if len(widths) != table.cols { common.Log.Debug("Mismatching number of widths and columns") return errors.New("range check error") } table.colWidths = widths return nil } // Height returns the total height of all rows. func (table *Table) Height() float64 { sum := float64(0.0) for _, h := range table.rowHeights { sum += h } return sum } // Width is not used. Not used as a Table element is designed to fill into // available width depending on the context. Returns 0. func (table *Table) Width() float64 { return 0 } // SetMargins sets the Table's left, right, top, bottom margins. func (table *Table) SetMargins(left, right, top, bottom float64) { table.margins.left = left table.margins.right = right table.margins.top = top table.margins.bottom = bottom } // GetMargins returns the left, right, top, bottom Margins. func (table *Table) GetMargins() (float64, float64, float64, float64) { return table.margins.left, table.margins.right, table.margins.top, table.margins.bottom } // SetRowHeight sets the height for a specified row. func (table *Table) SetRowHeight(row int, h float64) error { if row < 1 || row > len(table.rowHeights) { return errors.New("range check error") } table.rowHeights[row-1] = h return nil } // CurRow returns the currently active cell's row number. func (table *Table) CurRow() int { curRow := (table.curCell-1)/table.cols + 1 return curRow } // CurCol returns the currently active cell's column number. func (table *Table) CurCol() int { curCol := (table.curCell-1)%(table.cols) + 1 return curCol } // SetPos sets the Table's positioning to absolute mode and specifies the upper-left corner // coordinates as (x,y). // Note that this is only sensible to use when the table does not wrap over multiple pages. // TODO: Should be able to set width too (not just based on context/relative positioning mode). func (table *Table) SetPos(x, y float64) { table.positioning = positionAbsolute table.xPos = x table.yPos = y } // GeneratePageBlocks generate the page blocks. Multiple blocks are generated if the contents wrap // over multiple pages. // Implements the Drawable interface. func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { var blocks []*Block block := NewBlock(ctx.PageWidth, ctx.PageHeight) origCtx := ctx if table.positioning.isAbsolute() { ctx.X = table.xPos ctx.Y = table.yPos } else { // Relative mode: add margins. ctx.X += table.margins.left ctx.Y += table.margins.top ctx.Width -= table.margins.left + table.margins.right ctx.Height -= table.margins.bottom + table.margins.top } tableWidth := ctx.Width // Store table's upper left corner. ulX := ctx.X ulY := ctx.Y ctx.Height = ctx.PageHeight - ctx.Y - ctx.Margins.bottom origHeight := ctx.Height // Start row keeps track of starting row (wraps to 0 on new page). startrow := 0 // Prepare for drawing: Calculate cell dimensions, row, cell heights. for _, cell := range table.cells { // 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++ { yrel += table.rowHeights[i] } // Calculate the width out of available width. w := wf * tableWidth // Get total height. h := float64(0.0) for i := 0; i < cell.rowspan; i++ { h += table.rowHeights[cell.row+i-1] } // For text: Calculate width, height, wrapping within available space if specified. switch t := cell.content.(type) { case *Paragraph: p := t if p.enableWrap { p.SetWidth(w - cell.indent) } newh := p.Height() + p.margins.bottom + p.margins.bottom newh += 0.5 * p.fontSize * p.lineHeight // TODO: Make the top margin configurable? if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *StyledParagraph: sp := t if sp.enableWrap { sp.SetWidth(w - cell.indent) } newh := sp.Height() + sp.margins.top + sp.margins.bottom newh += 0.5 * sp.getTextHeight() // TODO: Make the top margin configurable? if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *Image: img := t newh := img.Height() + img.margins.top + img.margins.bottom if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *Table: tbl := t newh := tbl.Height() + tbl.margins.top + tbl.margins.bottom if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *List: lst := t newh := lst.tableHeight(w-cell.indent) + lst.margins.top + lst.margins.bottom if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *Division: div := t ctx := DrawContext{ X: xrel, Y: yrel, Width: w, } // Mock call to generate page blocks. divBlocks, updCtx, err := div.GeneratePageBlocks(ctx) if err != nil { return nil, ctx, err } if len(divBlocks) > 1 { // Wraps across page, make cell reach all the way to bottom of current page. newh := ctx.Height - h if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } } newh := div.Height() + div.margins.top + div.margins.bottom _ = updCtx // Get available width and height. if newh > h { diffh := newh - h // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } } } // Draw cells. // row height, cell height for _, cell := range table.cells { // 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++ { yrel += table.rowHeights[i] } // Calculate the width out of available width. w := wf * tableWidth // Get total height. h := float64(0.0) for i := 0; i < cell.rowspan; i++ { h += table.rowHeights[cell.row+i-1] } ctx.Height = origHeight - yrel if h > ctx.Height { // Go to next page. blocks = append(blocks, block) block = NewBlock(ctx.PageWidth, ctx.PageHeight) ulX = ctx.Margins.left ulY = ctx.Margins.top ctx.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom origHeight = ctx.Height startrow = cell.row - 1 yrel = 0 } // Height should be how much space there is left of the page. ctx.Width = w ctx.X = ulX + xrel ctx.Y = ulY + yrel // Creating border border := newBorder(ctx.X, ctx.Y, w, h) if cell.backgroundColor != nil { r := cell.backgroundColor.R() g := cell.backgroundColor.G() b := cell.backgroundColor.B() border.SetFillColor(ColorRGBFromArithmetic(r, g, b)) } border.LineStyle = cell.borderLineStyle border.styleLeft = cell.borderStyleLeft border.styleRight = cell.borderStyleRight border.styleTop = cell.borderStyleTop border.styleBottom = cell.borderStyleBottom if cell.borderColorLeft != nil { border.SetColorLeft(ColorRGBFromArithmetic(cell.borderColorLeft.R(), cell.borderColorLeft.G(), cell.borderColorLeft.B())) } if cell.borderColorBottom != nil { border.SetColorBottom(ColorRGBFromArithmetic(cell.borderColorBottom.R(), cell.borderColorBottom.G(), cell.borderColorBottom.B())) } if cell.borderColorRight != nil { border.SetColorRight(ColorRGBFromArithmetic(cell.borderColorRight.R(), cell.borderColorRight.G(), cell.borderColorRight.B())) } if cell.borderColorTop != nil { border.SetColorTop(ColorRGBFromArithmetic(cell.borderColorTop.R(), cell.borderColorTop.G(), cell.borderColorTop.B())) } border.SetWidthBottom(cell.borderWidthBottom) border.SetWidthLeft(cell.borderWidthLeft) border.SetWidthRight(cell.borderWidthRight) border.SetWidthTop(cell.borderWidthTop) err := block.Draw(border) if err != nil { common.Log.Debug("ERROR: %v", err) } if cell.content != nil { // content width. cw := cell.content.Width() switch t := cell.content.(type) { case *Paragraph: if t.enableWrap { cw = t.getMaxLineWidth() / 1000.0 } case *StyledParagraph: if t.enableWrap { cw = t.getMaxLineWidth() / 1000.0 } case *Table: cw = w case *List: cw = w } // Account for horizontal alignment: switch cell.horizontalAlignment { case CellHorizontalAlignmentLeft: // Account for indent. ctx.X += cell.indent ctx.Width -= cell.indent case CellHorizontalAlignmentCenter: // Difference between available space and content space. dw := w - cw if dw > 0 { ctx.X += dw / 2 ctx.Width -= dw / 2 } case CellHorizontalAlignmentRight: if w > cw { ctx.X = ctx.X + w - cw - cell.indent ctx.Width = cw } } // Account for vertical alignment. ch := cell.content.Height() // content height. switch cell.verticalAlignment { case CellVerticalAlignmentTop: // Default: do nothing. case CellVerticalAlignmentMiddle: dh := h - ch if dh > 0 { ctx.Y += dh / 2 ctx.Height -= dh / 2 } case CellVerticalAlignmentBottom: if h > ch { ctx.Y = ctx.Y + h - ch ctx.Height = ch } } err := block.DrawWithContext(cell.content, ctx) if err != nil { common.Log.Debug("ERROR: %v", err) } } ctx.Y += h } blocks = append(blocks, block) if table.positioning.isAbsolute() { return blocks, origCtx, nil } // Relative mode. // Move back X after. ctx.X = origCtx.X // Return original width. ctx.Width = origCtx.Width // Add the bottom margin. ctx.Y += table.margins.bottom return blocks, ctx, nil } // CellBorderStyle defines the table cell's border style. type CellBorderStyle int // Currently supported table styles are: None (no border) and boxed (line along each side). const ( CellBorderStyleNone CellBorderStyle = iota // no border // Borders along all sides (boxed). CellBorderStyleSingle CellBorderStyleDouble ) // CellBorderSide defines the table cell's border side. type CellBorderSide int const ( // CellBorderSideLeft adds border on the left side of the table. CellBorderSideLeft CellBorderSide = iota // CellBorderSideRight adds a border on the right side of the table. CellBorderSideRight // CellBorderSideTop adds a border on the top side of the table. CellBorderSideTop // CellBorderSideBottom adds a border on the bottom side of the table. CellBorderSideBottom // CellBorderSideAll adds borders on all sides of the table. CellBorderSideAll ) // CellHorizontalAlignment defines the table cell's horizontal alignment. type CellHorizontalAlignment int // Table cells have three horizontal alignment modes: left, center and right. const ( // CellHorizontalAlignmentLeft aligns cell content on the left (with specified indent); unused space on the right. CellHorizontalAlignmentLeft CellHorizontalAlignment = iota // CellHorizontalAlignmentCenter aligns cell content in the middle (unused space divided equally on the left/right). CellHorizontalAlignmentCenter // CellHorizontalAlignmentRight aligns the cell content on the right; unsued space on the left. CellHorizontalAlignmentRight ) // CellVerticalAlignment defines the table cell's vertical alignment. type CellVerticalAlignment int // Table cells have three vertical alignment modes: top, middle and bottom. const ( // CellVerticalAlignmentTop aligns cell content vertically to the top; unused space below. CellVerticalAlignmentTop CellVerticalAlignment = iota // CellVerticalAlignmentMiddle aligns cell content in the middle; unused space divided equally above and below. CellVerticalAlignmentMiddle // CellVerticalAlignmentBottom aligns cell content on the bottom; unused space above. CellVerticalAlignmentBottom ) // TableCell defines a table cell which can contain a Drawable as content. type TableCell struct { // Background backgroundColor *model.PdfColorDeviceRGB borderLineStyle draw.LineStyle // border borderStyleLeft CellBorderStyle borderColorLeft *model.PdfColorDeviceRGB borderWidthLeft float64 borderStyleBottom CellBorderStyle borderColorBottom *model.PdfColorDeviceRGB borderWidthBottom float64 borderStyleRight CellBorderStyle borderColorRight *model.PdfColorDeviceRGB borderWidthRight float64 borderStyleTop CellBorderStyle borderColorTop *model.PdfColorDeviceRGB borderWidthTop float64 // The row and column which the cell starts from. row, col int // Row, column span. rowspan int colspan int // Each cell can contain 1 drawable. content VectorDrawable // Alignment horizontalAlignment CellHorizontalAlignment verticalAlignment CellVerticalAlignment // Left indent. indent float64 // Table reference table *Table } // NewCell makes a new cell and inserts into the table at current position in the table. func (table *Table) NewCell() *TableCell { table.curCell++ curRow := (table.curCell-1)/table.cols + 1 for curRow > table.rows { table.rows++ table.rowHeights = append(table.rowHeights, table.defaultRowHeight) } curCol := (table.curCell-1)%(table.cols) + 1 cell := &TableCell{} cell.row = curRow cell.col = curCol // Default left indent cell.indent = 5 cell.borderStyleLeft = CellBorderStyleNone cell.borderLineStyle = draw.LineStyleSolid // Alignment defaults. cell.horizontalAlignment = CellHorizontalAlignmentLeft cell.verticalAlignment = CellVerticalAlignmentTop cell.borderWidthLeft = 0 cell.borderWidthBottom = 0 cell.borderWidthRight = 0 cell.borderWidthTop = 0 col := ColorBlack cell.borderColorLeft = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.borderColorBottom = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.borderColorRight = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.borderColorTop = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.rowspan = 1 cell.colspan = 1 table.cells = append(table.cells, cell) // Keep reference to the table. cell.table = table return cell } // SkipCells skips over a specified number of cells in the table. func (table *Table) SkipCells(num int) { if num < 0 { common.Log.Debug("Table: cannot skip back to previous cells") return } table.curCell += num } // SkipRows skips over a specified number of rows in the table. func (table *Table) SkipRows(num int) { ncells := num*table.cols - 1 if ncells < 0 { common.Log.Debug("Table: cannot skip back to previous cells") return } table.curCell += ncells } // SkipOver skips over a specified number of rows and cols. func (table *Table) SkipOver(rows, cols int) { ncells := rows*table.cols + cols - 1 if ncells < 0 { common.Log.Debug("Table: cannot skip back to previous cells") return } table.curCell += ncells } // SetIndent sets the cell's left indent. func (cell *TableCell) SetIndent(indent float64) { cell.indent = indent } // SetHorizontalAlignment sets the cell's horizontal alignment of content. // Can be one of: // - CellHorizontalAlignmentLeft // - CellHorizontalAlignmentCenter // - CellHorizontalAlignmentRight func (cell *TableCell) SetHorizontalAlignment(halign CellHorizontalAlignment) { cell.horizontalAlignment = halign } // SetVerticalAlignment set the cell's vertical alignment of content. // Can be one of: // - CellHorizontalAlignmentTop // - CellHorizontalAlignmentMiddle // - CellHorizontalAlignmentBottom func (cell *TableCell) SetVerticalAlignment(valign CellVerticalAlignment) { cell.verticalAlignment = valign } // SetBorder sets the cell's border style. func (cell *TableCell) SetBorder(side CellBorderSide, style CellBorderStyle, width float64) { if style == CellBorderStyleSingle && side == CellBorderSideAll { cell.borderStyleLeft = CellBorderStyleSingle cell.borderWidthLeft = width cell.borderStyleBottom = CellBorderStyleSingle cell.borderWidthBottom = width cell.borderStyleRight = CellBorderStyleSingle cell.borderWidthRight = width cell.borderStyleTop = CellBorderStyleSingle cell.borderWidthTop = width } else if style == CellBorderStyleDouble && side == CellBorderSideAll { cell.borderStyleLeft = CellBorderStyleDouble cell.borderWidthLeft = width cell.borderStyleBottom = CellBorderStyleDouble cell.borderWidthBottom = width cell.borderStyleRight = CellBorderStyleDouble cell.borderWidthRight = width cell.borderStyleTop = CellBorderStyleDouble cell.borderWidthTop = width } else if (style == CellBorderStyleSingle || style == CellBorderStyleDouble) && side == CellBorderSideLeft { cell.borderStyleLeft = style cell.borderWidthLeft = width } else if (style == CellBorderStyleSingle || style == CellBorderStyleDouble) && side == CellBorderSideBottom { cell.borderStyleBottom = style cell.borderWidthBottom = width } else if (style == CellBorderStyleSingle || style == CellBorderStyleDouble) && side == CellBorderSideRight { cell.borderStyleRight = style cell.borderWidthRight = width } else if (style == CellBorderStyleSingle || style == CellBorderStyleDouble) && side == CellBorderSideTop { cell.borderStyleTop = style cell.borderWidthTop = width } } // SetBorderColor sets the cell's border color. func (cell *TableCell) SetBorderColor(col Color) { cell.borderColorLeft = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.borderColorBottom = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.borderColorRight = model.NewPdfColorDeviceRGB(col.ToRGB()) cell.borderColorTop = model.NewPdfColorDeviceRGB(col.ToRGB()) } // SetBorderLineStyle sets border style (currently dashed or plain). func (cell *TableCell) SetBorderLineStyle(style draw.LineStyle) { cell.borderLineStyle = style } // SetBackgroundColor sets the cell's background color. func (cell *TableCell) SetBackgroundColor(col Color) { cell.backgroundColor = model.NewPdfColorDeviceRGB(col.ToRGB()) } // Width returns the cell's width based on the input draw context. func (cell *TableCell) Width(ctx DrawContext) float64 { fraction := float64(0.0) for j := 0; j < cell.colspan; j++ { fraction += cell.table.colWidths[cell.col+j-1] } w := ctx.Width * fraction return w } // SetContent sets the cell's content. The content is a VectorDrawable, i.e. a Drawable with a known height and width. // The currently supported VectorDrawable is: *Paragraph, *StyledParagraph. func (cell *TableCell) SetContent(vd VectorDrawable) error { switch t := vd.(type) { case *Paragraph: if t.defaultWrap { // Enable wrapping by default. t.enableWrap = true } cell.content = vd case *StyledParagraph: if t.defaultWrap { // Enable wrapping by default. t.enableWrap = true } cell.content = vd case *Image: cell.content = vd case *Table: cell.content = vd case *List: cell.content = vd case *Division: cell.content = vd default: common.Log.Debug("ERROR: unsupported cell content type %T", vd) return core.ErrTypeError } return nil }