Skip to content

Commit cefcbf1

Browse files
author
Olivier Bonnaure
committed
fix: enhance tables
1 parent e5abe92 commit cefcbf1

File tree

1 file changed

+149
-70
lines changed

1 file changed

+149
-70
lines changed

.lua/pdfgenerator.lua

Lines changed: 149 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ function PDFGenerator.new()
3939
current_y = 0,
4040
resources = {},
4141
font_metrics = {},
42+
last_font = nil,
4243
current_table = {
4344
current_row = {
4445
height = nil
4546
},
46-
padding = 5,
47+
padding_x = 5,
48+
padding_y = 5,
4749
header_columns = nil,
4850
data_columns = nil,
4951
header_options = nil,
@@ -112,13 +114,16 @@ function PDFGenerator:addPage(width, height)
112114

113115
--self:drawHeader()
114116
--self:drawFooter()
117+
if self.last_font then
118+
self:useFont(self.last_font.fontFamily, self.last_font.factor)
119+
end
115120

116121
self:setY(0)
117122
self:setX(0)
118123

119124
-- Display table header
120125
if self.current_table.header_columns then
121-
self:drawRowTable(self.current_table.header_columns, { fillColor = "eee" })
126+
self:drawRowTable(self.current_table.header_columns, self.current_table.header_options)
122127
end
123128

124129
return self
@@ -135,18 +140,15 @@ function PDFGenerator:addBasicFont()
135140
end
136141

137142
-- Add custom font (TrueType)
138-
function PDFGenerator:addCustomFont(fontPath, fontName)
143+
function PDFGenerator:addCustomFont(fontPath, fontName, fontWeight)
139144
local fontObj = getNewObjNum()
140145
local fontFileObj = getNewObjNum()
141146
local fontDescObj = getNewObjNum()
142147

143148
-- Read font file
144-
local file = io.open(fontPath, "rb")
145-
if not file then
146-
error("Could not open font file: " .. fontPath)
147-
end
148-
local fontData = file:read("*all")
149-
file:close()
149+
150+
local fontData = LoadAsset(fontPath)
151+
local fontMetrics = LoadAsset(fontPath:gsub("%.ttf$", ".json"))
150152

151153
-- Font descriptor object
152154
self.objects[fontDescObj] = string.format(
@@ -178,12 +180,14 @@ function PDFGenerator:addCustomFont(fontPath, fontName)
178180
self.custom_fonts = {}
179181
end
180182
self.custom_fonts[fontName] = fontObj
181-
182-
return fontObj
183+
self.font_metrics[fontName] = self.font_metrics[fontName] or {}
184+
self.font_metrics[fontName][fontWeight] = DecodeJson(fontMetrics)
183185
end
184186

185187
-- Use custom font for text
186-
function PDFGenerator:useFont(fontName)
188+
function PDFGenerator:useFont(fontName, factor)
189+
factor = factor or 1
190+
187191
if not self.custom_fonts or not self.custom_fonts[fontName] then
188192
error("Font not loaded: " .. fontName)
189193
end
@@ -202,6 +206,8 @@ function PDFGenerator:useFont(fontName)
202206
)
203207
)
204208

209+
self.last_font = { fontFamily = fontName, factor = factor }
210+
205211
return self
206212
end
207213
-- Get text width for current font and size using font metrics
@@ -212,7 +218,7 @@ function PDFGenerator:getTextWidth(text, fontSize, fontWeight)
212218
-- Helvetica character widths (in 1/1000 units of font size)
213219
-- These metrics are from the Adobe Font Metrics (AFM) file for Helvetica
214220
-- Values represent character widths in 1/1000 units of the font size
215-
local helveticaMetrics = {
221+
local fontMetrics = {
216222
normal = {
217223
[32]=278, [33]=278, [34]=355, [35]=556, [36]=556, [37]=889, [38]=667, [39]=191, [40]=333, [41]=333,
218224
[42]=389, [43]=584, [44]=278, [45]=333, [46]=278, [47]=278, [48]=556, [49]=556, [50]=556, [51]=556,
@@ -273,11 +279,16 @@ function PDFGenerator:getTextWidth(text, fontSize, fontWeight)
273279
}
274280
}
275281

282+
if self.font_metrics[self.last_font.fontFamily] then
283+
fontMetrics = self.font_metrics[self.last_font.fontFamily]
284+
end
285+
276286
local width = 0
277287
for i = 1, #text do
278288
local charCode = string.byte(text, i)
279-
local metrics = helveticaMetrics[fontWeight]
280-
width = width + (metrics[charCode] or 556) -- default to 556 for unknown chars
289+
local metrics = fontMetrics[fontWeight]
290+
291+
width = width + (metrics[""..charCode] or 556) -- default to 556 for unknown chars
281292
end
282293

283294
-- Convert from font units (1/1000) to points
@@ -397,24 +408,53 @@ function PDFGenerator:addText(text, fontSize, color, alignment, width)
397408
return self
398409
end
399410

411+
-- Add paragraph to current page
412+
function PDFGenerator:addParagraph(text, options)
413+
options = options or {}
414+
options.fontSize = options.fontSize or 12
415+
options.alignment = options.alignment or "left"
416+
options.width = options.width or (self.page_width - self.margin_x[1] - self.margin_x[2])
417+
options.color = options.color or "000000"
418+
options.width = options.width
419+
420+
local lines = self:splitTextToLines(text, options.fontSize, options.width)
421+
for i, line in ipairs(lines) do
422+
self.current_y = self.current_y + options.fontSize*1.2
423+
if self.out_of_page == false and self.page_height - self.current_y - self.header_height < self.margin_y[1] + self.margin_y[2] then
424+
self:addPage()
425+
end
426+
self:addText(line, options.fontSize, options.color, options.alignment, options.width)
427+
end
428+
return self
429+
end
430+
400431
-- Draw a table
401-
function PDFGenerator:drawTable(options)
432+
function PDFGenerator:drawTable(options, table_options)
402433
options = options or {}
434+
self.current_table = table.merge(self.current_table, table_options or {})
403435
self.current_table.header_columns = options.header_columns
404436
self.current_table.data_columns = options.data_columns
405437
self.current_table.header_options = options.header_options
406438
self.current_table.data_options = options.data_options
407439

408440
self:drawRowTable(options.header_columns, options.header_options)
409-
for _, column in ipairs(options.data_columns) do
441+
for line, column in ipairs(options.data_columns) do
442+
if options.data_options.oddFillColor and line % 2 == 0 then
443+
options.data_options.fillColor = options.data_options.oddFillColor
444+
end
445+
446+
if options.data_options.evenFillColor and line % 2 == 1 then
447+
options.data_options.fillColor = options.data_options.evenFillColor
448+
end
449+
410450
self:drawRowTable(column, options.data_options)
411451
end
412452

413453
self.current_table.header_columns = nil
414454
self.current_table.data_columns = nil
415455
self.current_table.header_options = nil
416456
self.current_table.data_options = nil
417-
self.current_table.current_row = { height = nil, padding = 5}
457+
self.current_table.current_row = { height = nil, padding_x = 5, padding_y = 5}
418458
end
419459

420460
-- Calculate maximum height needed for a collection of text items
@@ -426,14 +466,15 @@ function PDFGenerator:calculateMaxHeight(items)
426466
local text = item.text or ""
427467
local fontSize = item.fontSize or 12
428468
local width = item.width or (self.page_width - self.margin_x[1] - self.margin_x[2])
429-
local padding = item.padding or self.current_table.padding or 5
469+
local padding_x = item.padding_x or self.current_table.padding_x or 5
470+
local padding_y = item.padding_y or self.current_table.padding_y or 5
430471

431472
-- Split text into lines considering the available width
432-
local lines = self:splitTextToLines(text, fontSize, width - (padding * 2))
473+
local lines = self:splitTextToLines(text, fontSize, width - (padding_x * 2))
433474

434475
-- Calculate height for this item
435476
local line_height = fontSize * 1.5 -- Standard line height
436-
local text_height = #lines * line_height + 2 -- + (padding * 2) -- Include padding
477+
local text_height = #lines * line_height + 2 + (padding_y * 2) -- Include padding
437478
-- Update max_height if this item is taller
438479
if text_height > max_height then
439480
max_height = text_height
@@ -485,25 +526,28 @@ function PDFGenerator:drawTableCell(text, options)
485526
options.borderColor = options.borderColor or "000"
486527

487528
-- Draw cell border using existing rectangle method
488-
self:drawRectangle(
489-
options.width,
490-
self.current_table.current_row.height,
491-
options.borderWidth,
492-
"solid",
493-
options.borderColor,
494-
options.fillColor
495-
)
529+
self:drawRectangle({
530+
width = options.width,
531+
height = self.current_table.current_row.height,
532+
borderWidth = options.borderWidth,
533+
borderStyle = "solid",
534+
borderColor = options.borderColor,
535+
fillColor = options.fillColor,
536+
borderSides = options.borderSides;
537+
})
496538

497539
-- Save current position before drawing text
498540
local saved_x = self.current_x
499541
local saved_y = self.current_y
500542

501543
if options.alignment == "left" then
502-
self:moveX(self.current_table.padding)
544+
self:moveX(self.current_table.padding_x)
503545
elseif options.alignment == "right" then
504-
self:moveX(-self.current_table.padding)
546+
self:moveX(-self.current_table.padding_x)
505547
end
506548

549+
self:moveY(self.current_table.padding_y)
550+
507551
self:addParagraph(text, { fontSize = options.fontSize, alignment = options.alignment, width = options.width })
508552

509553
-- Restore cursor position after drawing text
@@ -542,26 +586,6 @@ function PDFGenerator:currentYPos()
542586
return self.page_height - self.margin_y[1] - self.current_y - self.header_height
543587
end
544588

545-
-- Add paragraph to current page
546-
function PDFGenerator:addParagraph(text, options)
547-
options = options or {}
548-
options.fontSize = options.fontSize or 12
549-
options.alignment = options.alignment or "left"
550-
options.width = options.width or (self.page_width - self.margin_x[1] - self.margin_x[2])
551-
options.color = options.color or "000000"
552-
options.width = options.width
553-
554-
local lines = self:splitTextToLines(text, options.fontSize, options.width)
555-
for i, line in ipairs(lines) do
556-
self.current_y = self.current_y + options.fontSize*1.2
557-
if self.out_of_page == false and self.page_height - self.current_y - self.header_height < self.margin_y[1] + self.margin_y[2] then
558-
self:addPage()
559-
end
560-
self:addText(line, options.fontSize, options.color, options.alignment, options.width)
561-
end
562-
return self
563-
end
564-
565589
-- Draw line on current page
566590
function PDFGenerator:drawLine(x1, y1, x2, y2, width)
567591
width = width or 1
@@ -686,13 +710,22 @@ end
686710
-- Draw rectangle on current page
687711
-- borderStyle can be "solid" or "dashed"
688712
-- borderColor and fillColor should be in format {r, g, b} where each value is between 0 and 1
689-
function PDFGenerator:drawRectangle(width, height, borderWidth, borderStyle, borderColor, fillColor)
690-
borderWidth = borderWidth or 1
691-
borderStyle = borderStyle or "solid"
692-
borderColor = borderColor or "000000" -- default gray
693-
borderColor = PDFGenerator:hexToRGB(borderColor)
694-
fillColor = fillColor or "ffffff" -- default gray
695-
fillColor = PDFGenerator:hexToRGB(fillColor)
713+
function PDFGenerator:drawRectangle(options)
714+
Logger(EncodeJson(options.borderSides))
715+
716+
options = options or {}
717+
options.borderWidth = options.borderWidth or 1
718+
options.borderStyle = options.borderStyle or "solid"
719+
options.borderColor = options.borderColor or "000000" -- default gray
720+
options.borderColor = PDFGenerator:hexToRGB(options.borderColor)
721+
options.fillColor = options.fillColor or "ffffff" -- default gray
722+
options.fillColor = PDFGenerator:hexToRGB(options.fillColor)
723+
724+
options.borderSides = options.borderSides or {}
725+
options.borderSides.left = options.borderSides.left or true
726+
options.borderSides.right = options.borderSides.right or true
727+
options.borderSides.top = options.borderSides.top or true
728+
options.borderSides.bottom = options.borderSides.bottom or true
696729

697730
local content = self.contents[self.current_page_obj]
698731

@@ -702,35 +735,81 @@ function PDFGenerator:drawRectangle(width, height, borderWidth, borderStyle, bor
702735
-- Set border color
703736
content.stream = content.stream .. string.format(
704737
"%s %s %s RG\n",
705-
numberToString(borderColor[1]),
706-
numberToString(borderColor[2]),
707-
numberToString(borderColor[3])
738+
numberToString(options.borderColor[1]),
739+
numberToString(options.borderColor[2]),
740+
numberToString(options.borderColor[3])
708741
)
709742

710743
-- Set line width
711-
content.stream = content.stream .. string.format("%s w\n", numberToString(borderWidth))
744+
content.stream = content.stream .. string.format("%s w\n", numberToString(options.borderWidth))
712745

713746
-- Set dash pattern if needed
714-
if borderStyle == "dashed" then
747+
if options.borderStyle == "dashed" then
715748
content.stream = content.stream .. "[3 3] 0 d\n"
716749
end
717750

718751
-- If fill color is provided, set it and draw filled rectangle
719752
content.stream = content.stream .. string.format(
720753
"%s %s %s rg\n",
721-
numberToString(fillColor[1]),
722-
numberToString(fillColor[2]),
723-
numberToString(fillColor[3])
754+
numberToString(options.fillColor[1]),
755+
numberToString(options.fillColor[2]),
756+
numberToString(options.fillColor[3])
724757
)
725758
-- Draw filled and stroked rectangle
726759
content.stream = content.stream .. string.format(
727-
"%s %s %s %s re\nB\n",
760+
"%s %s %s %s re\nf\n",
728761
numberToString(self.margin_x[1] + self.current_x),
729-
numberToString(self.currentYPos(self) - height),
730-
numberToString(width),
731-
numberToString(height)
762+
numberToString(self.currentYPos(self) - options.height),
763+
numberToString(options.width),
764+
numberToString(options.height)
732765
)
733766

767+
-- Draw left border
768+
Logger(EncodeJson(options.borderSides))
769+
if options.borderSides.left == true then
770+
content.stream = content.stream .. string.format(
771+
"%s w\n%s %s m\n%s %s l\nS\n",
772+
numberToString(options.borderWidth),
773+
numberToString(self.margin_x[1] + self.current_x),
774+
numberToString(self.currentYPos(self) - options.height),
775+
numberToString(self.margin_x[1] + self.current_x),
776+
numberToString(self.currentYPos(self))
777+
)
778+
end
779+
780+
if options.borderSides.right == true then
781+
content.stream = content.stream .. string.format(
782+
"%s w\n%s %s m\n%s %s l\nS\n",
783+
numberToString(options.borderWidth),
784+
numberToString(self.margin_x[1] + self.current_x + options.width),
785+
numberToString(self.currentYPos(self) - options.height),
786+
numberToString(self.margin_x[1] + self.current_x + options.width),
787+
numberToString(self.currentYPos(self))
788+
)
789+
end
790+
791+
if options.borderSides.top == true then
792+
content.stream = content.stream .. string.format(
793+
"%s w\n%s %s m\n%s %s l\nS\n",
794+
numberToString(options.borderWidth),
795+
numberToString(self.margin_x[1] + self.current_x),
796+
numberToString(self.currentYPos(self)),
797+
numberToString(self.margin_x[1] + self.current_x + options.width),
798+
numberToString(self.currentYPos(self))
799+
)
800+
end
801+
802+
if options.borderSides.bottom == true then
803+
content.stream = content.stream .. string.format(
804+
"%s w\n%s %s m\n%s %s l\nS\n",
805+
numberToString(options.borderWidth),
806+
numberToString(self.margin_x[1] + self.current_x),
807+
numberToString(self.currentYPos(self) - options.height),
808+
numberToString(self.margin_x[1] + self.current_x + options.width),
809+
numberToString(self.currentYPos(self) - options.height)
810+
)
811+
end
812+
734813
-- Restore graphics state
735814
content.stream = content.stream .. "Q\n"
736815

0 commit comments

Comments
 (0)