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:
Sebastian Unterschütz
2026-05-17 23:41:45 +02:00
parent 427372b82b
commit d6b3854681
9 changed files with 318 additions and 22 deletions
Generated
+2
View File
@@ -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>
+8 -3
View File
@@ -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.
+5
View File
@@ -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
View File
@@ -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
+149 -13
View File
@@ -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)
}
+97 -3
View File
@@ -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:
r.renderAppendixImage(app.Path)
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() {
+30
View File
@@ -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) {
BIN
View File
Binary file not shown.