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:
Generated
+2
@@ -2,5 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/realProjektarbeit" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/realProjektarbeit/Code" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
-3
@@ -4,12 +4,16 @@
|
||||
<option name="autoReloadType" value="ALL" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="c64f46d5-641a-468c-8fc1-94edec1f2deb" name="Changes" comment="Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration.">
|
||||
<list default="true" id="c64f46d5-641a-468c-8fc1-94edec1f2deb" name="Changes" comment="Refactor table rendering: replace plain text with `InlineSpan` for rich text support, update row preparation, and improve PDF formatting logic.">
|
||||
<change afterPath="$PROJECT_DIR$/real_report.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/MarkdownToIHKChemnits" beforeDir="false" afterPath="$PROJECT_DIR$/MarkdownToIHKChemnits" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/markdown_parser.go" beforeDir="false" afterPath="$PROJECT_DIR$/markdown_parser.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pdf_content.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_content.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pdf_pages.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_pages.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pdf_renderer.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_renderer.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/projektarbeit.pdf" beforeDir="false" afterPath="$PROJECT_DIR$/projektarbeit.pdf" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -17,7 +21,7 @@
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="EmbeddingIndexingInfo">
|
||||
<option name="cachedIndexableFilesCount" value="23" />
|
||||
<option name="cachedIndexableFilesCount" value="25" />
|
||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||
</component>
|
||||
<component name="GOROOT" url="file:///usr/lib/go" />
|
||||
@@ -101,7 +105,8 @@
|
||||
<MESSAGE value="Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic." />
|
||||
<MESSAGE value="Update MarkdownToIHKChemnitz: modify core functionality for improved PDF rendering" />
|
||||
<MESSAGE value="Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration." />
|
||||
<MESSAGE value="Refactor table rendering: replace plain text with `InlineSpan` for rich text support, update row preparation, and improve PDF formatting logic." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Refactor table rendering: replace plain text with `InlineSpan` for rich text support, update row preparation, and improve PDF formatting logic." />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
|
||||
Binary file not shown.
@@ -1,9 +1,14 @@
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/yuin/goldmark v1.8.1 h1:id2TeYXe5FpqwLco0Pso4cNM5Z6Okt4g7kDw9QBMhTA=
|
||||
github.com/yuin/goldmark v1.8.1/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
|
||||
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
+27
-3
@@ -46,6 +46,7 @@ func ParseMarkdown(mdPath string) (Config, ast.Node, []byte, error) {
|
||||
type parserState struct {
|
||||
nextCodeIsAppendix bool
|
||||
nextAppendixLandscape bool // set by @AnhangUMLQuer: — landscape for diagram appendix
|
||||
nextAppendixRotated bool // set by @AnhangUMLGedreht: — portrait page, image rotated 90° CCW
|
||||
appendixTitle string
|
||||
nextCodeBlockAppendix bool // set by @AnhangCode: — next non-diagram code block → appendix
|
||||
codeBlockAppendixTitle string
|
||||
@@ -122,6 +123,7 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
|
||||
state.nextDiagramCaption = ""
|
||||
state.nextCodeIsAppendix = false
|
||||
state.nextAppendixLandscape = false
|
||||
state.nextAppendixRotated = false
|
||||
}
|
||||
if err == nil {
|
||||
switch {
|
||||
@@ -130,15 +132,24 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
|
||||
state.nextDiagramLandscape = false
|
||||
state.nextDiagramCaption = ""
|
||||
case state.nextCodeIsAppendix:
|
||||
if state.nextAppendixLandscape {
|
||||
switch {
|
||||
case state.nextAppendixLandscape:
|
||||
r.AddLandscapeAppendix(state.appendixTitle + " | " + imgPath)
|
||||
state.nextAppendixLandscape = false
|
||||
} else {
|
||||
case state.nextAppendixRotated:
|
||||
r.AddRotatedUMLAppendix(state.appendixTitle, imgPath)
|
||||
state.nextAppendixRotated = false
|
||||
default:
|
||||
r.AddAppendix(state.appendixTitle + " | " + imgPath)
|
||||
}
|
||||
state.nextCodeIsAppendix = false
|
||||
default:
|
||||
r.RenderImage(imgPath, "Diagram ("+lang+")")
|
||||
caption := state.nextDiagramCaption
|
||||
if caption == "" {
|
||||
caption = "Diagram (" + lang + ")"
|
||||
}
|
||||
state.nextDiagramCaption = ""
|
||||
r.RenderImage(imgPath, caption)
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
@@ -287,6 +298,15 @@ func handleDirectives(text string, state *parserState, r *IHKRenderer) bool {
|
||||
state.nextAppendixLandscape = true
|
||||
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUMLQuer:"))
|
||||
handled = true
|
||||
case strings.HasPrefix(line, "@AnhangUMLGedreht:"):
|
||||
// Portrait page, image rotated 90° CCW — long axis runs top-to-bottom.
|
||||
state.nextCodeIsAppendix = true
|
||||
state.nextAppendixRotated = true
|
||||
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUMLGedreht:"))
|
||||
handled = true
|
||||
case strings.HasPrefix(line, "@AnhangBildGedreht:"):
|
||||
r.AddRotatedImageAppendix(strings.TrimSpace(strings.TrimPrefix(line, "@AnhangBildGedreht:")))
|
||||
handled = true
|
||||
case strings.HasPrefix(line, "@AnhangUML:"):
|
||||
state.nextCodeIsAppendix = true
|
||||
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUML:"))
|
||||
@@ -307,6 +327,10 @@ func handleDirectives(text string, state *parserState, r *IHKRenderer) bool {
|
||||
state.nextDiagramLandscape = true
|
||||
state.nextDiagramCaption = strings.TrimSpace(strings.TrimPrefix(line, "@DiagrammQuer:"))
|
||||
handled = true
|
||||
case strings.HasPrefix(line, "@Diagramm:"):
|
||||
// Portrait inline diagram — rendered at current position via RenderImage.
|
||||
state.nextDiagramCaption = strings.TrimSpace(strings.TrimPrefix(line, "@Diagramm:"))
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
return handled
|
||||
|
||||
+147
-11
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -30,9 +31,20 @@ 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() {
|
||||
// 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)
|
||||
}
|
||||
|
||||
+96
-2
@@ -2,7 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/go-pdf/fpdf"
|
||||
@@ -142,7 +147,7 @@ func (r *IHKRenderer) RenderAppendices() {
|
||||
r.pdf.SetFont("Helvetica", "", dinFontBody)
|
||||
for i, app := range r.appendices {
|
||||
r.pdf.CellFormat(0, dinLineHtBody,
|
||||
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
|
||||
r.tr(fmt.Sprintf("Anlage %s: %s", toRoman(i+1), app.Title)), "", 1, "L", false, 0, "")
|
||||
}
|
||||
|
||||
// Save portrait dimensions and standard margins for post-landscape restoration.
|
||||
@@ -164,12 +169,16 @@ func (r *IHKRenderer) RenderAppendices() {
|
||||
|
||||
r.pdf.SetFont("Helvetica", "B", dinFontBody)
|
||||
r.pdf.CellFormat(0, dinLineHtBody,
|
||||
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
|
||||
r.tr(fmt.Sprintf("Anlage %s: %s", toRoman(i+1), app.Title)), "", 1, "L", false, 0, "")
|
||||
r.pdf.Ln(dinSpaceAfterHeading)
|
||||
|
||||
switch app.Kind {
|
||||
case AppendixKindImage:
|
||||
if app.Rotated {
|
||||
r.renderAppendixImageRotated(app.Path)
|
||||
} else {
|
||||
r.renderAppendixImage(app.Path)
|
||||
}
|
||||
case AppendixKindTable:
|
||||
r.renderTableBody(app.TableData, "")
|
||||
case AppendixKindCode:
|
||||
@@ -215,6 +224,91 @@ func (r *IHKRenderer) renderAppendixImage(path string) {
|
||||
fpdf.ImageOptions{ReadDpi: true}, 0, "")
|
||||
}
|
||||
|
||||
// renderAppendixImageRotated places an image on a portrait page rotated 90° CW.
|
||||
// The image is pre-rotated in memory so that a wide (landscape-ratio) diagram
|
||||
// fills the page height. The reader tilts the page 90° CW to read it normally.
|
||||
func (r *IHKRenderer) renderAppendixImageRotated(path string) {
|
||||
rotatedPath, err := rotateImageCW(path)
|
||||
if err != nil {
|
||||
log.Printf("warning: could not rotate image %q: %v — falling back to normal", path, err)
|
||||
r.renderAppendixImage(path)
|
||||
return
|
||||
}
|
||||
|
||||
r.renderAppendixImageWithPath(rotatedPath)
|
||||
}
|
||||
|
||||
// rotateImageCW decodes a PNG/JPEG from disk, rotates it 90° clockwise, writes
|
||||
// the result to a temp file, and returns the temp file path.
|
||||
// The caller owns the temp file (it is left on disk; fpdf reads it lazily).
|
||||
func rotateImageCW(src string) (string, error) {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b := img.Bounds()
|
||||
// 90° CW: new width = old height, new height = old width.
|
||||
rotated := image.NewRGBA(image.Rect(0, 0, b.Max.Y-b.Min.Y, b.Max.X-b.Min.X))
|
||||
draw.Draw(rotated, rotated.Bounds(), image.White, image.Point{}, draw.Src)
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
// 90° CW: new(newW-1-y, x) = old(x, y)
|
||||
rotated.Set(b.Max.Y-1-y, x-b.Min.X, img.At(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "ihk_rotated_*.png")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tmp.Close()
|
||||
|
||||
if err := png.Encode(tmp, rotated); err != nil {
|
||||
os.Remove(tmp.Name())
|
||||
return "", err
|
||||
}
|
||||
return tmp.Name(), nil
|
||||
}
|
||||
|
||||
// renderAppendixImageWithPath is identical to renderAppendixImage but accepts
|
||||
// an arbitrary path (used for pre-rotated temp files).
|
||||
func (r *IHKRenderer) renderAppendixImageWithPath(path string) {
|
||||
info := r.ensureImageRegistered(path)
|
||||
if info == nil {
|
||||
r.pdf.CellFormat(0, dinLineHtBody, r.tr("[Image could not be loaded: "+path+"]"),
|
||||
"1", 1, "C", false, 0, "")
|
||||
return
|
||||
}
|
||||
|
||||
_, pageH := r.pdf.GetPageSize()
|
||||
lm, _, _, bm := r.pdf.GetMargins()
|
||||
uw := r.usableWidth()
|
||||
availH := pageH - r.pdf.GetY() - bm - 10
|
||||
|
||||
imgW := info.Width() * ptToMM
|
||||
imgH := info.Height() * ptToMM
|
||||
|
||||
displayW := uw
|
||||
displayH := imgH * (displayW / imgW)
|
||||
if displayH > availH {
|
||||
scale := availH / displayH
|
||||
displayH = availH
|
||||
displayW = displayW * scale
|
||||
}
|
||||
|
||||
posX := lm + (uw-displayW)/2
|
||||
r.pdf.ImageOptions(path, posX, r.pdf.GetY(), displayW, displayH, false,
|
||||
fpdf.ImageOptions{ReadDpi: true}, 0, "")
|
||||
r.pdf.SetY(r.pdf.GetY() + displayH + 2)
|
||||
}
|
||||
|
||||
// RenderAbbreviations renders the list of abbreviations from the YAML config.
|
||||
// It is placed after the TOC and uses Roman page numbering.
|
||||
func (r *IHKRenderer) RenderAbbreviations() {
|
||||
|
||||
@@ -55,6 +55,7 @@ const (
|
||||
type Appendix struct {
|
||||
Kind AppendixKind
|
||||
Landscape bool // true → render on a landscape A4 page (15 mm symmetric margins)
|
||||
Rotated bool // true → portrait page, image rotated 90° CCW so long axis runs top-to-bottom
|
||||
Title string
|
||||
Path string // image path (Kind == AppendixKindImage)
|
||||
TableData [][][]InlineSpan // table rows (Kind == AppendixKindTable)
|
||||
@@ -194,6 +195,35 @@ func (r *IHKRenderer) AddCodeAppendix(title, lang, code string) {
|
||||
})
|
||||
}
|
||||
|
||||
// AddRotatedImageAppendix registers an image annex rendered on a portrait A4 page
|
||||
// with the image rotated 90° CCW, so a wide (landscape-ratio) diagram fills the
|
||||
// full page height when the reader tilts the page 90° clockwise.
|
||||
// Format: "Title | /path/to/image"
|
||||
func (r *IHKRenderer) AddRotatedImageAppendix(titlePath string) {
|
||||
parts := strings.SplitN(titlePath, "|", 2)
|
||||
if len(parts) < 2 {
|
||||
log.Printf("warning: @AnhangBildGedreht directive missing '|' separator: %q", titlePath)
|
||||
return
|
||||
}
|
||||
r.appendices = append(r.appendices, Appendix{
|
||||
Kind: AppendixKindImage,
|
||||
Rotated: true,
|
||||
Title: strings.TrimSpace(parts[0]),
|
||||
Path: strings.TrimSpace(parts[1]),
|
||||
})
|
||||
}
|
||||
|
||||
// AddRotatedUMLAppendix registers a UML/diagram annex (code block) rendered on a
|
||||
// portrait A4 page with the diagram image rotated 90° CCW.
|
||||
func (r *IHKRenderer) AddRotatedUMLAppendix(title, imgPath string) {
|
||||
r.appendices = append(r.appendices, Appendix{
|
||||
Kind: AppendixKindImage,
|
||||
Rotated: true,
|
||||
Title: title,
|
||||
Path: imgPath,
|
||||
})
|
||||
}
|
||||
|
||||
// AddLandscapeAppendix registers an image annex in "Title | /path/to/image" format
|
||||
// that will be rendered on a landscape A4 page with 15 mm symmetric margins.
|
||||
func (r *IHKRenderer) AddLandscapeAppendix(titlePath string) {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user