Files
MarkdownToIHKChemnits/pdf_renderer.go

550 lines
14 KiB
Go

package main
import (
"fmt"
"github.com/go-pdf/fpdf"
"sort"
"strconv"
"strings"
)
type NumberingType int
const (
NumNone NumberingType = iota
NumRoman
NumArabic
)
type TOCItem struct {
Level int
Title string
PageStr string
}
type TableItem struct {
Title string
PageStr string
}
type Appendix struct {
Title string
Path string
}
type IHKRenderer struct {
pdf *fpdf.Fpdf
config Config
numType NumberingType
tocItems []TOCItem
tableItems []TableItem
sources []string
appendices []Appendix
pageOffset int
tableCount int
tr func(string) string
}
func NewIHKRenderer(config Config) *IHKRenderer {
pdf := fpdf.New("P", "mm", "A4", "")
// Margins in mm: Top 20, Bottom 25, Left 30, Right 40
pdf.SetMargins(30, 20, 40)
pdf.SetAutoPageBreak(true, 25)
r := &IHKRenderer{
pdf: pdf,
config: config,
numType: NumNone,
tocItems: make([]TOCItem, 0),
tableItems: make([]TableItem, 0),
sources: make([]string, 0),
appendices: make([]Appendix, 0),
tr: pdf.UnicodeTranslatorFromDescriptor(""),
}
pdf.SetFooterFunc(func() {
if r.numType == NumNone {
return
}
pdf.SetY(-15)
pdf.SetFont("Helvetica", "", 10)
var pageStr string
if r.numType == NumRoman {
pageStr = toRoman(pdf.PageNo())
} else {
// Arabic numbering starts at 1 for the main body
displayPage := pdf.PageNo() - r.pageOffset
if displayPage <= 0 {
return
}
pageStr = strconv.Itoa(displayPage)
}
pdf.CellFormat(0, 10, pageStr, "", 0, "C", false, 0, "")
})
return r
}
func (r *IHKRenderer) RenderTOC() {
r.numType = NumRoman
r.pdf.AddPage()
r.pdf.SetFont("Helvetica", "B", 14)
r.pdf.CellFormat(0, 20, r.tr("Inhaltsverzeichnis"), "", 1, "L", false, 0, "")
r.pdf.Ln(5)
// TOC Header as a table
totalWidth, _ := r.pdf.GetPageSize()
lm, _, rm, _ := r.pdf.GetMargins()
usableWidth := totalWidth - lm - rm
r.pdf.SetFont("Helvetica", "B", 12)
r.pdf.CellFormat(usableWidth-20, 10, r.tr("Inhalt"), "", 0, "L", false, 0, "")
r.pdf.CellFormat(20, 10, r.tr("Seite"), "", 1, "R", false, 0, "")
r.pdf.Ln(2)
r.pdf.SetFont("Helvetica", "", 12)
_, lineHt := r.pdf.GetFontSize()
lineHt *= 1.5
for _, item := range r.tocItems {
indent := float64((item.Level - 1) * 10)
r.pdf.SetX(lm + indent)
title := r.tr(item.Title)
pageStr := item.PageStr
titleWidth := r.pdf.GetStringWidth(title)
pageWidth := r.pdf.GetStringWidth(pageStr)
// Available width for dots
availableWidth := usableWidth - indent - titleWidth - pageWidth - 4
r.pdf.CellFormat(titleWidth+2, lineHt, title, "", 0, "L", false, 0, "")
if availableWidth > 0 {
dots := ""
dotWidth := r.pdf.GetStringWidth(".")
for i := 0; float64(i)*dotWidth < availableWidth; i++ {
dots += "."
}
r.pdf.CellFormat(availableWidth, lineHt, dots, "", 0, "L", false, 0, "")
}
r.pdf.CellFormat(pageWidth+2, lineHt, pageStr, "", 1, "R", false, 0, "")
}
}
func (r *IHKRenderer) RecordHeader(level int, title string) {
if r.numType != NumArabic {
return // Exclude front matter from TOC per IHK example
}
displayPage := r.pdf.PageNo() - r.pageOffset
pageStr := strconv.Itoa(displayPage)
if displayPage <= 0 {
pageStr = "1" // Fallback
}
r.tocItems = append(r.tocItems, TOCItem{
Level: level,
Title: title,
PageStr: pageStr,
})
}
func (r *IHKRenderer) RecordTable(title string) {
displayPage := r.pdf.PageNo() - r.pageOffset
pageStr := strconv.Itoa(displayPage)
if displayPage <= 0 {
pageStr = "1"
}
r.tableItems = append(r.tableItems, TableItem{
Title: title,
PageStr: pageStr,
})
}
func (r *IHKRenderer) RenderListOfTables() {
if len(r.tableItems) == 0 {
return
}
r.pdf.AddPage()
r.pdf.SetFont("Helvetica", "B", 14)
r.pdf.CellFormat(0, 20, r.tr("Tabellenverzeichnis"), "", 1, "L", false, 0, "")
r.pdf.Ln(5)
totalWidth, _ := r.pdf.GetPageSize()
lm, _, rm, _ := r.pdf.GetMargins()
usableWidth := totalWidth - lm - rm
r.pdf.SetFont("Helvetica", "B", 12)
r.pdf.CellFormat(usableWidth-20, 10, r.tr("Titel"), "", 0, "L", false, 0, "")
r.pdf.CellFormat(20, 10, r.tr("Seite"), "", 1, "R", false, 0, "")
r.pdf.Ln(2)
r.pdf.SetFont("Helvetica", "", 12)
_, lineHt := r.pdf.GetFontSize()
lineHt *= 1.5
for _, item := range r.tableItems {
title := r.tr(item.Title)
pageStr := item.PageStr
titleWidth := r.pdf.GetStringWidth(title)
pageWidth := r.pdf.GetStringWidth(pageStr)
availableWidth := usableWidth - titleWidth - pageWidth - 4
r.pdf.CellFormat(titleWidth+2, lineHt, title, "", 0, "L", false, 0, "")
if availableWidth > 0 {
dots := ""
dotWidth := r.pdf.GetStringWidth(".")
for i := 0; float64(i)*dotWidth < availableWidth; i++ {
dots += "."
}
r.pdf.CellFormat(availableWidth, lineHt, dots, "", 0, "L", false, 0, "")
}
r.pdf.CellFormat(pageWidth+2, lineHt, pageStr, "", 1, "R", false, 0, "")
}
}
func (r *IHKRenderer) RenderTitlePage() {
r.numType = NumNone
// Temporary symmetrical margins for title page to ensure visual centering
oldLM, oldTM, oldRM, _ := r.pdf.GetMargins()
r.pdf.SetMargins(30, 20, 30)
r.pdf.AddPage()
r.pdf.SetFont("Helvetica", "B", 16)
r.pdf.CellFormat(0, 20, r.tr("Abschlussprüfung zum ..."), "", 1, "C", false, 0, "")
r.pdf.CellFormat(0, 10, r.tr(r.config.Student.Profession), "", 1, "C", false, 0, "")
r.pdf.Ln(30)
r.pdf.SetFont("Helvetica", "", 12)
r.pdf.CellFormat(0, 10, r.tr("Projektarbeit von"), "", 1, "C", false, 0, "")
r.pdf.Ln(5)
r.pdf.SetFont("Helvetica", "B", 14)
r.pdf.CellFormat(0, 10, r.tr(r.config.Student.Name), "", 1, "C", false, 0, "")
r.pdf.Ln(30)
r.pdf.SetFont("Helvetica", "B", 16)
r.pdf.MultiCell(0, 10, r.tr(r.config.Project.Title), "", "C", false)
r.pdf.SetY(-80)
r.pdf.SetFont("Helvetica", "", 12)
r.pdf.CellFormat(60, 10, r.tr("Prüfungsperiode:"), "", 0, "L", false, 0, "")
r.pdf.CellFormat(0, 10, r.tr(r.config.Project.Period), "", 1, "L", false, 0, "")
r.pdf.CellFormat(60, 10, r.tr("Ausbildungsbetrieb:"), "", 0, "L", false, 0, "")
r.pdf.CellFormat(0, 10, r.tr(r.config.Student.Company), "", 1, "L", false, 0, "")
r.pdf.CellFormat(60, 10, r.tr("Projektbetreuer:"), "", 0, "L", false, 0, "")
r.pdf.CellFormat(0, 10, r.tr(r.config.Student.Supervisor), "", 1, "L", false, 0, "")
// Restore margins for the rest of the document
r.pdf.SetMargins(oldLM, oldTM, oldRM)
}
func (r *IHKRenderer) RenderDeclarationPage() {
r.numType = NumNone
r.pdf.AddPage()
r.pdf.SetFont("Helvetica", "B", 14)
r.pdf.CellFormat(0, 20, r.tr("Erklärung"), "", 1, "C", false, 0, "")
r.pdf.Ln(10)
r.pdf.SetFont("Helvetica", "", 12)
text := fmt.Sprintf("Ich versichere durch meine Unterschrift, dass ich diese Projektarbeit mit dem Thema „%s“ selbstständig, ohne fremde Hilfe angefertigt, alle Stellen, die ich wörtlich oder annähernd wörtlich aus Veröffentlichungen entnommen, als solche kenntlich gemacht und mich auch keiner anderen als der angegebenen Literatur oder sonstiger Hilfsmittel bedient habe. Die Projektarbeit hat in dieser oder ähnlicher Form weder der Industrie- und Handelskammer Chemnitz noch einer anderen Prüfungsinstitution vorgelegen.", r.config.Project.Title)
r.pdf.MultiCell(0, 7.5, r.tr(text), "", "J", false)
r.pdf.Ln(20)
r.pdf.CellFormat(0, 10, r.tr("Ort, Datum, Unterschrift (mit Vor- und Nachnamen)"), "", 1, "L", false, 0, "")
}
func (r *IHKRenderer) RenderHeader(level int, title string) {
if level == 1 {
r.pdf.AddPage() // New page for major sections? Maybe optional.
}
r.RecordHeader(level, title)
r.pdf.SetFont("Helvetica", "B", 14)
r.pdf.Ln(5)
r.pdf.CellFormat(0, 10, r.tr(title), "", 1, "L", false, 0, "")
r.pdf.Ln(2)
}
func (r *IHKRenderer) RenderParagraph(text string) {
r.pdf.SetFont("Helvetica", "", 12)
// Line height for 12pt is 12pt * 1.5 = 18pt.
// 18pt is approx 6.35mm.
r.pdf.MultiCell(0, 7.5, r.tr(text), "", "J", false)
r.pdf.Ln(4)
}
func (r *IHKRenderer) RenderListItem(text string, bullet bool, index int) {
r.pdf.SetFont("Helvetica", "", 12)
prefix := "• "
if !bullet {
prefix = strconv.Itoa(index) + ". "
}
currentX := r.pdf.GetX()
r.pdf.SetX(currentX + 10)
r.pdf.MultiCell(0, 7.5, r.tr(prefix+text), "", "J", false)
r.pdf.SetX(currentX)
}
func (r *IHKRenderer) RenderTable(data [][]string) {
if len(data) == 0 {
return
}
r.tableCount++
tableTitle := fmt.Sprintf("Tab. %d", r.tableCount)
r.RecordTable(tableTitle)
r.pdf.SetFont("Helvetica", "B", 10)
header := data[0]
// Calculate column widths
numCols := len(header)
totalWidth, _ := r.pdf.GetPageSize()
lm, _, rm, _ := r.pdf.GetMargins()
usableWidth := totalWidth - lm - rm
colWidth := usableWidth / float64(numCols)
// Function to render header with optional "(Fortsetzung)"
renderHeader := func(continued bool) {
r.pdf.SetFont("Helvetica", "B", 10)
title := tableTitle
if continued {
title += " (Fortsetzung)"
}
r.pdf.CellFormat(0, 10, r.tr(title), "", 1, "L", false, 0, "")
r.pdf.SetFillColor(230, 230, 230)
for _, col := range header {
r.pdf.CellFormat(colWidth, 10, r.tr(col), "1", 0, "C", true, 0, "")
}
r.pdf.Ln(-1)
}
renderHeader(false)
r.pdf.SetFont("Helvetica", "", 10)
r.pdf.SetFillColor(255, 255, 255)
for i := 1; i < len(data); i++ {
row := data[i]
// Check for page break
_, pageH := r.pdf.GetPageSize()
_, _, _, bm := r.pdf.GetMargins()
if r.pdf.GetY()+10 > pageH-bm {
r.pdf.AddPage()
renderHeader(true)
r.pdf.SetFont("Helvetica", "", 10)
}
for _, col := range row {
r.pdf.CellFormat(colWidth, 10, r.tr(col), "1", 0, "L", false, 0, "")
}
r.pdf.Ln(-1)
}
r.pdf.Ln(5)
}
func (r *IHKRenderer) RenderImage(path string, caption string) {
r.pdf.Ln(5)
info := r.pdf.GetImageInfo(path)
if info == nil {
// Try to register it first
r.pdf.RegisterImageOptions(path, fpdf.ImageOptions{ReadDpi: true})
info = r.pdf.GetImageInfo(path)
}
if info == nil {
r.pdf.CellFormat(0, 10, r.tr("[Fehler beim Laden des Bildes: "+path+"]"), "1", 1, "C", false, 0, "")
return
}
// Calculate dimensions in mm
// 1 point = 0.352778 mm
imgW := info.Width() * 0.352778
imgH := info.Height() * 0.352778
maxWidth := 140.0
displayW := imgW
displayH := imgH
if displayW > maxWidth {
ratio := maxWidth / displayW
displayW = maxWidth
displayH = displayH * ratio
}
// Check if we need a new page
_, pageH := r.pdf.GetPageSize()
_, _, _, bottomMargin := r.pdf.GetMargins()
currentY := r.pdf.GetY()
if currentY+displayH+15 > pageH-bottomMargin {
r.pdf.AddPage()
currentY = r.pdf.GetY()
}
// Center horizontally
posX := 30.0 + (maxWidth-displayW)/2
r.pdf.ImageOptions(path, posX, currentY, displayW, displayH, false, fpdf.ImageOptions{ReadDpi: true}, 0, "")
// Move Y after the image
r.pdf.SetY(currentY + displayH + 2)
if caption != "" {
r.pdf.SetFont("Helvetica", "I", 10)
r.pdf.CellFormat(0, 10, r.tr(caption), "", 1, "C", false, 0, "")
}
r.pdf.Ln(5)
}
func (r *IHKRenderer) StartMainBody() {
r.numType = NumArabic
// The current page is the last Roman page
// So the next page will be display page 1
r.pageOffset = r.pdf.PageNo()
}
func (r *IHKRenderer) StartFrontMatter() {
r.numType = NumRoman
}
func (r *IHKRenderer) RenderBibliography() {
if len(r.sources) == 0 {
return
}
// RenderHeader(1, ...) already adds a page
r.RenderHeader(1, "Literaturverzeichnis")
r.pdf.Ln(5)
r.pdf.SetFont("Helvetica", "", 12)
// IHK Rule 2.8.1: Sort by author name
sort.Strings(r.sources)
for _, source := range r.sources {
r.pdf.MultiCell(0, 7.5, r.tr("- "+source), "", "J", false)
r.pdf.Ln(2)
}
}
func (r *IHKRenderer) RenderAppendices() {
if len(r.appendices) == 0 {
return
}
// RenderHeader(1, ...) already adds a page
r.RenderHeader(1, "Anhang")
r.pdf.Ln(5)
r.pdf.SetFont("Helvetica", "B", 12)
r.pdf.CellFormat(0, 10, r.tr("Anlagenverzeichnis"), "", 1, "L", false, 0, "")
r.pdf.SetFont("Helvetica", "", 12)
for i, app := range r.appendices {
r.pdf.CellFormat(0, 10, r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
}
for i, app := range r.appendices {
r.pdf.AddPage()
r.pdf.SetFont("Helvetica", "B", 12)
r.pdf.CellFormat(0, 10, r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
r.pdf.Ln(5)
info := r.pdf.GetImageInfo(app.Path)
if info == nil {
r.pdf.RegisterImageOptions(app.Path, fpdf.ImageOptions{ReadDpi: true})
info = r.pdf.GetImageInfo(app.Path)
}
if info != nil {
imgW := info.Width() * 0.352778
imgH := info.Height() * 0.352778
maxWidth := 140.0
// Scale to full width
displayW := maxWidth
ratio := displayW / imgW
displayH := imgH * ratio
// Check if it fits on page height, if not, scale down
_, pageH := r.pdf.GetPageSize()
_, _, _, bottomMargin := r.pdf.GetMargins()
availableH := pageH - r.pdf.GetY() - bottomMargin - 10
if displayH > availableH {
ratio := availableH / displayH
displayH = availableH
displayW = displayW * ratio
}
posX := 30.0 + (140.0-displayW)/2
r.pdf.ImageOptions(app.Path, posX, r.pdf.GetY(), displayW, displayH, false, fpdf.ImageOptions{ReadDpi: true}, 0, "")
} else {
r.pdf.CellFormat(0, 10, r.tr("[Bild konnte nicht geladen werden]"), "1", 1, "C", false, 0, "")
}
}
}
func (r *IHKRenderer) AddAppendix(titlePath string) {
parts := strings.Split(titlePath, "|")
if len(parts) < 2 {
return
}
title := strings.TrimSpace(parts[0])
path := strings.TrimSpace(parts[1])
r.appendices = append(r.appendices, Appendix{Title: title, Path: path})
}
func (r *IHKRenderer) AddSource(source string) {
// Normalize and store
source = strings.TrimSpace(source)
if source == "" {
return
}
// Prevent duplicates
for _, s := range r.sources {
if s == source {
return
}
}
r.sources = append(r.sources, source)
}
func (r *IHKRenderer) Save(filename string) error {
return r.pdf.OutputFileAndClose(filename)
}
func toRoman(n int) string {
if n <= 0 {
return ""
}
values := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
symbols := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}
res := ""
for i := 0; i < len(values); i++ {
for n >= values[i] {
n -= values[i]
res += symbols[i]
}
}
return res
}