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 }