// Package main implements a Markdown-to-PDF converter that produces documents // compliant with IHK Chemnitz project documentation guidelines (Verordnung 2020) // and DIN 5008 formatting rules. package main import ( "log" "strconv" "strings" "github.com/go-pdf/fpdf" ) // IHK Chemnitz / DIN 5008 format constants. // Source: "Hinweise zur Erarbeitung der Dokumentation über die Projektarbeit", // IHK Chemnitz, Verordnung 2020. const ( // Page margins in mm dinMarginLeft = 30.0 // 3.0 cm left margin dinMarginRight = 40.0 // 4.0 cm right margin — Korrekturrand (examiner correction space) dinMarginTop = 20.0 // 2.0 cm top margin dinMarginBottom = 25.0 // 2.5 cm bottom margin // Font sizes in pt — Arial/Helvetica, black dinFontBody = 12.0 // body text dinFontHeading = 14.0 // headings, bold dinFontCaption = 10.0 // captions and footnotes // Line heights in mm — 1½ line spacing: pt × 1.5 × (25.4 / 72) dinLineHtBody = 6.35 // 12 pt × 1.5 = 18 pt = 6.35 mm dinLineHtHeading = 7.41 // 14 pt × 1.5 = 21 pt = 7.41 mm dinLineHtCaption = 5.29 // 10 pt × 1.5 = 15 pt = 5.29 mm // Vertical spacing in mm // IHK: two blank lines before a heading that is not at the top of the page; // one blank line after every heading. dinSpaceBeforeHeading = 2 * dinLineHtBody // ≈ 12.7 mm dinSpaceAfterHeading = dinLineHtBody // ≈ 6.35 mm dinSpaceAfterParagraph = 4.0 // between body paragraphs dinLineHtList = dinLineHtBody // 1.5× IHK standard, same as body text dinSpaceAfterList = 3.0 // gap inserted after the outermost list exits ) // AppendixKind distinguishes between image and table annexes. type AppendixKind int const ( AppendixKindImage AppendixKind = iota AppendixKindTable AppendixKindCode ) // Appendix holds the content for one annex entry. type Appendix struct { Kind AppendixKind Landscape bool // true → render on a landscape A4 page (15 mm symmetric margins) Title string Path string // image path (Kind == AppendixKindImage) TableData [][]string // table rows (Kind == AppendixKindTable) Lang string // language label (Kind == AppendixKindCode) Code string // source code (Kind == AppendixKindCode) } // IHKRenderer is the central PDF generator for IHK Chemnitz project documentation. // All DIN 5008 formatting rules are applied through its methods. // // Usage: create with NewIHKRenderer, call Render* methods in document order, // then call Save. The two-pass rendering in main.go fills the TOC correctly. type IHKRenderer struct { pdf *fpdf.Fpdf config Config numType NumberingType tocItems []TOCItem tableItems []TableItem figureItems []FigureItem sources []string appendices []Appendix // pageOffset is the PDF page number of the last Roman-numbered page. // Main body page 1 is rendered as PDF page (pageOffset + 1). pageOffset int tableCount int figureCount int // tr translates UTF-8 strings to the Latin-1 encoding used by fpdf. tr func(string) string } // NewIHKRenderer constructs a renderer pre-configured with DIN 5008 margins // and an auto-footer that renders the correct page number style per section. func NewIHKRenderer(config Config) *IHKRenderer { pdf := fpdf.New("P", "mm", "A4", "") pdf.SetMargins(dinMarginLeft, dinMarginTop, dinMarginRight) pdf.SetAutoPageBreak(true, dinMarginBottom) r := &IHKRenderer{ pdf: pdf, config: config, numType: NumNone, tocItems: make([]TOCItem, 0), tableItems: make([]TableItem, 0), figureItems: make([]FigureItem, 0), sources: make([]string, 0), appendices: make([]Appendix, 0), tr: pdf.UnicodeTranslatorFromDescriptor(""), } // IHK: page number centered at the bottom of every numbered page. pdf.SetFooterFunc(func() { if r.numType == NumNone { return } pdf.SetY(-15) pdf.SetFont("Helvetica", "", dinFontCaption) var pageStr string switch r.numType { case NumRoman: pageStr = toRoman(pdf.PageNo()) case NumArabic: dp := pdf.PageNo() - r.pageOffset if dp <= 0 { return } pageStr = strconv.Itoa(dp) } pdf.CellFormat(0, 10, pageStr, "", 0, "C", false, 0, "") }) return r } // StartFrontMatter activates Roman-numeral page numbering. // Must be called before adding the first front-matter page (i.e., before RenderTOC). // The title page (page 1) is unnumbered; the TOC appears as page II. func (r *IHKRenderer) StartFrontMatter() { r.numType = NumRoman } // StartMainBody switches to Arabic page numbering and captures the current PDF // page as the offset so that the first main-body page displays as "1". func (r *IHKRenderer) StartMainBody() { r.numType = NumArabic r.pageOffset = r.pdf.PageNo() } // AddSource registers a bibliography entry. Whitespace is trimmed; // duplicate entries are silently ignored. func (r *IHKRenderer) AddSource(source string) { source = strings.TrimSpace(source) if source == "" { return } for _, s := range r.sources { if s == source { return } } r.sources = append(r.sources, source) } // AddAppendix registers an image annex in "Title | /path/to/image" format. func (r *IHKRenderer) AddAppendix(titlePath string) { parts := strings.SplitN(titlePath, "|", 2) if len(parts) < 2 { log.Printf("warning: @Anhang directive missing '|' separator: %q", titlePath) return } r.appendices = append(r.appendices, Appendix{ Kind: AppendixKindImage, Title: strings.TrimSpace(parts[0]), Path: strings.TrimSpace(parts[1]), }) } // AddTableAppendix registers a table annex entry. func (r *IHKRenderer) AddTableAppendix(title string, data [][]string) { if len(data) == 0 { log.Printf("warning: @TabelleAnhang %q has no table data — skipped", title) return } r.appendices = append(r.appendices, Appendix{ Kind: AppendixKindTable, Title: title, TableData: data, }) } // AddCodeAppendix registers a source-code annex rendered with line-number gutter. func (r *IHKRenderer) AddCodeAppendix(title, lang, code string) { r.appendices = append(r.appendices, Appendix{ Kind: AppendixKindCode, Title: title, Lang: lang, Code: code, }) } // AddLandscapeAppendix registers an image annex in "Title | /path/to/image" format // that will be rendered on a landscape A4 page with 15 mm symmetric margins. func (r *IHKRenderer) AddLandscapeAppendix(titlePath string) { parts := strings.SplitN(titlePath, "|", 2) if len(parts) < 2 { log.Printf("warning: @AnhangBildQuer directive missing '|' separator: %q", titlePath) return } r.appendices = append(r.appendices, Appendix{ Kind: AppendixKindImage, Landscape: true, Title: strings.TrimSpace(parts[0]), Path: strings.TrimSpace(parts[1]), }) } // AddTableAppendixLandscape registers a table annex entry rendered on a landscape page. func (r *IHKRenderer) AddTableAppendixLandscape(title string, data [][]string) { if len(data) == 0 { log.Printf("warning: @TabelleAnhangQuer %q has no table data — skipped", title) return } r.appendices = append(r.appendices, Appendix{ Kind: AppendixKindTable, Landscape: true, Title: title, TableData: data, }) } // Save writes the completed PDF to the given file path. func (r *IHKRenderer) Save(filename string) error { return r.pdf.OutputFileAndClose(filename) } // usableWidth returns the printable line width on the current page in mm. func (r *IHKRenderer) usableWidth() float64 { w, _ := r.pdf.GetPageSize() lm, _, rm, _ := r.pdf.GetMargins() return w - lm - rm } // isAtPageTop returns true when the Y cursor sits at or within 1 mm of the // top margin, meaning no body content has been placed on this page yet. func (r *IHKRenderer) isAtPageTop() bool { _, tm, _, _ := r.pdf.GetMargins() return r.pdf.GetY() <= tm+1.0 }