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 }