Files
MarkdownToIHKChemnits/pdf_content.go
T

350 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"strconv"
"strings"
"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()
// 3 mm base indent + 5 mm per nesting level (reduced from previous 8 mm per level)
indentMM := 3.0 + float64(indent)*5.0
prefix := "• "
if ordered {
prefix = strconv.Itoa(index) + ". "
}
prefixW := r.pdf.GetStringWidth(prefix) + 1.0
// textX is where the item text starts; wrapped lines must align here too.
textX := lm + indentMM + prefixW
textW := r.usableWidth() - indentMM - prefixW
// Temporarily move the left margin to textX so that MultiCell wraps
// continuation lines flush with the text, not back to the page margin.
r.pdf.SetLeftMargin(textX)
defer r.pdf.SetLeftMargin(lm)
// Render bullet/number; CellFormat advances X to textX automatically.
r.pdf.SetX(lm + indentMM)
r.pdf.CellFormat(prefixW, dinLineHtBody, r.tr(prefix), "", 0, "L", false, 0, "")
hasFormatting := false
for _, s := range spans {
if s.Bold || s.Italic || s.Code {
hasFormatting = true
break
}
}
if !hasFormatting {
text := ""
for _, s := range spans {
text += s.Text
}
r.pdf.MultiCell(textW, dinLineHtBody, r.tr(text), "", "L", false)
} else {
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)
}
// Code block layout constants.
const (
codeFont = 9.0 // pt — Courier, smaller than body text
codeLineHt = 4.5 // mm ≈ 9 pt × 1.3 line spacing
codeGutterW = 11.0 // mm — column reserved for line numbers
)
// RenderCodeBlock renders a fenced code block with a line-number gutter.
//
// Layout:
//
// ┌──────┬──────────────────────────────────┐
// │ 1 │ package main │
// │ 2 │ │
// │ 3 │ func main() { … } │
// └──────┴──────────────────────────────────┘
//
// The language label is shown in small italic text above the block.
// Lines that exceed the printable width are clipped — code should be
// formatted to reasonable lengths before conversion.
func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
lines := strings.Split(strings.TrimRight(code, "\n"), "\n")
if len(lines) == 0 {
return
}
r.pdf.Ln(dinSpaceAfterParagraph)
// Language label — top-right, italic, grey
if lang != "" {
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
r.pdf.SetTextColor(100, 100, 100)
r.pdf.CellFormat(0, dinLineHtCaption, r.tr(lang), "", 1, "R", false, 0, "")
r.pdf.SetTextColor(0, 0, 0)
}
lm, _, _, bm := r.pdf.GetMargins()
_, pageH := r.pdf.GetPageSize()
uw := r.usableWidth()
codeW := uw - codeGutterW
r.pdf.SetFont("Courier", "", codeFont)
for i, line := range lines {
// Start a new page if this line would fall below the bottom margin.
if r.pdf.GetY()+codeLineHt > pageH-bm {
r.pdf.AddPage()
}
// Gutter: darker grey, right-aligned line number.
r.pdf.SetFillColor(218, 218, 218)
r.pdf.SetX(lm)
r.pdf.CellFormat(codeGutterW, codeLineHt,
fmt.Sprintf("%4d ", i+1), "", 0, "R", true, 0, "")
// Code line: lighter grey, left-aligned, small leading space.
r.pdf.SetFillColor(246, 246, 246)
r.pdf.CellFormat(codeW, codeLineHt,
r.tr(" "+line), "", 1, "L", true, 0, "")
}
// Reset colours so subsequent content is unaffected.
r.pdf.SetFillColor(255, 255, 255)
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 ""
}
}