Add support for rotated appendices: implement 90° CCW image rotation for portrait pages, enhance table rendering logic, and update diagram handling directives.
This commit is contained in:
+149
-13
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -30,8 +31,19 @@ func (r *IHKRenderer) RenderHeader(level int, title string) {
|
||||
// Major sections start on a new page per IHK convention.
|
||||
r.pdf.AddPage()
|
||||
} else if !r.isAtPageTop() {
|
||||
// IHK: two blank lines before a heading that is not at the page top.
|
||||
r.pdf.Ln(dinSpaceBeforeHeading)
|
||||
// Require enough space for: before-spacing + heading + after-spacing + 3 body
|
||||
// lines. If the remaining page space is too tight, start a new page instead
|
||||
// of leaving an orphaned heading at the bottom.
|
||||
const minBodyLines = 3
|
||||
needed := dinSpaceBeforeHeading + dinLineHtHeading + dinSpaceAfterHeading + minBodyLines*dinLineHtBody
|
||||
_, pageH := r.pdf.GetPageSize()
|
||||
_, _, _, bm := r.pdf.GetMargins()
|
||||
if r.pdf.GetY()+needed > pageH-bm {
|
||||
r.pdf.AddPage()
|
||||
} else {
|
||||
// IHK: two blank lines before a heading that is not at the page top.
|
||||
r.pdf.Ln(dinSpaceBeforeHeading)
|
||||
}
|
||||
}
|
||||
|
||||
r.RecordHeader(level, title)
|
||||
@@ -229,9 +241,93 @@ func (r *IHKRenderer) wrapCellSpans(spans []InlineSpan, maxWidth float64, forceB
|
||||
return lines
|
||||
}
|
||||
|
||||
// computeColWidths measures the natural content width of each column and
|
||||
// returns per-column widths that sum to at most uw.
|
||||
//
|
||||
// When columns do not fit at natural width, narrow columns are protected:
|
||||
// processing from smallest to largest, each column receives its natural width
|
||||
// if a fair share of the remaining space allows it; only the widest columns
|
||||
// absorb the compression. This prevents narrow ID/label columns from being
|
||||
// crushed to illegibility by a single very wide content column.
|
||||
func (r *IHKRenderer) computeColWidths(data [][][]InlineSpan, numCols int, uw float64) []float64 {
|
||||
const padding = 4.0 // 2 mm left + 2 mm right per cell
|
||||
natural := make([]float64, numCols)
|
||||
|
||||
for rowIdx, row := range data {
|
||||
isHeader := rowIdx == 0
|
||||
for j := 0; j < numCols; j++ {
|
||||
if j >= len(row) {
|
||||
continue
|
||||
}
|
||||
w := padding
|
||||
for _, span := range row[j] {
|
||||
if span.Code {
|
||||
r.pdf.SetFont("Courier", "", dinFontCaption)
|
||||
} else {
|
||||
r.pdf.SetFont("Helvetica", fontStyle(span.Bold || isHeader, span.Italic), dinFontCaption)
|
||||
}
|
||||
w += r.pdf.GetStringWidth(r.tr(span.Text))
|
||||
}
|
||||
if w > natural[j] {
|
||||
natural[j] = w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, w := range natural {
|
||||
total += w
|
||||
}
|
||||
|
||||
if total <= uw {
|
||||
// Scale all columns up proportionally so the table always fills the full
|
||||
// usable width. Without this, tables with short content leave a blank gap
|
||||
// on the right and borders don't align with the page layout.
|
||||
scale := uw / total
|
||||
for j := range natural {
|
||||
natural[j] *= scale
|
||||
}
|
||||
return natural
|
||||
}
|
||||
|
||||
// total > uw: scale down, but protect narrow columns from being crushed.
|
||||
// Sort column indices from narrowest to widest natural width.
|
||||
// Give each narrow column its full natural width while possible;
|
||||
// the remaining (wider) columns share whatever space is left, proportionally.
|
||||
order := make([]int, numCols)
|
||||
for i := range order {
|
||||
order[i] = i
|
||||
}
|
||||
sort.Slice(order, func(a, b int) bool { return natural[order[a]] < natural[order[b]] })
|
||||
|
||||
result := make([]float64, numCols)
|
||||
remaining := uw
|
||||
|
||||
for pass, j := range order {
|
||||
colsLeft := numCols - pass
|
||||
fairShare := remaining / float64(colsLeft)
|
||||
if natural[j] <= fairShare {
|
||||
result[j] = natural[j]
|
||||
remaining -= natural[j]
|
||||
} else {
|
||||
// This and all following (wider) columns share the remaining space
|
||||
// proportionally to their natural widths.
|
||||
naturalTail := 0.0
|
||||
for _, k := range order[pass:] {
|
||||
naturalTail += natural[k]
|
||||
}
|
||||
for _, k := range order[pass:] {
|
||||
result[k] = natural[k] / naturalTail * remaining
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// prepareRow wraps each cell's inline spans and returns a tableRowData.
|
||||
// If bold is true, all cell text is forced bold (used for the header row).
|
||||
func (r *IHKRenderer) prepareRow(rawCells [][]InlineSpan, numCols int, colW, lineHt float64, bold bool) tableRowData {
|
||||
func (r *IHKRenderer) prepareRow(rawCells [][]InlineSpan, numCols int, colWidths []float64, lineHt float64, bold bool) tableRowData {
|
||||
cells := make([][][]InlineSpan, numCols)
|
||||
maxLines := 0
|
||||
for j := 0; j < numCols; j++ {
|
||||
@@ -239,7 +335,11 @@ func (r *IHKRenderer) prepareRow(rawCells [][]InlineSpan, numCols int, colW, lin
|
||||
if j < len(rawCells) {
|
||||
spans = rawCells[j]
|
||||
}
|
||||
lines := r.wrapCellSpans(spans, colW-2, bold)
|
||||
cw := 0.0
|
||||
if j < len(colWidths) {
|
||||
cw = colWidths[j]
|
||||
}
|
||||
lines := r.wrapCellSpans(spans, cw-2, bold)
|
||||
cells[j] = lines
|
||||
if len(lines) > maxLines {
|
||||
maxLines = len(lines)
|
||||
@@ -254,7 +354,15 @@ func (r *IHKRenderer) prepareRow(rawCells [][]InlineSpan, numCols int, colW, lin
|
||||
// drawRow renders a pre-computed row at the current Y position.
|
||||
// Cell borders are drawn as rectangles so all cells share a uniform height.
|
||||
// Header cells are rendered bold and centred; body cells honour per-span formatting.
|
||||
func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float64, isHeader bool) {
|
||||
func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colWidths []float64, lineHt float64, isHeader bool) {
|
||||
// Disable fpdf's auto page break for the duration of the row so that
|
||||
// CellFormat calls inside a row can never trigger a mid-row page break.
|
||||
// (Rect already doesn't trigger page breaks, but CellFormat does — this
|
||||
// makes the two consistent and prevents row-content from appearing on the
|
||||
// wrong page relative to the row background.)
|
||||
r.pdf.SetAutoPageBreak(false, 0)
|
||||
defer r.pdf.SetAutoPageBreak(true, dinMarginBottom)
|
||||
|
||||
startY := r.pdf.GetY()
|
||||
lm, _, _, _ := r.pdf.GetMargins()
|
||||
|
||||
@@ -264,14 +372,25 @@ func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float6
|
||||
r.pdf.SetFillColor(255, 255, 255)
|
||||
}
|
||||
|
||||
// Precompute cumulative x positions for each column.
|
||||
xPos := make([]float64, numCols+1)
|
||||
xPos[0] = lm
|
||||
for j, cw := range colWidths {
|
||||
xPos[j+1] = xPos[j] + cw
|
||||
}
|
||||
|
||||
// Draw all cell backgrounds and borders first (uniform height via Rect).
|
||||
for j := 0; j < numCols; j++ {
|
||||
r.pdf.Rect(lm+float64(j)*colW, startY, colW, row.height, "FD")
|
||||
r.pdf.Rect(xPos[j], startY, colWidths[j], row.height, "FD")
|
||||
}
|
||||
|
||||
// Render text on top, line by line per cell.
|
||||
for j, cellLines := range row.cells {
|
||||
x := lm + float64(j)*colW
|
||||
x := xPos[j]
|
||||
cw := 0.0
|
||||
if j < len(colWidths) {
|
||||
cw = colWidths[j]
|
||||
}
|
||||
for k, spanLine := range cellLines {
|
||||
y := startY + float64(k)*lineHt
|
||||
if isHeader {
|
||||
@@ -282,7 +401,7 @@ func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float6
|
||||
}
|
||||
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
||||
r.pdf.SetXY(x+1, y)
|
||||
r.pdf.CellFormat(colW-2, lineHt, r.tr(plainText), "", 0, "C", false, 0, "")
|
||||
r.pdf.CellFormat(cw-2, lineHt, r.tr(plainText), "", 0, "C", false, 0, "")
|
||||
} else {
|
||||
// Body: render each span with its own font, left-aligned.
|
||||
r.pdf.SetXY(x+1, y)
|
||||
@@ -330,17 +449,17 @@ func (r *IHKRenderer) renderTableBody(data [][][]InlineSpan, label string) {
|
||||
}
|
||||
|
||||
uw := r.usableWidth()
|
||||
colW := uw / float64(numCols)
|
||||
colWidths := r.computeColWidths(data, numCols, uw)
|
||||
lineHt := dinLineHtCaption + 2
|
||||
|
||||
_, pageH := r.pdf.GetPageSize()
|
||||
_, _, _, bm := r.pdf.GetMargins()
|
||||
|
||||
// Pre-compute all rows so we know heights before rendering.
|
||||
headerRow := r.prepareRow(header, numCols, colW, lineHt, true)
|
||||
headerRow := r.prepareRow(header, numCols, colWidths, lineHt, true)
|
||||
bodyRows := make([]tableRowData, len(data)-1)
|
||||
for i := 1; i < len(data); i++ {
|
||||
bodyRows[i-1] = r.prepareRow(data[i], numCols, colW, lineHt, false)
|
||||
bodyRows[i-1] = r.prepareRow(data[i], numCols, colWidths, lineHt, false)
|
||||
}
|
||||
|
||||
renderHeader := func(continued bool) {
|
||||
@@ -352,7 +471,24 @@ func (r *IHKRenderer) renderTableBody(data [][][]InlineSpan, label string) {
|
||||
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
||||
r.pdf.CellFormat(0, lineHt, r.tr(title), "", 1, "L", false, 0, "")
|
||||
}
|
||||
r.drawRow(headerRow, numCols, colW, lineHt, true)
|
||||
r.drawRow(headerRow, numCols, colWidths, lineHt, true)
|
||||
}
|
||||
|
||||
// Ensure label + header + at least the first body row fit on the current page
|
||||
// before starting to render. Without this check, drawRow can start rendering
|
||||
// the header near the bottom of the page and the background rect (drawn by
|
||||
// Rect, which doesn't trigger auto page break) ends up on a different page
|
||||
// from the cell text (drawn by CellFormat, which used to trigger page breaks).
|
||||
labelH := 0.0
|
||||
if label != "" {
|
||||
labelH = lineHt
|
||||
}
|
||||
minH := labelH + headerRow.height
|
||||
if len(bodyRows) > 0 {
|
||||
minH += bodyRows[0].height
|
||||
}
|
||||
if r.pdf.GetY()+minH > pageH-bm {
|
||||
r.pdf.AddPage()
|
||||
}
|
||||
|
||||
renderHeader(false)
|
||||
@@ -362,7 +498,7 @@ func (r *IHKRenderer) renderTableBody(data [][][]InlineSpan, label string) {
|
||||
r.pdf.AddPage()
|
||||
renderHeader(true)
|
||||
}
|
||||
r.drawRow(row, numCols, colW, lineHt, false)
|
||||
r.drawRow(row, numCols, colWidths, lineHt, false)
|
||||
}
|
||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user