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
+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() {