172 lines
5.3 KiB
Go
172 lines
5.3 KiB
Go
// 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 (
|
||
"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
|
||
)
|
||
|
||
// Appendix holds the title and image path for one annex entry.
|
||
type Appendix struct {
|
||
Title string
|
||
Path string
|
||
}
|
||
|
||
// 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 annex entry in "Title | /path/to/image" format.
|
||
func (r *IHKRenderer) AddAppendix(titlePath string) {
|
||
parts := strings.SplitN(titlePath, "|", 2)
|
||
if len(parts) < 2 {
|
||
return
|
||
}
|
||
r.appendices = append(r.appendices, Appendix{
|
||
Title: strings.TrimSpace(parts[0]),
|
||
Path: strings.TrimSpace(parts[1]),
|
||
})
|
||
}
|
||
|
||
// 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
|
||
}
|