Files
MarkdownToIHKChemnits/pdf_content.go
T

270 lines
7.1 KiB
Go

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 ""
}
}