@@ -1162,6 +1162,312 @@ function PDFGenerator:totalPage()
11621162 return # self .page_list
11631163end
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
11661472function PDFGenerator :generate ()
11671473 local output = {}
0 commit comments