Skip to content

Commit e50c203

Browse files
committed
feat: pdf renderer refactoring
1 parent 5515bdf commit e50c203

15 files changed

Lines changed: 329 additions & 114 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ go 1.24.3
44

55
require (
66
github.com/lucasepe/x v0.7.1
7+
github.com/mattn/go-runewidth v0.0.16
78
github.com/signintech/gopdf v0.32.0
89
golang.org/x/image v0.27.0
910
)
1011

1112
require (
12-
github.com/mattn/go-runewidth v0.0.16 // indirect
1313
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
1414
github.com/pkg/errors v0.8.1 // indirect
1515
github.com/rivo/uniseg v0.4.7 // indirect

internal/parser/parser.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ func Parse(src io.Reader) (CheckList, error) {
5151

5252
case strings.HasPrefix(line, ">"):
5353
if currentItem == nil {
54-
return list, fmt.Errorf("note does not belong to any item: %q", line)
54+
if currentGroup == nil {
55+
return list, fmt.Errorf("note does not belong to any item: %q", line)
56+
}
57+
note := strings.TrimSpace(line[1:])
58+
currentGroup.Notes = append(currentGroup.Notes, note)
59+
continue
5560
}
5661
note := strings.TrimSpace(line[1:])
5762
currentItem.Notes = append(currentItem.Notes, note)

internal/parser/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type Item struct {
77

88
type Group struct {
99
Title string
10+
Notes []string
1011
Items []Item
1112
}
1213

internal/render/pdf/options.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,12 @@ type pdfRenderOptions struct {
3737
marginLeft float64
3838

3939
groupTitleFontSize float64
40-
groupTitleMargin float64
41-
42-
itemFontSize float64
43-
itemMargin float64
40+
groupNoteFontSize float64
4441

42+
itemFontSize float64
4543
itemNoteFontSize float64
46-
itemNoteMargin float64
4744

4845
documentTitleFontSize float64
49-
50-
lineSpacing float64
5146
}
5247

5348
func defaultOptions() pdfRenderOptions {

internal/render/pdf/render.go

Lines changed: 57 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"math"
66
"os"
77
"path/filepath"
8+
"strings"
89
"time"
910

1011
"github.com/lucasepe/checkit/internal/parser"
1112
"github.com/lucasepe/checkit/internal/render"
1213
"github.com/lucasepe/x/text/slugify"
14+
"github.com/mattn/go-runewidth"
1315
"github.com/signintech/gopdf"
1416
"golang.org/x/image/font/gofont/gomono"
1517
)
@@ -35,7 +37,7 @@ func New(opts ...RenderOption) (render.Renderer, error) {
3537
H: g.opts.pageHeight,
3638
},
3739
})
38-
err := g.doc.AddTTFFontData(fontName, gomono.TTF)
40+
err := g.doc.AddTTFFontData(defaultFontName, gomono.TTF)
3941
if err != nil {
4042
return g, err
4143
}
@@ -47,12 +49,10 @@ func New(opts ...RenderOption) (render.Renderer, error) {
4749
}
4850

4951
const (
50-
fontName = "GoMono"
51-
symbol = "\u25CB" // ○
52-
itemIndent = " " // Indent for wrapped lines
53-
itemNoteIndent = " " // Indent for wrapped lines
54-
55-
defaultCreator = "Check IT (github.com/lucasepe/checkit)"
52+
defaultFontName = "GoMono"
53+
defaultSymbol = " \u25CB " // ○
54+
defaultLineSpacingRatio = 0.5
55+
defaultCreator = "Check IT (github.com/lucasepe/checkit)"
5656
)
5757

5858
var _ render.Renderer = (*pdfRenderImpl)(nil)
@@ -83,6 +83,13 @@ func (g *pdfRenderImpl) Render(lst *parser.CheckList) (err error) {
8383
return err
8484
}
8585

86+
for _, el := range grp.Notes {
87+
y, err = g.handleGroupNote(y, el)
88+
if err != nil {
89+
return err
90+
}
91+
}
92+
8693
for _, it := range grp.Items {
8794
y, err = g.handleItem(y, it.Title)
8895
if err != nil {
@@ -105,12 +112,9 @@ func (g *pdfRenderImpl) setup() error {
105112
fontSize := 0.02 * math.Min(g.opts.pageWidth, g.opts.pageHeight)
106113

107114
g.opts.itemFontSize = fontSize
108-
g.opts.lineSpacing = 0.5 * fontSize
109-
g.opts.itemMargin = 0.8 * fontSize
110115
g.opts.itemNoteFontSize = 0.75 * fontSize
111-
g.opts.itemNoteMargin = 1.15 * (0.75 * fontSize)
112116
g.opts.groupTitleFontSize = fontSize + 4
113-
g.opts.groupTitleMargin = 1.1 * (fontSize + 4)
117+
g.opts.groupNoteFontSize = fontSize + 2
114118
g.opts.documentTitleFontSize = (fontSize + 4) + 4
115119

116120
return os.MkdirAll(g.outputDir, 0755)
@@ -135,130 +139,78 @@ func (g *pdfRenderImpl) setMeta(title string) {
135139
})
136140
}
137141

138-
func (g *pdfRenderImpl) handleGroup(y float64, title string) (float64, error) {
139-
if y+g.opts.groupTitleFontSize+g.opts.lineSpacing > g.opts.pageHeight-g.opts.marginBottom {
140-
g.doc.AddPage()
141-
g.pageCount++
142-
143-
y = g.opts.marginTop
144-
} else {
145-
y += g.opts.groupTitleMargin
146-
}
147-
148-
g.doc.SetTextColor(53, 57, 53)
149-
150-
g.doc.SetFont(fontName, "", g.opts.groupTitleFontSize)
151-
g.doc.SetX(g.opts.marginLeft)
152-
g.doc.SetY(y)
153-
g.doc.Text(title)
154-
155-
y += g.opts.groupTitleFontSize //+ 0.5*g.opts.groupTitleMargin
156-
157-
return y, nil
158-
}
159-
160142
func (g *pdfRenderImpl) handleDocumentTitle(y float64, title string) (float64, error) {
161143
g.filename = fmt.Sprintf("%s.pdf", slugify.Sprint(title))
162144

163-
err := g.doc.SetFont(fontName, "", g.opts.documentTitleFontSize)
164-
if err != nil {
165-
return y, err
166-
}
167-
168-
// Calcola larghezza testo
169-
tw, err := g.doc.MeasureTextWidth(title)
170-
if err != nil {
171-
return y, err
172-
}
173-
174-
th, err := g.doc.MeasureCellHeightByText(title)
175-
if err != nil {
176-
return y, err
177-
}
178-
179-
y += th
145+
prefix := ""
146+
y += defaultLineSpacingRatio * g.opts.documentTitleFontSize
147+
return g.renderText(y, title, prefix, g.opts.documentTitleFontSize, true, 53, 57, 53)
148+
}
180149

181-
// Centra il testo orizzontalmente
182-
titleX := (g.opts.pageWidth - tw) / 2
150+
func (g *pdfRenderImpl) handleGroup(y float64, title string) (float64, error) {
151+
prefix := ""
152+
y += defaultLineSpacingRatio * g.opts.groupTitleFontSize
153+
return g.renderText(y, title, prefix, g.opts.groupTitleFontSize, false, 53, 57, 53)
154+
}
183155

184-
g.doc.SetTextColor(112, 128, 144)
156+
func (g *pdfRenderImpl) handleGroupNote(y float64, line string) (float64, error) {
157+
prefix := " "
158+
return g.renderText(y, line, prefix, g.opts.groupNoteFontSize, false, 178, 190, 181)
159+
}
185160

186-
// Posiziona in alto
187-
g.doc.SetX(titleX)
188-
g.doc.SetY(y)
189-
g.doc.Text(title)
161+
func (g *pdfRenderImpl) handleItem(y float64, line string) (float64, error) {
162+
prefix := defaultSymbol
190163

191-
// Sposta `y` sotto il titolo
192-
y += g.opts.documentTitleFontSize + g.opts.groupTitleMargin
164+
return g.renderText(y, line, prefix, g.opts.itemFontSize, false, 54, 69, 79)
165+
}
193166

194-
return y, nil
167+
func (g *pdfRenderImpl) handleItemNote(y float64, line string) (float64, error) {
168+
prefix := strings.Repeat(" ", 5)
169+
return g.renderText(y, line, prefix, g.opts.itemNoteFontSize, false, 132, 136, 139)
195170
}
196171

197-
func (g *pdfRenderImpl) handleItem(y float64, line string) (float64, error) {
198-
err := g.doc.SetFont(fontName, "", g.opts.itemFontSize) // reset to item font
172+
func (rdr *pdfRenderImpl) renderText(y float64, line string, firstPrefix string, fontSize float64, center bool, r, g, b uint8) (float64, error) {
173+
err := rdr.doc.SetFont(defaultFontName, "", fontSize)
199174
if err != nil {
200175
return y, err
201176
}
202177

203-
g.doc.SetTextColor(54, 69, 79)
178+
rdr.doc.SetTextColor(r, g, b)
204179

205-
prefix := fmt.Sprintf(" %s ", symbol)
206-
maxTextWidth := g.opts.pageWidth - 2*g.opts.marginLeft
207-
wrappedLines := wrapTextWithPrefix(g.doc, line, prefix, itemIndent, maxTextWidth)
180+
indent := strings.Repeat(" ", runewidth.StringWidth(firstPrefix))
181+
maxTextWidth := rdr.opts.pageWidth - 2*rdr.opts.marginLeft
182+
wrappedLines := wrapTextWithPrefix(rdr.doc, line, firstPrefix, indent, maxTextWidth)
208183

209-
y += g.opts.itemMargin
184+
deltaY := defaultLineSpacingRatio * fontSize
210185

211186
for _, l := range wrappedLines {
212-
if y+g.opts.itemFontSize+g.opts.lineSpacing > g.opts.pageHeight-g.opts.marginBottom {
213-
g.doc.AddPage()
214-
g.pageCount++
215187

216-
th, err := g.doc.MeasureCellHeightByText(l)
188+
if y+fontSize+2*deltaY > rdr.opts.pageHeight-rdr.opts.marginBottom {
189+
rdr.doc.AddPage()
190+
rdr.pageCount++
191+
y = rdr.opts.marginTop
192+
}
193+
194+
x := rdr.opts.marginLeft
195+
if center {
196+
tw, err := rdr.doc.MeasureTextWidth(l)
217197
if err != nil {
218198
return y, err
219199
}
220200

221-
y = g.opts.marginTop + th
222-
}
223-
224-
g.doc.SetX(g.opts.marginLeft)
225-
g.doc.SetY(y)
226-
g.doc.Text(l)
227-
y += g.opts.itemFontSize + g.opts.itemMargin
228-
}
229-
230-
return y, nil
231-
}
232-
233-
func (g *pdfRenderImpl) handleItemNote(y float64, line string) (float64, error) {
234-
err := g.doc.SetFont(fontName, "", g.opts.itemNoteFontSize) // reset to item font
235-
if err != nil {
236-
return y, err
237-
}
238-
239-
g.doc.SetTextColor(132, 136, 139)
240-
241-
maxTextWidth := g.opts.pageWidth - 2*g.opts.marginLeft
242-
wrappedLines := wrapTextWithPrefix(g.doc, line, itemNoteIndent, itemNoteIndent, maxTextWidth)
243-
244-
for _, l := range wrappedLines {
245-
if y+g.opts.itemNoteFontSize+g.opts.lineSpacing > g.opts.pageHeight-g.opts.marginBottom {
246-
g.doc.AddPage()
247-
g.pageCount++
248-
y = g.opts.marginTop
201+
x = ((rdr.opts.pageWidth - rdr.opts.marginLeft) - tw) / 2
249202
}
250-
251-
g.doc.SetX(g.opts.marginLeft)
252-
g.doc.SetY(y)
253-
g.doc.Text(l)
254-
y += g.opts.itemNoteFontSize + g.opts.itemNoteMargin
203+
rdr.doc.SetX(x)
204+
rdr.doc.SetY(y + deltaY)
205+
rdr.doc.Text(l)
206+
y += (fontSize + 2*deltaY)
255207
}
256208

257209
return y, nil
258210
}
259211

260212
func (g *pdfRenderImpl) savePDF() error {
261-
err := g.doc.SetFont(fontName, "", 8.0)
213+
err := g.doc.SetFont(defaultFontName, "", 8.0)
262214
if err != nil {
263215
return err
264216
}

internal/render/pdf/support.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,68 @@ func wrapTextWithPrefix(doc *gopdf.GoPdf, text, firstPrefix, indent string, maxW
3434
}
3535
return lines
3636
}
37+
38+
func FitText(text string, maxWidth float64, measurer TextMeasurer) (int, bool, error) {
39+
runes := []rune(text)
40+
low, high := 0, len(runes)
41+
var fitCount int
42+
43+
for low <= high {
44+
mid := (low + high) / 2
45+
substr := string(runes[:mid])
46+
width, _, err := measurer.Measure(substr)
47+
if err != nil {
48+
return 0, false, err
49+
}
50+
51+
if width <= maxWidth {
52+
fitCount = mid
53+
low = mid + 1
54+
} else {
55+
high = mid - 1
56+
}
57+
}
58+
59+
//fitText := string(runes[:fitCount])
60+
fitsAll := fitCount == len(runes)
61+
return fitCount, fitsAll, nil
62+
}
63+
64+
type TextMeasurer interface {
65+
Measure(text string) (width float64, height float64, err error)
66+
}
67+
68+
type Margins struct {
69+
Left, Right, Top, Bottom float64
70+
}
71+
72+
func WrapText(text, firstPrefix, indent string, maxWidth float64, measurer TextMeasurer) ([]string, error) {
73+
words := strings.Fields(text)
74+
var lines []string
75+
var currentLine string
76+
prefix := firstPrefix
77+
78+
for _, word := range words {
79+
testLine := strings.TrimSpace(currentLine + " " + word)
80+
width, _, err := measurer.Measure(prefix + testLine)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
if width > maxWidth && currentLine != "" {
86+
lines = append(lines, prefix+currentLine)
87+
currentLine = word
88+
prefix = indent
89+
} else {
90+
if currentLine == "" {
91+
currentLine = word
92+
} else {
93+
currentLine += " " + word
94+
}
95+
}
96+
}
97+
if currentLine != "" {
98+
lines = append(lines, prefix+currentLine)
99+
}
100+
return lines, nil
101+
}

0 commit comments

Comments
 (0)