diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..13cadbb 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,7 @@
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 25b9f81..380e643 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,12 +4,16 @@
-
+
+
+
+
+
@@ -17,7 +21,7 @@
-
+
@@ -101,7 +105,8 @@
-
+
+
diff --git a/MarkdownToIHKChemnits b/MarkdownToIHKChemnits
index 09bc7cc..a75a15c 100755
Binary files a/MarkdownToIHKChemnits and b/MarkdownToIHKChemnits differ
diff --git a/go.sum b/go.sum
index 1ab40c3..cbc4352 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/markdown_parser.go b/markdown_parser.go
index 34c5fd3..945b74c 100644
--- a/markdown_parser.go
+++ b/markdown_parser.go
@@ -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
diff --git a/pdf_content.go b/pdf_content.go
index 3eab56d..6e71a36 100644
--- a/pdf_content.go
+++ b/pdf_content.go
@@ -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)
}
diff --git a/pdf_pages.go b/pdf_pages.go
index 5b7cbfd..1cd32dd 100644
--- a/pdf_pages.go
+++ b/pdf_pages.go
@@ -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() {
diff --git a/pdf_renderer.go b/pdf_renderer.go
index c9c1828..37f4c60 100644
--- a/pdf_renderer.go
+++ b/pdf_renderer.go
@@ -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) {
diff --git a/projektarbeit.pdf b/projektarbeit.pdf
index 32b0903..ea22a52 100644
Binary files a/projektarbeit.pdf and b/projektarbeit.pdf differ