Files
MarkdownToIHKChemnits/pdf_renderer.go
T

245 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}