Remove outdated toc_pages.txt, add new Go modules for IHK Chemnitz PDF rendering including diagrams, tables, and TOC functionality.
This commit is contained in:
+269
@@ -0,0 +1,269 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-pdf/fpdf"
|
||||
)
|
||||
|
||||
// InlineSpan represents a run of text with consistent inline formatting.
|
||||
// Used to pass parsed inline Markdown nodes from the parser to the renderer.
|
||||
type InlineSpan struct {
|
||||
Text string
|
||||
Bold bool
|
||||
Italic bool
|
||||
Code bool // inline code (`…`)
|
||||
}
|
||||
|
||||
// RenderHeader renders a section heading per IHK/DIN 5008 rules:
|
||||
// - Font: Helvetica Bold, 14pt
|
||||
// - Before: 2 blank lines if not at the top of the page
|
||||
// - After: 1 blank line
|
||||
// - Level 1 headings always start on a new page
|
||||
//
|
||||
// The heading is also recorded in the TOC.
|
||||
func (r *IHKRenderer) RenderHeader(level int, title string) {
|
||||
if level == 1 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
r.RecordHeader(level, title)
|
||||
|
||||
r.pdf.SetFont("Helvetica", "B", dinFontHeading)
|
||||
r.pdf.MultiCell(0, dinLineHtHeading, r.tr(title), "", "L", false)
|
||||
// IHK: one blank line (Leerzeile) after every heading.
|
||||
r.pdf.Ln(dinSpaceAfterHeading)
|
||||
}
|
||||
|
||||
// RenderParagraphSpans renders a paragraph from pre-parsed inline spans.
|
||||
//
|
||||
// Plain paragraphs (no inline formatting) use MultiCell with Blocksatz (justified)
|
||||
// alignment as required by IHK. Paragraphs with mixed bold/italic use Write()
|
||||
// which produces left-aligned output — an inherent fpdf limitation for mixed fonts.
|
||||
func (r *IHKRenderer) RenderParagraphSpans(spans []InlineSpan) {
|
||||
if len(spans) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hasFormatting := false
|
||||
for _, s := range spans {
|
||||
if s.Bold || s.Italic || s.Code {
|
||||
hasFormatting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFormatting {
|
||||
// All plain text: collect and render justified (Blocksatz).
|
||||
text := ""
|
||||
for _, s := range spans {
|
||||
text += s.Text
|
||||
}
|
||||
r.pdf.SetFont("Helvetica", "", dinFontBody)
|
||||
r.pdf.MultiCell(0, dinLineHtBody, r.tr(text), "", "J", false)
|
||||
} else {
|
||||
// Mixed formatting: render span-by-span using Write().
|
||||
for _, span := range spans {
|
||||
style := fontStyle(span.Bold, span.Italic)
|
||||
if span.Code {
|
||||
r.pdf.SetFont("Courier", style, dinFontBody)
|
||||
} else {
|
||||
r.pdf.SetFont("Helvetica", style, dinFontBody)
|
||||
}
|
||||
r.pdf.Write(dinLineHtBody, r.tr(span.Text))
|
||||
}
|
||||
r.pdf.Ln(dinLineHtBody)
|
||||
}
|
||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||
}
|
||||
|
||||
// RenderListItem renders a single list item with a bullet prefix.
|
||||
// ordered: true → bullet "•"; false → numbered with index.
|
||||
// indent: nesting depth (0 = top level).
|
||||
func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, indent int) {
|
||||
r.pdf.SetFont("Helvetica", "", dinFontBody)
|
||||
|
||||
lm, _, _, _ := r.pdf.GetMargins()
|
||||
indentMM := float64(indent+1) * 8.0
|
||||
r.pdf.SetX(lm + indentMM)
|
||||
|
||||
prefix := "• "
|
||||
if ordered {
|
||||
prefix = strconv.Itoa(index) + ". "
|
||||
}
|
||||
|
||||
hasFormatting := false
|
||||
for _, s := range spans {
|
||||
if s.Bold || s.Italic || s.Code {
|
||||
hasFormatting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
uw := r.usableWidth() - indentMM
|
||||
if !hasFormatting {
|
||||
text := prefix
|
||||
for _, s := range spans {
|
||||
text += s.Text
|
||||
}
|
||||
r.pdf.MultiCell(uw, dinLineHtBody, r.tr(text), "", "L", false)
|
||||
} else {
|
||||
r.pdf.Write(dinLineHtBody, r.tr(prefix))
|
||||
for _, span := range spans {
|
||||
style := fontStyle(span.Bold, span.Italic)
|
||||
if span.Code {
|
||||
r.pdf.SetFont("Courier", style, dinFontBody)
|
||||
} else {
|
||||
r.pdf.SetFont("Helvetica", style, dinFontBody)
|
||||
}
|
||||
r.pdf.Write(dinLineHtBody, r.tr(span.Text))
|
||||
}
|
||||
r.pdf.Ln(dinLineHtBody)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderTable renders a data table with a header row (grey background) and
|
||||
// a auto-repeat header on page breaks. Each table is numbered and recorded
|
||||
// in the Tabellenverzeichnis per IHK section 2.4.
|
||||
//
|
||||
// data[0] is the header row; data[1:] are body rows.
|
||||
func (r *IHKRenderer) RenderTable(data [][]string, caption string) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
r.tableCount++
|
||||
label := fmt.Sprintf("Tab. %d", r.tableCount)
|
||||
if caption != "" {
|
||||
label += ": " + caption
|
||||
}
|
||||
r.RecordTable(label)
|
||||
|
||||
header := data[0]
|
||||
numCols := len(header)
|
||||
uw := r.usableWidth()
|
||||
colW := uw / float64(numCols)
|
||||
|
||||
// renderHeaderRow draws the (grey) header row, optionally with a "(Fortsetzung)" suffix.
|
||||
renderHeaderRow := func(continued bool) {
|
||||
title := label
|
||||
if continued {
|
||||
title += " (Fortsetzung)"
|
||||
}
|
||||
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
||||
r.pdf.CellFormat(0, dinLineHtCaption+2, r.tr(title), "", 1, "L", false, 0, "")
|
||||
|
||||
r.pdf.SetFillColor(230, 230, 230)
|
||||
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
||||
for _, col := range header {
|
||||
r.pdf.CellFormat(colW, dinLineHtCaption+2, r.tr(col), "1", 0, "C", true, 0, "")
|
||||
}
|
||||
r.pdf.Ln(-1)
|
||||
}
|
||||
|
||||
renderHeaderRow(false)
|
||||
|
||||
r.pdf.SetFont("Helvetica", "", dinFontCaption)
|
||||
r.pdf.SetFillColor(255, 255, 255)
|
||||
|
||||
_, pageH := r.pdf.GetPageSize()
|
||||
_, _, _, bm := r.pdf.GetMargins()
|
||||
rowH := dinLineHtCaption + 2
|
||||
|
||||
for i := 1; i < len(data); i++ {
|
||||
if r.pdf.GetY()+rowH > pageH-bm {
|
||||
r.pdf.AddPage()
|
||||
renderHeaderRow(true)
|
||||
r.pdf.SetFont("Helvetica", "", dinFontCaption)
|
||||
}
|
||||
for _, col := range data[i] {
|
||||
r.pdf.CellFormat(colW, rowH, r.tr(col), "1", 0, "L", false, 0, "")
|
||||
}
|
||||
r.pdf.Ln(-1)
|
||||
}
|
||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||
}
|
||||
|
||||
// RenderImage embeds an image with a caption below it.
|
||||
// The image is scaled to fit the printable width and the remaining page height.
|
||||
// If the image does not fit on the current page, a new page is started.
|
||||
// The caption is recorded in the Abbildungsverzeichnis.
|
||||
func (r *IHKRenderer) RenderImage(path string, caption string) {
|
||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||
|
||||
info := r.ensureImageRegistered(path)
|
||||
if info == nil {
|
||||
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
|
||||
r.pdf.CellFormat(0, dinLineHtCaption, r.tr("[Bild nicht gefunden: "+path+"]"),
|
||||
"1", 1, "C", false, 0, "")
|
||||
return
|
||||
}
|
||||
|
||||
imgW := info.Width() * ptToMM
|
||||
imgH := info.Height() * ptToMM
|
||||
|
||||
uw := r.usableWidth()
|
||||
captionH := dinLineHtCaption + 4
|
||||
|
||||
// Scale to available width
|
||||
displayW := uw
|
||||
displayH := imgH * (displayW / imgW)
|
||||
|
||||
_, pageH := r.pdf.GetPageSize()
|
||||
_, _, _, bm := r.pdf.GetMargins()
|
||||
availH := pageH - r.pdf.GetY() - bm - captionH
|
||||
|
||||
if displayH > availH {
|
||||
if availH < 20 {
|
||||
// Almost no space left — start a new page
|
||||
r.pdf.AddPage()
|
||||
availH = pageH - r.pdf.GetY() - bm - captionH
|
||||
}
|
||||
// Scale down to fit height
|
||||
displayH = availH
|
||||
displayW = imgW * (displayH / imgH)
|
||||
if displayW > uw {
|
||||
displayW = uw
|
||||
displayH = imgH * (displayW / imgW)
|
||||
}
|
||||
}
|
||||
|
||||
lm, _, _, _ := r.pdf.GetMargins()
|
||||
posX := lm + (uw-displayW)/2
|
||||
currentY := r.pdf.GetY()
|
||||
|
||||
r.pdf.ImageOptions(path, posX, currentY, displayW, displayH, false,
|
||||
fpdf.ImageOptions{ReadDpi: true}, 0, "")
|
||||
r.pdf.SetY(currentY + displayH + 2)
|
||||
|
||||
// Figure caption and TOC registration
|
||||
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, "")
|
||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||
}
|
||||
|
||||
// fontStyle returns the fpdf font style string for a given bold/italic combination.
|
||||
func fontStyle(bold, italic bool) string {
|
||||
switch {
|
||||
case bold && italic:
|
||||
return "BI"
|
||||
case bold:
|
||||
return "B"
|
||||
case italic:
|
||||
return "I"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user