Skip to content

Commit 90c3a1f

Browse files
author
Olivier Bonnaure
committed
feat: pdf generator support SVG path
1 parent 7982c13 commit 90c3a1f

File tree

3 files changed

+318
-1
lines changed

3 files changed

+318
-1
lines changed

.lua/pdfgenerator.lua

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,312 @@ function PDFGenerator:totalPage()
11621162
return #self.page_list
11631163
end
11641164

1165+
-- Fit an SVG path into a width x height block at current position
1166+
-- signature: pdf:drawSvgPath(width, height, pathData, options)
1167+
function PDFGenerator:drawSvgPath(width, height, pathData, options)
1168+
options = options or {}
1169+
options.strokeColor = options.strokeColor or "000000"
1170+
options.fillColor = options.fillColor or nil
1171+
options.borderWidth = options.borderWidth or 1
1172+
options.align = options.align or "min" -- "min" or "center"
1173+
if options.scaleStroke == nil then options.scaleStroke = true end
1174+
1175+
local x = (self.current_x) + self.margin_x[1]
1176+
local y = self.page_height - self.current_y - self.margin_y[1] - height
1177+
local nts = numberToString or function(n) return ("%.4f"):format(n) end
1178+
local hexToRGB = (self.hexToRGB) and function(h) return self:hexToRGB(h) end or function(h) return {0,0,0} end
1179+
local strokeRGB = hexToRGB(options.strokeColor)
1180+
local fillRGB = options.fillColor and hexToRGB(options.fillColor) or nil
1181+
local content = self.contents[self.current_page_obj]
1182+
1183+
-- helper to parse numbers
1184+
local function parseNumbers(str)
1185+
local nums = {}
1186+
for n in str:gmatch("([+-]?%d*%.?%d+)") do table.insert(nums, tonumber(n)) end
1187+
return nums
1188+
end
1189+
1190+
-- Compute bounding box from path
1191+
local function computeBounds(path)
1192+
local cx, cy = 0, 0
1193+
local sx, sy = 0, 0
1194+
local cpx, cpy = 0,0
1195+
local qx,qy = nil,nil
1196+
local lastCmd
1197+
local minx, miny = math.huge, math.huge
1198+
local maxx, maxy = -math.huge, -math.huge
1199+
local function updateBounds(...) local args = {...} for i=1,#args,2 do local px,py=args[i],args[i+1] if px and py then minx=math.min(minx,px) miny=math.min(miny,py) maxx=math.max(maxx,px) maxy=math.max(maxy,py) end end end
1200+
1201+
for cmd,argsStr in path:gmatch("([MLHVCSQTZmlhvcsqtz])([^MLHVCSQTZmlhvcsqtz]*)") do
1202+
local nums = parseNumbers(argsStr)
1203+
local i=1
1204+
local function lastWasCubic() return lastCmd and (lastCmd:lower()=="c" or lastCmd:lower()=="s") end
1205+
local function lastWasQuad() return lastCmd and (lastCmd:lower()=="q" or lastCmd:lower()=="t") end
1206+
1207+
if cmd=="M" then while i<=#nums do cx,cy=nums[i],nums[i+1];i=i+2; sx,sy=cx,cy; updateBounds(cx,cy); lastCmd="M" end
1208+
elseif cmd=="m" then while i<=#nums do cx,cy=cx+nums[i],cy+nums[i+1];i=i+2; sx,sy=cx,cy; updateBounds(cx,cy); lastCmd="m" end
1209+
elseif cmd=="L" then while i<=#nums do cx,cy=nums[i],nums[i+1];i=i+2; updateBounds(cx,cy); lastCmd="L" end
1210+
elseif cmd=="l" then while i<=#nums do cx,cy=cx+nums[i],cy+nums[i+1];i=i+2; updateBounds(cx,cy); lastCmd="l" end
1211+
elseif cmd=="H" then while i<=#nums do cx=nums[i];i=i+1; updateBounds(cx,cy); lastCmd="H" end
1212+
elseif cmd=="h" then while i<=#nums do cx=cx+nums[i];i=i+1; updateBounds(cx,cy); lastCmd="h" end
1213+
elseif cmd=="V" then while i<=#nums do cy=nums[i];i=i+1; updateBounds(cx,cy); lastCmd="V" end
1214+
elseif cmd=="v" then while i<=#nums do cy=cy+nums[i];i=i+1; updateBounds(cx,cy); lastCmd="v" end
1215+
elseif cmd=="C" then while i<=#nums do local x1,y1,x2,y2,x,y=nums[i],nums[i+1],nums[i+2],nums[i+3],nums[i+4],nums[i+5]; i=i+6; updateBounds(x1,y1,x2,y2,x,y); cpx,cpy=x2,y2; cx,cy=x,y; lastCmd="C" end
1216+
elseif cmd=="c" then while i<=#nums do local x1,y1=cx+nums[i],cy+nums[i+1]; local x2,y2=cx+nums[i+2],cy+nums[i+3]; local x,y=cx+nums[i+4],cy+nums[i+5]; i=i+6; updateBounds(x1,y1,x2,y2,x,y); cpx,cpy=x2,y2; cx,cy=x,y; lastCmd="c" end
1217+
elseif cmd=="S" then while i<=#nums do local x2,y2,x,y=nums[i],nums[i+1],nums[i+2],nums[i+3]; i=i+4; local x1,y1 = lastWasCubic() and (2*cx-cpx), (2*cy-cpy) or cx,cy; updateBounds(x1,y1,x2,y2,x,y); cpx,cpy=x2,y2; cx,cy=x,y; lastCmd="S" end
1218+
elseif cmd=="s" then while i<=#nums do local x2,y2,x,y=cx+nums[i],cy+nums[i+1],cx+nums[i+2],cy+nums[i+3]; i=i+4; local x1,y1 = lastWasCubic() and (2*cx-cpx), (2*cy-cpy) or cx,cy; updateBounds(x1,y1,x2,y2,x,y); cpx,cpy=x2,y2; cx,cy=x,y; lastCmd="s" end
1219+
elseif cmd=="Q" then while i<=#nums do local x1,y1,x,y=nums[i],nums[i+1],nums[i+2],nums[i+3];i=i+4; updateBounds(x1,y1,x,y); qx,qy=x1,y1; cx,cy=x,y; lastCmd="Q" end
1220+
elseif cmd=="q" then while i<=#nums do local x1,y1,x,y=cx+nums[i],cy+nums[i+1],cx+nums[i+2],cy+nums[i+3];i=i+4; updateBounds(x1,y1,x,y); qx,qy=x1,y1; cx,cy=x,y; lastCmd="q" end
1221+
elseif cmd=="T" then while i<=#nums do local x,y=nums[i],nums[i+1];i=i+2; local x1,y1 = lastWasQuad() and qx and (2*cx-qx),(2*cy-qy) or cx,cy; updateBounds(x1,y1,x,y); qx,qy=x1,y1; cx,cy=x,y; lastCmd="T" end
1222+
elseif cmd=="t" then while i<=#nums do local x,y=cx+nums[i],cy+nums[i+1];i=i+2; local x1,y1 = lastWasQuad() and qx and (2*cx-qx),(2*cy-qy) or cx,cy; updateBounds(x1,y1,x,y); qx,qy=x1,y1; cx,cy=x,y; lastCmd="t" end
1223+
elseif cmd=="Z" or cmd=="z" then updateBounds(sx,sy); cx,cy=sx,sy; lastCmd=cmd end
1224+
end
1225+
if minx==math.huge then minx,miny,maxx,maxy=0,0,0,0 end
1226+
return minx,miny,maxx,maxy
1227+
end
1228+
1229+
local minx,miny,maxx,maxy = computeBounds(pathData)
1230+
local origW,origH = maxx-minx,maxy-miny
1231+
if origW==0 then origW=1 end
1232+
if origH==0 then origH=1 end
1233+
local scale = math.min(width/origW, height/origH)
1234+
local offsetX, offsetY = x - minx*scale, y - miny*scale
1235+
if options.align=="center" then
1236+
offsetX = offsetX + (width - origW*scale)/2
1237+
offsetY = offsetY + (height - origH*scale)/2
1238+
end
1239+
1240+
local function transform(px,py) return px*scale+offsetX, py*scale+offsetY end
1241+
1242+
-- === emit path ===
1243+
local strokeW = options.borderWidth
1244+
if options.scaleStroke then strokeW=strokeW*scale end
1245+
if strokeW<=0 then strokeW=options.borderWidth end
1246+
content.stream = content.stream.."q\n"
1247+
content.stream = content.stream..string.format("%s w\n", nts(strokeW))
1248+
if strokeRGB then content.stream = content.stream..string.format("%s %s %s RG\n", nts(strokeRGB[1]), nts(strokeRGB[2]), nts(strokeRGB[3])) end
1249+
if fillRGB then content.stream = content.stream..string.format("%s %s %s rg\n", nts(fillRGB[1]), nts(fillRGB[2]), nts(fillRGB[3])) end
1250+
1251+
-- parse & render
1252+
local cx,cy = 0,0
1253+
local sx,sy = 0,0
1254+
local cpx,cpy=0,0
1255+
local qx,qy=nil,nil
1256+
local lastCmd=nil
1257+
for cmd,argsStr in pathData:gmatch("([MLHVCSQTZmlhvcsqtz])([^MLHVCSQTZmlhvcsqtz]*)") do
1258+
local nums=parseNumbers(argsStr)
1259+
local i=1
1260+
local function lastWasCubic() return lastCmd and (lastCmd:lower()=="c" or lastCmd:lower()=="s") end
1261+
local function lastWasQuad() return lastCmd and (lastCmd:lower()=="q" or lastCmd:lower()=="t") end
1262+
1263+
if cmd == "M" then
1264+
-- first pair is move, subsequent pairs are L
1265+
if #nums >= 2 then
1266+
cx, cy = nums[1], nums[2]; i = 3
1267+
local tx, ty = transform(cx, cy)
1268+
content.stream = content.stream .. string.format("%s %s m\n", nts(tx), nts(ty))
1269+
sx, sy = cx, cy
1270+
lastCmd = "M"
1271+
end
1272+
while i <= #nums do
1273+
cx, cy = nums[i], nums[i+1]; i = i + 2
1274+
local tx, ty = transform(cx, cy)
1275+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1276+
lastCmd = "L"
1277+
end
1278+
1279+
elseif cmd == "m" then
1280+
if #nums >= 2 then
1281+
cx, cy = cx + nums[1], cy + nums[2]; i = 3
1282+
local tx, ty = transform(cx, cy)
1283+
content.stream = content.stream .. string.format("%s %s m\n", nts(tx), nts(ty))
1284+
sx, sy = cx, cy
1285+
lastCmd = "m"
1286+
end
1287+
while i <= #nums do
1288+
cx, cy = cx + nums[i], cy + nums[i+1]; i = i + 2
1289+
local tx, ty = transform(cx, cy)
1290+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1291+
lastCmd = "l"
1292+
end
1293+
1294+
elseif cmd == "L" then
1295+
while i <= #nums do
1296+
cx, cy = nums[i], nums[i+1]; i = i + 2
1297+
local tx, ty = transform(cx, cy)
1298+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1299+
lastCmd = "L"
1300+
end
1301+
elseif cmd == "l" then
1302+
while i <= #nums do
1303+
cx, cy = cx + nums[i], cy + nums[i+1]; i = i + 2
1304+
local tx, ty = transform(cx, cy)
1305+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1306+
lastCmd = "l"
1307+
end
1308+
1309+
elseif cmd == "H" then
1310+
while i <= #nums do
1311+
cx = nums[i]; i = i + 1
1312+
local tx, ty = transform(cx, cy)
1313+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1314+
lastCmd = "H"
1315+
end
1316+
elseif cmd == "h" then
1317+
while i <= #nums do
1318+
cx = cx + nums[i]; i = i + 1
1319+
local tx, ty = transform(cx, cy)
1320+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1321+
lastCmd = "h"
1322+
end
1323+
1324+
elseif cmd == "V" then
1325+
while i <= #nums do
1326+
cy = nums[i]; i = i + 1
1327+
local tx, ty = transform(cx, cy)
1328+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1329+
lastCmd = "V"
1330+
end
1331+
elseif cmd == "v" then
1332+
while i <= #nums do
1333+
cy = cy + nums[i]; i = i + 1
1334+
local tx, ty = transform(cx, cy)
1335+
content.stream = content.stream .. string.format("%s %s l\n", nts(tx), nts(ty))
1336+
lastCmd = "v"
1337+
end
1338+
1339+
elseif cmd == "C" then
1340+
while i <= #nums do
1341+
local x1,y1,x2,y2,x,y = nums[i],nums[i+1],nums[i+2],nums[i+3],nums[i+4],nums[i+5]; i = i + 6
1342+
local tx1,ty1 = transform(x1,y1)
1343+
local tx2,ty2 = transform(x2,y2)
1344+
local tx, ty = transform(x,y)
1345+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1346+
cpx, cpy = x2, y2
1347+
cx, cy = x, y
1348+
lastCmd = "C"
1349+
end
1350+
elseif cmd == "c" then
1351+
while i <= #nums do
1352+
local x1,y1 = cx + nums[i], cy + nums[i+1]
1353+
local x2,y2 = cx + nums[i+2], cy + nums[i+3]
1354+
local x,y = cx + nums[i+4], cy + nums[i+5]; i = i + 6
1355+
local tx1,ty1 = transform(x1,y1)
1356+
local tx2,ty2 = transform(x2,y2)
1357+
local tx, ty = transform(x,y)
1358+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1359+
cpx, cpy = x2, y2
1360+
cx, cy = x, y
1361+
lastCmd = "c"
1362+
end
1363+
1364+
elseif cmd == "S" then
1365+
while i <= #nums do
1366+
local x2,y2,x,y = nums[i],nums[i+1],nums[i+2],nums[i+3]; i = i + 4
1367+
local x1,y1
1368+
if lastWasCubic() then x1,y1 = 2*cx - cpx, 2*cy - cpy else x1,y1 = cx, cy end
1369+
local tx1,ty1 = transform(x1,y1)
1370+
local tx2,ty2 = transform(x2,y2)
1371+
local tx, ty = transform(x,y)
1372+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1373+
cpx, cpy = x2, y2
1374+
cx, cy = x, y
1375+
lastCmd = "S"
1376+
end
1377+
elseif cmd == "s" then
1378+
while i <= #nums do
1379+
local x2,y2,x,y = cx + nums[i], cy + nums[i+1], cx + nums[i+2], cy + nums[i+3]; i = i + 4
1380+
local x1,y1
1381+
if lastWasCubic() then x1,y1 = 2*cx - cpx, 2*cy - cpy else x1,y1 = cx, cy end
1382+
local tx1,ty1 = transform(x1,y1)
1383+
local tx2,ty2 = transform(x2,y2)
1384+
local tx, ty = transform(x,y)
1385+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1386+
cpx, cpy = x2, y2
1387+
cx, cy = x, y
1388+
lastCmd = "s"
1389+
end
1390+
1391+
elseif cmd == "Q" then
1392+
while i <= #nums do
1393+
local x1,y1,x,y = nums[i],nums[i+1],nums[i+2],nums[i+3]; i = i + 4
1394+
-- convert quadratic to cubic:
1395+
local c1x = cx + (2/3) * (x1 - cx)
1396+
local c1y = cy + (2/3) * (y1 - cy)
1397+
local c2x = x + (2/3) * (x1 - x)
1398+
local c2y = y + (2/3) * (y1 - y)
1399+
local tx1,ty1 = transform(c1x,c1y)
1400+
local tx2,ty2 = transform(c2x,c2y)
1401+
local tx, ty = transform(x,y)
1402+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1403+
qx, qy = x1, y1
1404+
cx, cy = x, y
1405+
lastCmd = "Q"
1406+
end
1407+
elseif cmd == "q" then
1408+
while i <= #nums do
1409+
local x1,y1,x,y = cx + nums[i], cy + nums[i+1], cx + nums[i+2], cy + nums[i+3]; i = i + 4
1410+
local c1x = cx + (2/3) * (x1 - cx)
1411+
local c1y = cy + (2/3) * (y1 - cy)
1412+
local c2x = x + (2/3) * (x1 - x)
1413+
local c2y = y + (2/3) * (y1 - y)
1414+
local tx1,ty1 = transform(c1x,c1y)
1415+
local tx2,ty2 = transform(c2x,c2y)
1416+
local tx, ty = transform(x,y)
1417+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1418+
qx, qy = x1, y1
1419+
cx, cy = x, y
1420+
lastCmd = "q"
1421+
end
1422+
1423+
elseif cmd == "T" then
1424+
while i <= #nums do
1425+
local x,y = nums[i], nums[i+1]; i = i + 2
1426+
local x1,y1
1427+
if lastWasQuad() and qx then x1,y1 = 2*cx - qx, 2*cy - qy else x1,y1 = cx, cy end
1428+
local c1x = cx + (2/3) * (x1 - cx)
1429+
local c1y = cy + (2/3) * (y1 - cy)
1430+
local c2x = x + (2/3) * (x1 - x)
1431+
local c2y = y + (2/3) * (y1 - y)
1432+
local tx1,ty1 = transform(c1x,c1y)
1433+
local tx2,ty2 = transform(c2x,c2y)
1434+
local tx, ty = transform(x,y)
1435+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1436+
qx, qy = x1, y1
1437+
cx, cy = x, y
1438+
lastCmd = "T"
1439+
end
1440+
elseif cmd == "t" then
1441+
while i <= #nums do
1442+
local x,y = cx + nums[i], cy + nums[i+1]; i = i + 2
1443+
local x1,y1
1444+
if lastWasQuad() and qx then x1,y1 = 2*cx - qx, 2*cy - qy else x1,y1 = cx, cy end
1445+
local c1x = cx + (2/3) * (x1 - cx)
1446+
local c1y = cy + (2/3) * (y1 - cy)
1447+
local c2x = x + (2/3) * (x1 - x)
1448+
local c2y = y + (2/3) * (y1 - y)
1449+
local tx1,ty1 = transform(c1x,c1y)
1450+
local tx2,ty2 = transform(c2x,c2y)
1451+
local tx, ty = transform(x,y)
1452+
content.stream = content.stream .. string.format("%s %s %s %s %s %s c\n", nts(tx1),nts(ty1), nts(tx2),nts(ty2), nts(tx),nts(ty))
1453+
qx, qy = x1, y1
1454+
cx, cy = x, y
1455+
lastCmd = "t"
1456+
end
1457+
1458+
elseif cmd == "Z" or cmd == "z" then
1459+
-- emit closepath; PDF 'h' closes current subpath
1460+
content.stream = content.stream .. "h\n"
1461+
cx, cy = sx, sy
1462+
lastCmd = cmd
1463+
end
1464+
end
1465+
1466+
if fillRGB then content.stream = content.stream.."B\n" else content.stream = content.stream.."S\n" end
1467+
content.stream = content.stream.."Q\n"
1468+
return self
1469+
end
1470+
11651471
-- Generate PDF and return as string
11661472
function PDFGenerator:generate()
11671473
local output = {}

0 commit comments

Comments
 (0)