Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic.

This commit is contained in:
Sebastian Unterschütz
2026-05-12 21:44:37 +02:00
parent 436cdcc516
commit 67f9d63f24
12 changed files with 1025 additions and 221 deletions
+108 -4
View File
@@ -2,6 +2,7 @@ package main
import (
"fmt"
"log"
"sort"
"github.com/go-pdf/fpdf"
@@ -126,8 +127,8 @@ func (r *IHKRenderer) RenderBibliography() {
}
// RenderAppendices renders the appendix section followed by individual annex
// pages. The opening page contains the index of all annexes. Each annex image
// is scaled to fill the available page area.
// pages. The opening page contains the index of all annexes. Annexes marked
// Landscape=true are placed on a landscape A4 page with 15 mm symmetric margins.
func (r *IHKRenderer) RenderAppendices() {
if len(r.appendices) == 0 {
return
@@ -144,14 +145,41 @@ func (r *IHKRenderer) RenderAppendices() {
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
}
// Save portrait dimensions and standard margins for post-landscape restoration.
portraitW, portraitH := r.pdf.GetPageSize()
lm, tm, rm, _ := r.pdf.GetMargins()
const diagMargin = 15.0
// Individual annex pages
for i, app := range r.appendices {
r.pdf.AddPage()
if app.Landscape {
r.pdf.SetMargins(diagMargin, diagMargin, diagMargin)
r.pdf.SetAutoPageBreak(true, diagMargin)
// portraitH > portraitW → {Wd: portraitH, Ht: portraitW} = landscape 297×210.
r.pdf.AddPageFormat("L", fpdf.SizeType{Wd: portraitH, Ht: portraitW})
} else {
r.pdf.AddPage()
}
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.pdf.Ln(dinSpaceAfterHeading)
r.renderAppendixImage(app.Path)
switch app.Kind {
case AppendixKindImage:
r.renderAppendixImage(app.Path)
case AppendixKindTable:
// Render without numbering/recording; the annex header already identifies the table.
r.renderTableBody(app.TableData, "")
}
if app.Landscape {
// Restore standard margins so the next AddPage() returns to portrait.
r.pdf.SetMargins(lm, tm, rm)
r.pdf.SetAutoPageBreak(true, dinMarginBottom)
}
}
}
@@ -234,6 +262,79 @@ func (r *IHKRenderer) RenderGlossary() {
}
}
// RenderLandscapeDiagram renders an image on a dedicated landscape A4 page,
// scaled to fill the full printable area. A figure caption is added below the
// image and recorded in the Abbildungsverzeichnis.
//
// The page uses symmetric 15 mm margins (no Korrekturrand — examiners do not
// mark up diagram pages). After rendering, a fresh portrait page is opened so
// subsequent content continues in the correct orientation.
func (r *IHKRenderer) RenderLandscapeDiagram(path, caption string) {
// Current dimensions before the landscape page (portrait A4: 210 × 297 mm).
pw, ph := r.pdf.GetPageSize()
lm, tm, rm, _ := r.pdf.GetMargins()
const diagMargin = 15.0
// Apply symmetric margins before adding the landscape page so the page
// inherits them. The Korrekturrand (40 mm) is not needed on a diagram page.
r.pdf.SetMargins(diagMargin, diagMargin, diagMargin)
r.pdf.SetAutoPageBreak(true, diagMargin)
// ph > pw for portrait A4 → {Wd: ph, Ht: pw} = landscape 297 × 210 mm.
r.pdf.AddPageFormat("L", fpdf.SizeType{Wd: ph, Ht: pw})
captionH := dinLineHtCaption + 4
availW := ph - 2*diagMargin // 297 30 = 267 mm
availH := pw - 2*diagMargin - captionH - 2 // 210 30 ~10 ≈ 170 mm
info := r.ensureImageRegistered(path)
if info == nil {
log.Printf("warning: landscape diagram image not found: %q", path)
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
r.pdf.CellFormat(0, dinLineHtCaption,
r.tr("[Bild nicht gefunden: "+path+"]"), "1", 1, "C", false, 0, "")
} else {
imgW := info.Width() * ptToMM
imgH := info.Height() * ptToMM
// Scale to fill available width, clamp to available height.
displayW := availW
displayH := imgH * (displayW / imgW)
if displayH > availH {
displayH = availH
displayW = imgW * (displayH / imgH)
if displayW > availW {
displayW = availW
displayH = imgH * (displayW / imgW)
}
}
posX := diagMargin + (availW-displayW)/2
posY := diagMargin
r.pdf.ImageOptions(path, posX, posY, displayW, displayH, false,
fpdf.ImageOptions{ReadDpi: true}, 0, "")
r.pdf.SetY(posY + displayH + 2)
r.figureCount++
label := fmt.Sprintf("Abb. %d", r.figureCount)
if caption != "" {
label += ": " + caption
}
r.RecordFigure(label)
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
r.pdf.CellFormat(0, captionH, r.tr(label), "", 1, "C", false, 0, "")
}
// Restore portrait margins and open a fresh portrait page.
// AddPage() always uses the default orientation ("P") set at construction,
// so subsequent content is guaranteed to be in portrait.
r.pdf.SetMargins(lm, tm, rm)
r.pdf.SetAutoPageBreak(true, dinMarginBottom)
r.pdf.AddPage()
}
// ensureImageRegistered registers an image with fpdf if not already known
// and returns its metadata. Returns nil if the image cannot be loaded.
func (r *IHKRenderer) ensureImageRegistered(path string) *fpdf.ImageInfoType {
@@ -242,6 +343,9 @@ func (r *IHKRenderer) ensureImageRegistered(path string) *fpdf.ImageInfoType {
r.pdf.RegisterImageOptions(path, fpdf.ImageOptions{ReadDpi: true})
info = r.pdf.GetImageInfo(path)
}
if info == nil {
log.Printf("warning: could not load image %q", path)
}
return info
}