package main import ( "fmt" "log" "sort" "github.com/go-pdf/fpdf" ) // RenderTitlePage renders the cover page without a page number. // Layout follows the IHK Chemnitz template (section 2.2 of the guidelines). func (r *IHKRenderer) RenderTitlePage() { r.numType = NumNone r.pdf.AddPage() // Use symmetrical margins on the title page for visual centering. r.pdf.SetLeftMargin(dinMarginLeft) r.pdf.SetRightMargin(dinMarginLeft) defer func() { r.pdf.SetLeftMargin(dinMarginLeft) r.pdf.SetRightMargin(dinMarginRight) }() // Exam type and profession r.pdf.SetFont("Helvetica", "B", 16) r.pdf.CellFormat(0, 12, r.tr("Abschlussprüfung zum"), "", 1, "C", false, 0, "") r.pdf.CellFormat(0, 12, r.tr(r.config.Student.Profession), "", 1, "C", false, 0, "") r.pdf.Ln(20) r.pdf.SetFont("Helvetica", "", dinFontBody) r.pdf.CellFormat(0, dinLineHtBody, r.tr("Projektarbeit von"), "", 1, "C", false, 0, "") r.pdf.Ln(4) // Candidate name — bold, 14 pt r.pdf.SetFont("Helvetica", "B", dinFontHeading) r.pdf.CellFormat(0, dinLineHtHeading, r.tr(r.config.Student.Name), "", 1, "C", false, 0, "") r.pdf.Ln(20) // Project title — bold, 16 pt, centred, word-wrapped r.pdf.SetFont("Helvetica", "B", 16) r.pdf.MultiCell(0, 10, r.tr(r.config.Project.Title), "", "C", false) if r.config.Project.Subtitle != "" { r.pdf.Ln(4) r.pdf.SetFont("Helvetica", "I", dinFontBody) r.pdf.MultiCell(0, dinLineHtBody, r.tr(r.config.Project.Subtitle), "", "C", false) } // Bottom block: exam period, training company, supervisor r.pdf.SetY(-80) r.pdf.SetFont("Helvetica", "", dinFontBody) labelW := 60.0 r.pdf.CellFormat(labelW, dinLineHtBody, r.tr("Prüfungsperiode:"), "", 0, "L", false, 0, "") r.pdf.MultiCell(0, dinLineHtBody, r.tr(r.config.Project.Period), "", "L", false) r.pdf.CellFormat(labelW, dinLineHtBody, r.tr("Ausbildungsbetrieb:"), "", 0, "L", false, 0, "") r.pdf.MultiCell(0, dinLineHtBody, r.tr(r.config.Student.Company), "", "L", false) r.pdf.CellFormat(labelW, dinLineHtBody, r.tr("Projektbetreuer:"), "", 0, "L", false, 0, "") r.pdf.MultiCell(0, dinLineHtBody, r.tr(r.config.Student.Supervisor), "", "L", false) } // RenderDeclarationPage renders the declaration of authenticity without a page // number, as required by IHK Chemnitz (section 2.12 of the guidelines). // This page must be physically inserted in every submitted copy. func (r *IHKRenderer) RenderDeclarationPage() { r.numType = NumNone r.pdf.AddPage() r.pdf.SetFont("Helvetica", "B", dinFontHeading) r.pdf.CellFormat(0, dinLineHtHeading+4, r.tr("Erklärung"), "", 1, "C", false, 0, "") r.pdf.Ln(dinSpaceAfterHeading) // Legally required declaration text — must remain in German. r.pdf.SetFont("Helvetica", "", dinFontBody) declaration := 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, dinLineHtBody, r.tr(declaration), "", "J", false) r.pdf.Ln(dinLineHtBody) additional := "Mir ist bekannt, dass gemäß der Prüfungsordnung für die Durchführung" + " von Abschlussprüfungen der Industrie- und Handelskammer Chemnitz" + " Täuschungshandlungen zum Ausschluss von der Prüfung führen können und" + " die Prüfung als nicht bestanden erklärt werden kann." r.pdf.MultiCell(0, dinLineHtBody, r.tr(additional), "", "J", false) r.pdf.Ln(20) r.pdf.CellFormat(0, dinLineHtBody, r.tr("Ort, Datum, Unterschrift (mit Vor- und Nachnamen)"), "", 1, "L", false, 0, "") r.pdf.Ln(4) // Signature line r.pdf.SetDrawColor(0, 0, 0) x, y := r.pdf.GetXY() r.pdf.Line(x, y, x+80, y) } // RenderBibliography renders the bibliography sorted alphabetically per IHK // rule 2.8.1. Skipped when no @Quelle directives were found in the document. func (r *IHKRenderer) RenderBibliography() { if len(r.sources) == 0 { return } r.RenderHeader(1, "Literaturverzeichnis") sorted := make([]string, len(r.sources)) copy(sorted, r.sources) sort.Strings(sorted) r.pdf.SetFont("Helvetica", "", dinFontBody) for _, source := range sorted { r.pdf.MultiCell(0, dinLineHtBody, r.tr("– "+source), "", "J", false) r.pdf.Ln(2) } } // RenderAppendices renders the appendix section followed by individual annex // pages. The opening page contains the index of all annexes. Annexes marked // Landscape=true are placed on a landscape A4 page with 15 mm symmetric margins. func (r *IHKRenderer) RenderAppendices() { if len(r.appendices) == 0 { return } r.RenderHeader(1, "Anhang") // Annex index r.pdf.SetFont("Helvetica", "B", dinFontBody) r.pdf.CellFormat(0, dinLineHtBody, r.tr("Anlagenverzeichnis"), "", 1, "L", false, 0, "") r.pdf.Ln(dinSpaceAfterHeading) r.pdf.SetFont("Helvetica", "", dinFontBody) for i, app := range r.appendices { r.pdf.CellFormat(0, dinLineHtBody, r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "") } // Save portrait dimensions and standard margins for post-landscape restoration. portraitW, portraitH := r.pdf.GetPageSize() lm, tm, rm, _ := r.pdf.GetMargins() const diagMargin = 15.0 // Individual annex pages for i, app := range r.appendices { if app.Landscape { r.pdf.SetMargins(diagMargin, diagMargin, diagMargin) r.pdf.SetAutoPageBreak(true, diagMargin) // portraitH > portraitW → {Wd: portraitH, Ht: portraitW} = landscape 297×210. r.pdf.AddPageFormat("L", fpdf.SizeType{Wd: portraitH, Ht: portraitW}) } else { r.pdf.AddPage() } r.pdf.SetFont("Helvetica", "B", dinFontBody) r.pdf.CellFormat(0, dinLineHtBody, r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "") r.pdf.Ln(dinSpaceAfterHeading) switch app.Kind { case AppendixKindImage: r.renderAppendixImage(app.Path) case AppendixKindTable: // Render without numbering/recording; the annex header already identifies the table. r.renderTableBody(app.TableData, "") } if app.Landscape { // Restore standard margins so the next AddPage() returns to portrait. r.pdf.SetMargins(lm, tm, rm) r.pdf.SetAutoPageBreak(true, dinMarginBottom) } } } // renderAppendixImage scales and embeds an image to fill the remaining page area. func (r *IHKRenderer) renderAppendixImage(path string) { info := r.ensureImageRegistered(path) if info == nil { r.pdf.CellFormat(0, dinLineHtBody, r.tr("[Image could not be loaded: "+path+"]"), "1", 1, "C", false, 0, "") return } _, pageH := r.pdf.GetPageSize() lm, _, _, bm := r.pdf.GetMargins() uw := r.usableWidth() availH := pageH - r.pdf.GetY() - bm - 10 imgW := info.Width() * ptToMM imgH := info.Height() * ptToMM // Scale to fill available width, then clamp to available height. displayW := uw displayH := imgH * (displayW / imgW) if displayH > availH { scale := availH / displayH displayH = availH displayW = displayW * scale } posX := lm + (uw-displayW)/2 r.pdf.ImageOptions(path, posX, r.pdf.GetY(), displayW, displayH, false, fpdf.ImageOptions{ReadDpi: true}, 0, "") } // RenderAbbreviations renders the list of abbreviations from the YAML config. // It is placed after the TOC and uses Roman page numbering. func (r *IHKRenderer) RenderAbbreviations() { if len(r.config.Abbreviations) == 0 { return } r.pdf.AddPage() r.RecordHeader(1, "Abkürzungsverzeichnis") r.pdf.SetFont("Helvetica", "B", dinFontHeading) r.pdf.CellFormat(0, dinLineHtHeading+4, r.tr("Abkürzungsverzeichnis"), "", 1, "L", false, 0, "") r.pdf.Ln(dinSpaceAfterHeading) uw := r.usableWidth() abbrColW := 40.0 meaningColW := uw - abbrColW // Header row with grey fill r.pdf.SetFillColor(230, 230, 230) r.pdf.SetFont("Helvetica", "B", dinFontBody) r.pdf.CellFormat(abbrColW, dinLineHtBody, r.tr("Abkürzung"), "1", 0, "L", true, 0, "") r.pdf.CellFormat(meaningColW, dinLineHtBody, r.tr("Bedeutung"), "1", 1, "L", true, 0, "") r.pdf.SetFillColor(255, 255, 255) r.pdf.SetFont("Helvetica", "", dinFontBody) for _, a := range r.config.Abbreviations { r.pdf.CellFormat(abbrColW, dinLineHtBody, r.tr(a.Abbr), "1", 0, "L", false, 0, "") r.pdf.CellFormat(meaningColW, dinLineHtBody, r.tr(a.Meaning), "1", 1, "L", false, 0, "") } } // RenderGlossary renders the glossary from the YAML config. // It is placed after the appendices and uses Arabic page numbering. func (r *IHKRenderer) RenderGlossary() { if len(r.config.Glossary) == 0 { return } r.RenderHeader(1, "Glossar") for _, entry := range r.config.Glossary { r.pdf.SetFont("Helvetica", "B", dinFontBody) r.pdf.CellFormat(0, dinLineHtBody, r.tr(entry.Term), "", 1, "L", false, 0, "") r.pdf.SetFont("Helvetica", "", dinFontBody) r.pdf.MultiCell(0, dinLineHtBody, r.tr(entry.Definition), "", "J", false) r.pdf.Ln(4) } } // RenderLandscapeDiagram renders an image on a dedicated landscape A4 page, // scaled to fill the full printable area. A figure caption is added below the // image and recorded in the Abbildungsverzeichnis. // // The page uses symmetric 15 mm margins (no Korrekturrand — examiners do not // mark up diagram pages). After rendering, a fresh portrait page is opened so // subsequent content continues in the correct orientation. func (r *IHKRenderer) RenderLandscapeDiagram(path, caption string) { // Current dimensions before the landscape page (portrait A4: 210 × 297 mm). pw, ph := r.pdf.GetPageSize() lm, tm, rm, _ := r.pdf.GetMargins() const diagMargin = 15.0 // Apply symmetric margins before adding the landscape page so the page // inherits them. The Korrekturrand (40 mm) is not needed on a diagram page. r.pdf.SetMargins(diagMargin, diagMargin, diagMargin) r.pdf.SetAutoPageBreak(true, diagMargin) // ph > pw for portrait A4 → {Wd: ph, Ht: pw} = landscape 297 × 210 mm. r.pdf.AddPageFormat("L", fpdf.SizeType{Wd: ph, Ht: pw}) captionH := dinLineHtCaption + 4 availW := ph - 2*diagMargin // 297 − 30 = 267 mm availH := pw - 2*diagMargin - captionH - 2 // 210 − 30 − ~10 ≈ 170 mm info := r.ensureImageRegistered(path) if info == nil { log.Printf("warning: landscape diagram image not found: %q", path) r.pdf.SetFont("Helvetica", "I", dinFontCaption) r.pdf.CellFormat(0, dinLineHtCaption, r.tr("[Bild nicht gefunden: "+path+"]"), "1", 1, "C", false, 0, "") } else { imgW := info.Width() * ptToMM imgH := info.Height() * ptToMM // Scale to fill available width, clamp to available height. displayW := availW displayH := imgH * (displayW / imgW) if displayH > availH { displayH = availH displayW = imgW * (displayH / imgH) if displayW > availW { displayW = availW displayH = imgH * (displayW / imgW) } } posX := diagMargin + (availW-displayW)/2 posY := diagMargin r.pdf.ImageOptions(path, posX, posY, displayW, displayH, false, fpdf.ImageOptions{ReadDpi: true}, 0, "") r.pdf.SetY(posY + displayH + 2) 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, "") } // Restore portrait margins and open a fresh portrait page. // AddPage() always uses the default orientation ("P") set at construction, // so subsequent content is guaranteed to be in portrait. r.pdf.SetMargins(lm, tm, rm) r.pdf.SetAutoPageBreak(true, dinMarginBottom) r.pdf.AddPage() } // ensureImageRegistered registers an image with fpdf if not already known // and returns its metadata. Returns nil if the image cannot be loaded. func (r *IHKRenderer) ensureImageRegistered(path string) *fpdf.ImageInfoType { info := r.pdf.GetImageInfo(path) if info == nil { r.pdf.RegisterImageOptions(path, fpdf.ImageOptions{ReadDpi: true}) info = r.pdf.GetImageInfo(path) } if info == nil { log.Printf("warning: could not load image %q", path) } return info } // ptToMM converts PDF points to millimetres (1 pt = 25.4 / 72 mm). const ptToMM = 25.4 / 72.0