550 lines
14 KiB
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
|
|
}
|